Skip to content

Commit ae2c03c

Browse files
authored
fix: Fail early on invalid class and file names (serverpod#3288)
1 parent 460d517 commit ae2c03c

File tree

10 files changed

+193
-14
lines changed

10 files changed

+193
-14
lines changed

tools/serverpod_cli/lib/src/analyzer/code_analysis_collector.dart

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import 'package:super_string/super_string.dart';
44
abstract class CodeAnalysisCollector {
55
List<SourceSpanException> get errors;
66

7-
static bool containsSeverErrors(List<SourceSpanException> errors) {
7+
static bool containsSevereErrors(List<SourceSpanException> errors) {
88
return errors.where(
99
(error) {
1010
return error is! SourceSpanSeverityException ||
@@ -14,7 +14,7 @@ abstract class CodeAnalysisCollector {
1414
).isNotEmpty;
1515
}
1616

17-
bool get hasSeverErrors => containsSeverErrors(errors);
17+
bool get hasSevereErrors => containsSevereErrors(errors);
1818

1919
void addError(SourceSpanException error);
2020

tools/serverpod_cli/lib/src/analyzer/models/stateful_analyzer.dart

+4-4
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ class StatefulAnalyzer {
1414
final Map<String, _ModelState> _modelStates = {};
1515

1616
/// Returns true if any of the models have severe errors.
17-
bool get hasSeverErrors => _modelStates.values.any(
18-
(state) => CodeAnalysisCollector.containsSeverErrors(state.errors),
17+
bool get hasSevereErrors => _modelStates.values.any(
18+
(state) => CodeAnalysisCollector.containsSevereErrors(state.errors),
1919
);
2020

2121
Function(Uri, CodeGenerationCollector)? _onErrorsChangedNotifier;
@@ -38,7 +38,7 @@ class StatefulAnalyzer {
3838
List<SerializableModelDefinition> get _validProjectModels => _modelStates
3939
.values
4040
.where(
41-
(state) => !CodeAnalysisCollector.containsSeverErrors(state.errors))
41+
(state) => !CodeAnalysisCollector.containsSevereErrors(state.errors))
4242
.where((state) => state.source.moduleAlias == defaultModuleAlias)
4343
.map((state) => state.model)
4444
.whereType<SerializableModelDefinition>()
@@ -139,7 +139,7 @@ class StatefulAnalyzer {
139139
parsedModels,
140140
);
141141

142-
if (collector.hasSeverErrors) {
142+
if (collector.hasSevereErrors) {
143143
state.errors = collector.errors;
144144
} else {
145145
state.errors = [];

tools/serverpod_cli/lib/src/analyzer/models/validation/model_validator.dart

+13
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,19 @@ List<SourceSpanSeverityException> validateDuplicateFileName(
5151

5252
if (modelDefinition == null) return [];
5353

54+
if (const [
55+
'client', // we are already generating files with these names
56+
'protocol',
57+
'endpoints',
58+
].contains(modelDefinition.fileName)) {
59+
return [
60+
SourceSpanSeverityException(
61+
'The file name "${modelDefinition.fileName}" is reserved and cannot be used.',
62+
documentContents.span,
63+
)
64+
];
65+
}
66+
5467
if (parsedModels.isFilePathUnique(modelDefinition)) {
5568
return [];
5669
}

tools/serverpod_cli/lib/src/analyzer/models/validation/restrictions.dart

+10-1
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,16 @@ class Restrictions {
156156
];
157157
}
158158

159-
var reservedClassNames = const {'List', 'Set', 'Map', 'String', 'DateTime'};
159+
const reservedClassNames = {
160+
'List',
161+
'Set',
162+
'Map',
163+
'String',
164+
'DateTime',
165+
'Client',
166+
'Endpoints',
167+
'Protocol',
168+
};
160169
if (reservedClassNames.contains(className)) {
161170
return [
162171
SourceSpanSeverityException(

tools/serverpod_cli/lib/src/generator/generator.dart

+2-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ Future<bool> performGenerate({
1616
log.debug('Analyzing serializable models in the protocol directory.');
1717

1818
var models = modelAnalyzer.validateAll();
19-
success &= !modelAnalyzer.hasSeverErrors;
19+
success &= !modelAnalyzer.hasSevereErrors;
2020

2121
log.debug('Generating files for serializable models.');
2222

@@ -34,7 +34,7 @@ Future<bool> performGenerate({
3434
changedFiles: generatedModelFiles.toSet(),
3535
);
3636

37-
success &= !endpointAnalyzerCollector.hasSeverErrors;
37+
success &= !endpointAnalyzerCollector.hasSevereErrors;
3838
endpointAnalyzerCollector.printErrors();
3939

4040
log.debug('Generating the protocol.');

tools/serverpod_cli/lib/src/migrations/generator.dart

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ class MigrationGenerator {
6969
var modelDefinitions = StatefulAnalyzer(config, models, (uri, collector) {
7070
collector.printErrors();
7171

72-
if (collector.hasSeverErrors) {
72+
if (collector.hasSevereErrors) {
7373
throw GenerateMigrationDatabaseDefinitionException();
7474
}
7575
}).validateAll();

tools/serverpod_cli/test/analyzer/models/stateful_analyzer/model_validation/file_name_conflicts_test.dart

+33
Original file line numberDiff line numberDiff line change
@@ -91,4 +91,37 @@ void main() {
9191
);
9292
});
9393
});
94+
95+
for (var fileName in ['client', 'endpoints', 'protocol']) {
96+
group(
97+
'Given a model with the reserved file name "$fileName" when analyzing',
98+
() {
99+
late final modelSources = [
100+
ModelSourceBuilder().withFileName(fileName).withYaml(
101+
'''
102+
class: Whatever
103+
fields:
104+
name: String
105+
''',
106+
).build(),
107+
];
108+
109+
late final collector = CodeGenerationCollector();
110+
111+
setUp(() {
112+
StatefulAnalyzer(config, modelSources, onErrorsCollector(collector))
113+
.validateAll();
114+
});
115+
116+
test(
117+
'then an error informs the user that there is a generated file collision.',
118+
() {
119+
var error = collector.errors.first;
120+
expect(
121+
error.message,
122+
'The file name "$fileName" is reserved and cannot be used.',
123+
);
124+
});
125+
});
126+
}
94127
}

tools/serverpod_cli/test/analyzer/models/stateful_analyzer/model_validation/model_types_test.dart

+124
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,130 @@ void main() {
293293
},
294294
);
295295

296+
test(
297+
'Given a class name with reserved value Endpoints, then give an error that the class name is reserved.',
298+
() {
299+
var models = [
300+
ModelSourceBuilder().withYaml(
301+
'''
302+
class: Endpoints
303+
fields:
304+
name: String
305+
''',
306+
).build()
307+
];
308+
309+
var collector = CodeGenerationCollector();
310+
StatefulAnalyzer(config, models, onErrorsCollector(collector))
311+
.validateAll();
312+
313+
expect(
314+
collector.errors,
315+
isNotEmpty,
316+
reason: 'Expected an error but none was generated.',
317+
);
318+
319+
var error = collector.errors.first;
320+
expect(
321+
error.message,
322+
'The class name "Endpoints" is reserved and cannot be used.',
323+
);
324+
},
325+
);
326+
327+
test(
328+
'Given a class name with reserved value DateTime, then give an error that the class name is reserved.',
329+
() {
330+
var models = [
331+
ModelSourceBuilder().withYaml(
332+
'''
333+
class: DateTime
334+
fields:
335+
name: String
336+
''',
337+
).build()
338+
];
339+
340+
var collector = CodeGenerationCollector();
341+
StatefulAnalyzer(config, models, onErrorsCollector(collector))
342+
.validateAll();
343+
344+
expect(
345+
collector.errors,
346+
isNotEmpty,
347+
reason: 'Expected an error but none was generated.',
348+
);
349+
350+
var error = collector.errors.first;
351+
expect(
352+
error.message,
353+
'The class name "DateTime" is reserved and cannot be used.',
354+
);
355+
},
356+
);
357+
358+
test(
359+
'Given a class name with reserved value Protocol, then give an error that the class name is reserved.',
360+
() {
361+
var models = [
362+
ModelSourceBuilder().withYaml(
363+
'''
364+
class: Protocol
365+
fields:
366+
name: String
367+
''',
368+
).build()
369+
];
370+
371+
var collector = CodeGenerationCollector();
372+
StatefulAnalyzer(config, models, onErrorsCollector(collector))
373+
.validateAll();
374+
375+
expect(
376+
collector.errors,
377+
isNotEmpty,
378+
reason: 'Expected an error but none was generated.',
379+
);
380+
381+
var error = collector.errors.first;
382+
expect(
383+
error.message,
384+
'The class name "Protocol" is reserved and cannot be used.',
385+
);
386+
},
387+
);
388+
389+
test(
390+
'Given a class name with reserved value Client, then give an error that the class name is reserved.',
391+
() {
392+
var models = [
393+
ModelSourceBuilder().withYaml(
394+
'''
395+
class: Client
396+
fields:
397+
name: String
398+
''',
399+
).build()
400+
];
401+
402+
var collector = CodeGenerationCollector();
403+
StatefulAnalyzer(config, models, onErrorsCollector(collector))
404+
.validateAll();
405+
406+
expect(
407+
collector.errors,
408+
isNotEmpty,
409+
reason: 'Expected an error but none was generated.',
410+
);
411+
412+
var error = collector.errors.first;
413+
expect(
414+
error.message,
415+
'The class name "Client" is reserved and cannot be used.',
416+
);
417+
},
418+
);
419+
296420
group('Given a model without any defined model type', () {
297421
test(
298422
'Then return a human readable error message informing the user that the model type is missing.',

tools/serverpod_cli/test/analyzer/models/stateful_analyzer/stateful_analyzer_test.dart

+2-2
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ fields:
123123
});
124124

125125
test(
126-
'Given a model with a severe error (invalid syntax), when validating all, then hasSeverErrors returns true',
126+
'Given a model with a severe error (invalid syntax), when validating all, then hasSevereErrors returns true',
127127
() {
128128
var yamlSource = ModelSourceBuilder().withYaml('''''').build();
129129

@@ -133,7 +133,7 @@ fields:
133133
);
134134

135135
statefulAnalyzer.validateAll();
136-
expect(statefulAnalyzer.hasSeverErrors, true);
136+
expect(statefulAnalyzer.hasSevereErrors, true);
137137
});
138138

139139
test(

tools/serverpod_cli/test/util/string_manipulation_test.dart

+2-2
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ void main() {
215215
});
216216

217217
test(
218-
'with a single excaped single quote when splitting then it is recognized as a single token.',
218+
'with a single escaped single quote when splitting then it is recognized as a single token.',
219219
() {
220220
var result = splitIgnoringBracketsAndQuotes(
221221
'controlToken, "This \\\'is a default value", controlToken',
@@ -228,7 +228,7 @@ void main() {
228228
});
229229

230230
test(
231-
'with a single excaped double quote when splitting then it is recognized as a single token.',
231+
'with a single escaped double quote when splitting then it is recognized as a single token.',
232232
() {
233233
var result = splitIgnoringBracketsAndQuotes(
234234
'controlToken, "This \\"is a default value", controlToken',

0 commit comments

Comments
 (0)