diff --git a/src/language/validation/names.ts b/src/language/validation/names.ts index bafa7715e..6357ffb48 100644 --- a/src/language/validation/names.ts +++ b/src/language/validation/names.ts @@ -15,7 +15,7 @@ import { SdsSchema, SdsSegment, } from '../generated/ast.js'; -import { ValidationAcceptor } from 'langium'; +import { getDocument, ValidationAcceptor } from 'langium'; import { blockLambdaResultsOrEmpty, classMembersOrEmpty, @@ -25,6 +25,7 @@ import { importsOrEmpty, isStatic, moduleMembersOrEmpty, + packageNameOrUndefined, parametersOrEmpty, placeholdersOrEmpty, resultsOrEmpty, @@ -33,6 +34,7 @@ import { import { duplicatesBy } from '../helpers/collectionUtils.js'; import { isInPipelineFile, isInStubFile, isInTestFile } from '../helpers/fileExtensions.js'; import { declarationIsAllowedInPipelineFile, declarationIsAllowedInStubFile } from './other/modules.js'; +import { SafeDsServices } from '../safe-ds-module.js'; export const CODE_NAME_BLOCK_LAMBDA_PREFIX = 'name/block-lambda-prefix'; export const CODE_NAME_CASING = 'name/casing'; @@ -146,53 +148,6 @@ const acceptCasingWarning = ( // Uniqueness // ----------------------------------------------------------------------------- -export const modulesMustContainUniqueNames = (node: SdsModule, accept: ValidationAcceptor): void => { - // Names of imported declarations must be unique - const importedDeclarations = importsOrEmpty(node).filter(isSdsQualifiedImport).flatMap(importedDeclarationsOrEmpty); - for (const duplicate of duplicatesBy(importedDeclarations, importedDeclarationName)) { - if (duplicate.alias) { - accept('error', `A declaration with name '${importedDeclarationName(duplicate)}' was imported already.`, { - node: duplicate.alias, - property: 'alias', - code: CODE_NAME_DUPLICATE, - }); - } else { - accept('error', `A declaration with name '${importedDeclarationName(duplicate)}' was imported already.`, { - node: duplicate, - property: 'declaration', - code: CODE_NAME_DUPLICATE, - }); - } - } - - // Names of module members must be unique - if (isInPipelineFile(node)) { - namesMustBeUnique( - moduleMembersOrEmpty(node), - (name) => `A declaration with name '${name}' exists already in this file.`, - accept, - declarationIsAllowedInPipelineFile, - ); - } else if (isInStubFile(node)) { - namesMustBeUnique( - moduleMembersOrEmpty(node), - (name) => `A declaration with name '${name}' exists already in this file.`, - accept, - declarationIsAllowedInStubFile, - ); - } else if (isInTestFile(node)) { - namesMustBeUnique( - moduleMembersOrEmpty(node), - (name) => `A declaration with name '${name}' exists already in this file.`, - accept, - ); - } -}; - -const importedDeclarationName = (node: SdsImportedDeclaration | undefined): string | undefined => { - return node?.alias?.alias ?? node?.declaration.ref?.name; -}; - export const annotationMustContainUniqueNames = (node: SdsAnnotation, accept: ValidationAcceptor): void => { namesMustBeUnique(parametersOrEmpty(node), (name) => `A parameter with name '${name}' exists already.`, accept); }; @@ -268,6 +223,75 @@ export const functionMustContainUniqueNames = (node: SdsFunction, accept: Valida ); }; +export const moduleMemberMustHaveNameThatIsUniqueInPackage = (services: SafeDsServices) => { + const packageManager = services.workspace.PackageManager; + + return (node: SdsModule, accept: ValidationAcceptor): void => { + for (const member of moduleMembersOrEmpty(node)) { + const packageName = packageNameOrUndefined(member) ?? ''; + const declarationsInPackage = packageManager.getDeclarationsInPackage(packageName); + const memberUri = getDocument(member).uri?.toString(); + + if ( + declarationsInPackage.some((it) => it.name === member.name && it.documentUri.toString() !== memberUri) + ) { + accept('error', `Multiple declarations in this package have the name '${member.name}'.`, { + node: member, + property: 'name', + code: CODE_NAME_DUPLICATE, + }); + } + } + }; +}; + +export const moduleMustContainUniqueNames = (node: SdsModule, accept: ValidationAcceptor): void => { + // Names of imported declarations must be unique + const importedDeclarations = importsOrEmpty(node).filter(isSdsQualifiedImport).flatMap(importedDeclarationsOrEmpty); + for (const duplicate of duplicatesBy(importedDeclarations, importedDeclarationName)) { + if (duplicate.alias) { + accept('error', `A declaration with name '${importedDeclarationName(duplicate)}' was imported already.`, { + node: duplicate.alias, + property: 'alias', + code: CODE_NAME_DUPLICATE, + }); + } else { + accept('error', `A declaration with name '${importedDeclarationName(duplicate)}' was imported already.`, { + node: duplicate, + property: 'declaration', + code: CODE_NAME_DUPLICATE, + }); + } + } + + // Names of module members must be unique + if (isInPipelineFile(node)) { + namesMustBeUnique( + moduleMembersOrEmpty(node), + (name) => `A declaration with name '${name}' exists already in this file.`, + accept, + declarationIsAllowedInPipelineFile, + ); + } else if (isInStubFile(node)) { + namesMustBeUnique( + moduleMembersOrEmpty(node), + (name) => `A declaration with name '${name}' exists already in this file.`, + accept, + declarationIsAllowedInStubFile, + ); + } else if (isInTestFile(node)) { + namesMustBeUnique( + moduleMembersOrEmpty(node), + (name) => `A declaration with name '${name}' exists already in this file.`, + accept, + ); + } +}; + +const importedDeclarationName = (node: SdsImportedDeclaration | undefined): string | undefined => { + return node?.alias?.alias ?? node?.declaration.ref?.name; +}; + export const pipelineMustContainUniqueNames = (node: SdsPipeline, accept: ValidationAcceptor): void => { namesMustBeUnique( placeholdersOrEmpty(node.body), diff --git a/src/language/validation/safe-ds-validator.ts b/src/language/validation/safe-ds-validator.ts index ab2bf0579..1afecb66c 100644 --- a/src/language/validation/safe-ds-validator.ts +++ b/src/language/validation/safe-ds-validator.ts @@ -9,10 +9,12 @@ import { enumMustContainUniqueNames, enumVariantMustContainUniqueNames, expressionLambdaMustContainUniqueNames, - functionMustContainUniqueNames, modulesMustContainUniqueNames, + functionMustContainUniqueNames, moduleMemberMustHaveNameThatIsUniqueInPackage, + moduleMustContainUniqueNames, nameMustNotStartWithBlockLambdaPrefix, nameShouldHaveCorrectCasing, - pipelineMustContainUniqueNames, schemaMustContainUniqueNames, + pipelineMustContainUniqueNames, + schemaMustContainUniqueNames, segmentMustContainUniqueNames, } from './names.js'; import { @@ -158,8 +160,9 @@ export const registerValidationChecks = function (services: SafeDsServices) { ], SdsModule: [ moduleDeclarationsMustMatchFileKind, - modulesMustContainUniqueNames, - moduleWithDeclarationsMustStatePackage + moduleMemberMustHaveNameThatIsUniqueInPackage(services), + moduleMustContainUniqueNames, + moduleWithDeclarationsMustStatePackage, ], SdsNamedType: [ namedTypeDeclarationShouldNotBeDeprecated(services), diff --git a/tests/resources/validation/names/duplicates/across files b/tests/resources/validation/names/duplicates/across files deleted file mode 100644 index e69de29bb..000000000 diff --git a/tests/resources/validation/names/duplicates/across files/main.sdstest b/tests/resources/validation/names/duplicates/across files/main.sdstest new file mode 100644 index 000000000..433eb3458 --- /dev/null +++ b/tests/resources/validation/names/duplicates/across files/main.sdstest @@ -0,0 +1,78 @@ +package tests.validation.names.acrossFiles + +// $TEST$ error "Multiple declarations in this package have the name 'DuplicateAnnotation'." +annotation »DuplicateAnnotation« +// $TEST$ error "Multiple declarations in this package have the name 'DuplicateClass'." +class »DuplicateClass« +// $TEST$ error "Multiple declarations in this package have the name 'DuplicateEnum'." +enum »DuplicateEnum« +// $TEST$ error "Multiple declarations in this package have the name 'duplicateFunction'." +fun »duplicateFunction«() +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +pipeline »duplicatePipeline« {} +// $TEST$ error "Multiple declarations in this package have the name 'DuplicateSchema'." +schema »DuplicateSchema« {} +// $TEST$ error "Multiple declarations in this package have the name 'duplicatePublicSegment'." +segment »duplicatePublicSegment«() {} +// $TEST$ error "Multiple declarations in this package have the name 'duplicateInternalSegment'." +internal segment »duplicateInternalSegment«() {} +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +private segment »duplicatePrivateSegment«() {} + + +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +annotation »UniqueAnnotation« +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +class »UniqueClass« +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +enum »UniqueEnum« +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +fun »uniqueFunction«() +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +pipeline »uniquePipeline« {} +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +schema »UniqueSchema« {} +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +segment »uniquePublicSegment«() {} +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +internal segment »uniqueInternalSegment«() {} +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +private segment »uniquePrivateSegment«() {} + + +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +annotation »MyAnnotation« +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +annotation »MyAnnotation« +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +class »MyClass« +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +class »MyClass« +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +enum »MyEnum« +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +enum »MyEnum« +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +fun »myFunction«() +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +fun »myFunction«() +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +pipeline »myPipeline« {} +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +pipeline »myPipeline« {} +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +schema »MySchema« {} +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +schema »MySchema« {} +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +segment »myPublicSegment«() {} +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +segment »myPublicSegment«() {} +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +internal segment »myInternalSegment«() {} +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +internal segment »myInternalSegment«() {} +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +private segment »myPrivateSegment«() {} +// $TEST$ no error r"Multiple declarations in this package have the name '\w*'\." +private segment »myPrivateSegment«() {} diff --git a/tests/resources/validation/names/duplicates/across files/other package.sdstest b/tests/resources/validation/names/duplicates/across files/other package.sdstest new file mode 100644 index 000000000..c859e3989 --- /dev/null +++ b/tests/resources/validation/names/duplicates/across files/other package.sdstest @@ -0,0 +1,11 @@ +package tests.validation.names.acrossFiles.other + +annotation UniqueAnnotation +class UniqueClass +enum UniqueEnum +fun uniqueFunction() +pipeline uniquePipeline {} +schema UniqueSchema {} +segment uniquePublicSegment() {} +internal segment uniqueInternalSegment() {} +private segment uniquePrivateSegment() {} diff --git a/tests/resources/validation/names/duplicates/across files/same package.sdstest b/tests/resources/validation/names/duplicates/across files/same package.sdstest new file mode 100644 index 000000000..04c8a4401 --- /dev/null +++ b/tests/resources/validation/names/duplicates/across files/same package.sdstest @@ -0,0 +1,11 @@ +package tests.validation.names.acrossFiles + +annotation DuplicateAnnotation +class DuplicateClass +enum DuplicateEnum +fun duplicateFunction() +pipeline duplicatePipeline {} +schema DuplicateSchema {} +segment duplicatePublicSegment() {} +internal segment duplicateInternalSegment() {} +private segment duplicatePrivateSegment() {} diff --git a/tests/resources/validation/names/duplicates/in test file/main.sdstest b/tests/resources/validation/names/duplicates/in test file/main.sdstest index b00c62de9..acd73161c 100644 --- a/tests/resources/validation/names/duplicates/in test file/main.sdstest +++ b/tests/resources/validation/names/duplicates/in test file/main.sdstest @@ -47,11 +47,11 @@ fun »duplicateFunction«() fun »duplicateFunction«() // $TEST$ no error r"A declaration with name '\w*' exists already in this file\." -pipeline »uniqueTest« {} +pipeline »uniquePipeline« {} // $TEST$ no error r"A declaration with name '\w*' exists already in this file\." -pipeline »duplicateTest« {} -// $TEST$ error "A declaration with name 'duplicateTest' exists already in this file." -pipeline »duplicateTest« {} +pipeline »duplicatePipeline« {} +// $TEST$ error "A declaration with name 'duplicatePipeline' exists already in this file." +pipeline »duplicatePipeline« {} // $TEST$ no error r"A declaration with name '\w*' exists already in this file\." schema »UniqueSchema« {}