Skip to content

Commit 38d1181

Browse files
feat: error if names are not unique (part 2) (#640)
Closes partially #543 ### Summary of Changes Show an error if * module members in the same file have duplicate names, * module members in the same package, but different files have duplicate names, * schema columns have duplicate names. --------- Co-authored-by: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com>
1 parent e0fa032 commit 38d1181

File tree

15 files changed

+521
-19
lines changed

15 files changed

+521
-19
lines changed

src/language/helpers/nodeProperties.ts

+14-6
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import {
2727
SdsCallable,
2828
SdsClass,
2929
SdsClassMember,
30+
SdsColumn,
3031
SdsDeclaration,
3132
SdsEnum,
3233
SdsEnumVariant,
@@ -42,6 +43,7 @@ import {
4243
SdsQualifiedImport,
4344
SdsResult,
4445
SdsResultList,
46+
SdsSchema,
4547
SdsStatement,
4648
SdsType,
4749
SdsTypeArgument,
@@ -132,20 +134,18 @@ export const blockLambdaResultsOrEmpty = (node: SdsBlockLambda | undefined): Sds
132134
.filter(isSdsBlockLambdaResult)
133135
.toArray();
134136
};
135-
export const importedDeclarationsOrEmpty = (node: SdsQualifiedImport | undefined): SdsImportedDeclaration[] => {
136-
return node?.importedDeclarationList?.importedDeclarations ?? [];
137-
};
138137

139-
export const literalsOrEmpty = (node: SdsLiteralType | undefined): SdsLiteral[] => {
140-
return node?.literalList?.literals ?? [];
141-
};
142138
export const classMembersOrEmpty = (
143139
node: SdsClass | undefined,
144140
filterFunction: (member: SdsClassMember) => boolean = () => true,
145141
): SdsClassMember[] => {
146142
return node?.body?.members?.filter(filterFunction) ?? [];
147143
};
148144

145+
export const columnsOrEmpty = (node: SdsSchema | undefined): SdsColumn[] => {
146+
return node?.columnList?.columns ?? [];
147+
};
148+
149149
export const enumVariantsOrEmpty = (node: SdsEnum | undefined): SdsEnumVariant[] => {
150150
return node?.body?.variants ?? [];
151151
};
@@ -154,6 +154,14 @@ export const importsOrEmpty = (node: SdsModule | undefined): SdsImport[] => {
154154
return node?.imports ?? [];
155155
};
156156

157+
export const importedDeclarationsOrEmpty = (node: SdsQualifiedImport | undefined): SdsImportedDeclaration[] => {
158+
return node?.importedDeclarationList?.importedDeclarations ?? [];
159+
};
160+
161+
export const literalsOrEmpty = (node: SdsLiteralType | undefined): SdsLiteral[] => {
162+
return node?.literalList?.literals ?? [];
163+
};
164+
157165
export const moduleMembersOrEmpty = (node: SdsModule | undefined): SdsModuleMember[] => {
158166
return node?.members?.filter(isSdsModuleMember) ?? [];
159167
};

src/language/validation/names.ts

+101-6
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {
2+
isSdsQualifiedImport,
23
SdsAnnotation,
34
SdsBlockLambda,
45
SdsCallableType,
@@ -8,21 +9,32 @@ import {
89
SdsEnumVariant,
910
SdsExpressionLambda,
1011
SdsFunction,
12+
SdsImportedDeclaration,
13+
SdsModule,
1114
SdsPipeline,
15+
SdsSchema,
1216
SdsSegment,
1317
} from '../generated/ast.js';
14-
import { ValidationAcceptor } from 'langium';
18+
import { getDocument, ValidationAcceptor } from 'langium';
1519
import {
1620
blockLambdaResultsOrEmpty,
1721
classMembersOrEmpty,
22+
columnsOrEmpty,
1823
enumVariantsOrEmpty,
24+
importedDeclarationsOrEmpty,
25+
importsOrEmpty,
1926
isStatic,
27+
moduleMembersOrEmpty,
28+
packageNameOrUndefined,
2029
parametersOrEmpty,
2130
placeholdersOrEmpty,
2231
resultsOrEmpty,
2332
typeParametersOrEmpty,
2433
} from '../helpers/nodeProperties.js';
2534
import { duplicatesBy } from '../helpers/collectionUtils.js';
35+
import { isInPipelineFile, isInStubFile, isInTestFile } from '../helpers/fileExtensions.js';
36+
import { declarationIsAllowedInPipelineFile, declarationIsAllowedInStubFile } from './other/modules.js';
37+
import { SafeDsServices } from '../safe-ds-module.js';
2638

2739
export const CODE_NAME_BLOCK_LAMBDA_PREFIX = 'name/block-lambda-prefix';
2840
export const CODE_NAME_CASING = 'name/casing';
@@ -211,6 +223,75 @@ export const functionMustContainUniqueNames = (node: SdsFunction, accept: Valida
211223
);
212224
};
213225

226+
export const moduleMemberMustHaveNameThatIsUniqueInPackage = (services: SafeDsServices) => {
227+
const packageManager = services.workspace.PackageManager;
228+
229+
return (node: SdsModule, accept: ValidationAcceptor): void => {
230+
for (const member of moduleMembersOrEmpty(node)) {
231+
const packageName = packageNameOrUndefined(member) ?? '';
232+
const declarationsInPackage = packageManager.getDeclarationsInPackage(packageName);
233+
const memberUri = getDocument(member).uri?.toString();
234+
235+
if (
236+
declarationsInPackage.some((it) => it.name === member.name && it.documentUri.toString() !== memberUri)
237+
) {
238+
accept('error', `Multiple declarations in this package have the name '${member.name}'.`, {
239+
node: member,
240+
property: 'name',
241+
code: CODE_NAME_DUPLICATE,
242+
});
243+
}
244+
}
245+
};
246+
};
247+
248+
export const moduleMustContainUniqueNames = (node: SdsModule, accept: ValidationAcceptor): void => {
249+
// Names of imported declarations must be unique
250+
const importedDeclarations = importsOrEmpty(node).filter(isSdsQualifiedImport).flatMap(importedDeclarationsOrEmpty);
251+
for (const duplicate of duplicatesBy(importedDeclarations, importedDeclarationName)) {
252+
if (duplicate.alias) {
253+
accept('error', `A declaration with name '${importedDeclarationName(duplicate)}' was imported already.`, {
254+
node: duplicate.alias,
255+
property: 'alias',
256+
code: CODE_NAME_DUPLICATE,
257+
});
258+
} else {
259+
accept('error', `A declaration with name '${importedDeclarationName(duplicate)}' was imported already.`, {
260+
node: duplicate,
261+
property: 'declaration',
262+
code: CODE_NAME_DUPLICATE,
263+
});
264+
}
265+
}
266+
267+
// Names of module members must be unique
268+
if (isInPipelineFile(node)) {
269+
namesMustBeUnique(
270+
moduleMembersOrEmpty(node),
271+
(name) => `A declaration with name '${name}' exists already in this file.`,
272+
accept,
273+
declarationIsAllowedInPipelineFile,
274+
);
275+
} else if (isInStubFile(node)) {
276+
namesMustBeUnique(
277+
moduleMembersOrEmpty(node),
278+
(name) => `A declaration with name '${name}' exists already in this file.`,
279+
accept,
280+
declarationIsAllowedInStubFile,
281+
);
282+
} else if (isInTestFile(node)) {
283+
namesMustBeUnique(
284+
moduleMembersOrEmpty(node),
285+
(name) => `A declaration with name '${name}' exists already in this file.`,
286+
accept,
287+
);
288+
}
289+
};
290+
291+
const importedDeclarationName = (node: SdsImportedDeclaration | undefined): string | undefined => {
292+
return node?.alias?.alias ?? node?.declaration.ref?.name;
293+
};
294+
214295
export const pipelineMustContainUniqueNames = (node: SdsPipeline, accept: ValidationAcceptor): void => {
215296
namesMustBeUnique(
216297
placeholdersOrEmpty(node.body),
@@ -219,6 +300,17 @@ export const pipelineMustContainUniqueNames = (node: SdsPipeline, accept: Valida
219300
);
220301
};
221302

303+
export const schemaMustContainUniqueNames = (node: SdsSchema, accept: ValidationAcceptor): void => {
304+
const duplicates = duplicatesBy(columnsOrEmpty(node), (it) => it.columnName.value);
305+
for (const duplicate of duplicates) {
306+
accept('error', `A column with name '${duplicate.columnName.value}' exists already.`, {
307+
node: duplicate,
308+
property: 'columnName',
309+
code: CODE_NAME_DUPLICATE,
310+
});
311+
}
312+
};
313+
222314
export const segmentMustContainUniqueNames = (node: SdsSegment, accept: ValidationAcceptor): void => {
223315
const parametersAndPlaceholder = [...parametersOrEmpty(node), ...placeholdersOrEmpty(node.body)];
224316
namesMustBeUnique(
@@ -238,12 +330,15 @@ const namesMustBeUnique = (
238330
nodes: Iterable<SdsDeclaration>,
239331
createMessage: (name: string) => string,
240332
accept: ValidationAcceptor,
333+
shouldReportErrorOn: (node: SdsDeclaration) => boolean = () => true,
241334
): void => {
242335
for (const node of duplicatesBy(nodes, (it) => it.name)) {
243-
accept('error', createMessage(node.name), {
244-
node,
245-
property: 'name',
246-
code: CODE_NAME_DUPLICATE,
247-
});
336+
if (shouldReportErrorOn(node)) {
337+
accept('error', createMessage(node.name), {
338+
node,
339+
property: 'name',
340+
code: CODE_NAME_DUPLICATE,
341+
});
342+
}
248343
}
249344
};

src/language/validation/other/modules.ts

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ValidationAcceptor } from 'langium';
2-
import { isSdsDeclaration, isSdsPipeline, isSdsSegment, SdsModule } from '../../generated/ast.js';
2+
import { isSdsDeclaration, isSdsPipeline, isSdsSegment, SdsDeclaration, SdsModule } from '../../generated/ast.js';
33
import { isInPipelineFile, isInStubFile } from '../../helpers/fileExtensions.js';
44

55
export const CODE_MODULE_MISSING_PACKAGE = 'module/missing-package';
@@ -24,7 +24,7 @@ export const moduleDeclarationsMustMatchFileKind = (node: SdsModule, accept: Val
2424

2525
if (isInPipelineFile(node)) {
2626
for (const declaration of declarations) {
27-
if (!isSdsPipeline(declaration) && !isSdsSegment(declaration)) {
27+
if (!declarationIsAllowedInPipelineFile(declaration)) {
2828
accept('error', 'A pipeline file must only declare pipelines and segments.', {
2929
node: declaration,
3030
property: 'name',
@@ -34,7 +34,7 @@ export const moduleDeclarationsMustMatchFileKind = (node: SdsModule, accept: Val
3434
}
3535
} else if (isInStubFile(node)) {
3636
for (const declaration of declarations) {
37-
if (isSdsPipeline(declaration) || isSdsSegment(declaration)) {
37+
if (!declarationIsAllowedInStubFile(declaration)) {
3838
accept('error', 'A stub file must not declare pipelines or segments.', {
3939
node: declaration,
4040
property: 'name',
@@ -44,3 +44,11 @@ export const moduleDeclarationsMustMatchFileKind = (node: SdsModule, accept: Val
4444
}
4545
}
4646
};
47+
48+
export const declarationIsAllowedInPipelineFile = (declaration: SdsDeclaration): boolean => {
49+
return isSdsPipeline(declaration) || isSdsSegment(declaration);
50+
};
51+
52+
export const declarationIsAllowedInStubFile = (declaration: SdsDeclaration): boolean => {
53+
return !isSdsPipeline(declaration) && !isSdsSegment(declaration);
54+
};

src/language/validation/safe-ds-validator.ts

+10-1
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@ import {
1010
enumVariantMustContainUniqueNames,
1111
expressionLambdaMustContainUniqueNames,
1212
functionMustContainUniqueNames,
13+
moduleMemberMustHaveNameThatIsUniqueInPackage,
14+
moduleMustContainUniqueNames,
1315
nameMustNotStartWithBlockLambdaPrefix,
1416
nameShouldHaveCorrectCasing,
1517
pipelineMustContainUniqueNames,
18+
schemaMustContainUniqueNames,
1619
segmentMustContainUniqueNames,
1720
} from './names.js';
1821
import {
@@ -156,7 +159,12 @@ export const registerValidationChecks = function (services: SafeDsServices) {
156159
memberAccessMustBeNullSafeIfReceiverIsNullable(services),
157160
memberAccessNullSafetyShouldBeNeeded(services),
158161
],
159-
SdsModule: [moduleDeclarationsMustMatchFileKind, moduleWithDeclarationsMustStatePackage],
162+
SdsModule: [
163+
moduleDeclarationsMustMatchFileKind,
164+
moduleMemberMustHaveNameThatIsUniqueInPackage(services),
165+
moduleMustContainUniqueNames,
166+
moduleWithDeclarationsMustStatePackage,
167+
],
160168
SdsNamedType: [
161169
namedTypeDeclarationShouldNotBeDeprecated(services),
162170
namedTypeDeclarationShouldNotBeExperimental(services),
@@ -181,6 +189,7 @@ export const registerValidationChecks = function (services: SafeDsServices) {
181189
referenceTargetShouldNotExperimental(services),
182190
],
183191
SdsResult: [resultMustHaveTypeHint],
192+
SdsSchema: [schemaMustContainUniqueNames],
184193
SdsSegment: [
185194
segmentMustContainUniqueNames,
186195
segmentParameterShouldBeUsed(services),

src/resources/builtins/safeds/lang/coreClasses.sdsstub

-3
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,6 @@ class Int sub Number
1818
@Description("A floating-point number.")
1919
class Float sub Number
2020

21-
@Description("A floating-point number.")
22-
class Float sub Number
23-
2421
@Description("A list of elements.")
2522
class List<E>
2623

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package tests.validation.names.acrossFiles
2+
3+
// $TEST$ error "Multiple declarations in this package have the name 'DuplicateAnnotation'."
4+
annotation »DuplicateAnnotation«
5+
// $TEST$ error "Multiple declarations in this package have the name 'DuplicateClass'."
6+
class »DuplicateClass«
7+
// $TEST$ error "Multiple declarations in this package have the name 'DuplicateEnum'."
8+
enum »DuplicateEnum«
9+
// $TEST$ error "Multiple declarations in this package have the name 'duplicateFunction'."
10+
fun »duplicateFunction«()
11+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
12+
pipeline »duplicatePipeline« {}
13+
// $TEST$ error "Multiple declarations in this package have the name 'DuplicateSchema'."
14+
schema »DuplicateSchema« {}
15+
// $TEST$ error "Multiple declarations in this package have the name 'duplicatePublicSegment'."
16+
segment »duplicatePublicSegment«() {}
17+
// $TEST$ error "Multiple declarations in this package have the name 'duplicateInternalSegment'."
18+
internal segment »duplicateInternalSegment«() {}
19+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
20+
private segment »duplicatePrivateSegment«() {}
21+
22+
23+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
24+
annotation »UniqueAnnotation«
25+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
26+
class »UniqueClass«
27+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
28+
enum »UniqueEnum«
29+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
30+
fun »uniqueFunction«()
31+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
32+
pipeline »uniquePipeline« {}
33+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
34+
schema »UniqueSchema« {}
35+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
36+
segment »uniquePublicSegment«() {}
37+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
38+
internal segment »uniqueInternalSegment«() {}
39+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
40+
private segment »uniquePrivateSegment«() {}
41+
42+
43+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
44+
annotation »MyAnnotation«
45+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
46+
annotation »MyAnnotation«
47+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
48+
class »MyClass«
49+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
50+
class »MyClass«
51+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
52+
enum »MyEnum«
53+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
54+
enum »MyEnum«
55+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
56+
fun »myFunction«()
57+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
58+
fun »myFunction«()
59+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
60+
pipeline »myPipeline« {}
61+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
62+
pipeline »myPipeline« {}
63+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
64+
schema »MySchema« {}
65+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
66+
schema »MySchema« {}
67+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
68+
segment »myPublicSegment«() {}
69+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
70+
segment »myPublicSegment«() {}
71+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
72+
internal segment »myInternalSegment«() {}
73+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
74+
internal segment »myInternalSegment«() {}
75+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
76+
private segment »myPrivateSegment«() {}
77+
// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\."
78+
private segment »myPrivateSegment«() {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package tests.validation.names.acrossFiles.other
2+
3+
annotation UniqueAnnotation
4+
class UniqueClass
5+
enum UniqueEnum
6+
fun uniqueFunction()
7+
pipeline uniquePipeline {}
8+
schema UniqueSchema {}
9+
segment uniquePublicSegment() {}
10+
internal segment uniqueInternalSegment() {}
11+
private segment uniquePrivateSegment() {}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
package tests.validation.names.acrossFiles
2+
3+
annotation DuplicateAnnotation
4+
class DuplicateClass
5+
enum DuplicateEnum
6+
fun duplicateFunction()
7+
pipeline duplicatePipeline {}
8+
schema DuplicateSchema {}
9+
segment duplicatePublicSegment() {}
10+
internal segment duplicateInternalSegment() {}
11+
private segment duplicatePrivateSegment() {}

0 commit comments

Comments
 (0)