diff --git a/src/language/helpers/stringUtils.ts b/src/language/helpers/stringUtils.ts new file mode 100644 index 000000000..a2bc03da9 --- /dev/null +++ b/src/language/helpers/stringUtils.ts @@ -0,0 +1,6 @@ +/** + * Based on the given count, returns the singular or plural form of the given word. + */ +export const pluralize = (count: number, singular: string, plural: string = `${singular}s`): string => { + return count === 1 ? singular : plural; +}; diff --git a/src/language/validation/other/types/namedTypes.ts b/src/language/validation/other/types/namedTypes.ts new file mode 100644 index 000000000..0a0adfefc --- /dev/null +++ b/src/language/validation/other/types/namedTypes.ts @@ -0,0 +1,69 @@ +import { SdsNamedType } from '../../../generated/ast.js'; +import { ValidationAcceptor } from 'langium'; +import { SafeDsServices } from '../../../safe-ds-module.js'; +import { typeArgumentsOrEmpty, typeParametersOrEmpty } from '../../../helpers/nodeProperties.js'; +import { duplicatesBy } from '../../../helpers/collectionUtils.js'; +import { pluralize } from '../../../helpers/stringUtils.js'; + +export const CODE_NAMED_TYPE_DUPLICATE_TYPE_PARAMETER = 'named-type/duplicate-type-parameter'; +export const CODE_NAMED_TYPE_POSITIONAL_AFTER_NAMED = 'named-type/positional-after-named'; +export const CODE_NAMED_TYPE_TOO_MANY_TYPE_ARGUMENTS = 'named-type/too-many-type-arguments'; + +export const namedTypeMustNotSetTypeParameterMultipleTimes = (services: SafeDsServices) => { + const nodeMapper = services.helpers.NodeMapper; + const typeArgumentToTypeParameterOrUndefined = nodeMapper.typeArgumentToTypeParameterOrUndefined.bind(nodeMapper); + + return (node: SdsNamedType, accept: ValidationAcceptor): void => { + const typeArguments = typeArgumentsOrEmpty(node.typeArgumentList); + const duplicates = duplicatesBy(typeArguments, typeArgumentToTypeParameterOrUndefined); + + for (const duplicate of duplicates) { + const correspondingTypeParameter = typeArgumentToTypeParameterOrUndefined(duplicate)!; + accept('error', `The type parameter '${correspondingTypeParameter.name}' is already set.`, { + node: duplicate, + code: CODE_NAMED_TYPE_DUPLICATE_TYPE_PARAMETER, + }); + } + }; +}; + +export const namedTypeTypeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments = ( + node: SdsNamedType, + accept: ValidationAcceptor, +): void => { + const typeArgumentList = node.typeArgumentList; + if (!typeArgumentList) { + return; + } + + let foundNamed = false; + for (const typeArgument of typeArgumentList.typeArguments) { + if (typeArgument.typeParameter) { + foundNamed = true; + } else if (foundNamed) { + accept('error', 'After the first named type argument all type arguments must be named.', { + node: typeArgument, + code: CODE_NAMED_TYPE_POSITIONAL_AFTER_NAMED, + }); + } + } +}; + +export const namedTypeMustNotHaveTooManyTypeArguments = (node: SdsNamedType, accept: ValidationAcceptor): void => { + // If the declaration is unresolved, we already show another error + if (!node.declaration.ref) { + return; + } + + const typeParameters = typeParametersOrEmpty(node.declaration.ref); + const typeArguments = typeArgumentsOrEmpty(node.typeArgumentList); + + if (typeArguments.length > typeParameters.length) { + const kind = pluralize(typeParameters.length, 'type argument'); + accept('error', `Expected ${typeParameters.length} ${kind} but got ${typeArguments.length}.`, { + node, + property: 'typeArgumentList', + code: CODE_NAMED_TYPE_TOO_MANY_TYPE_ARGUMENTS, + }); + } +}; diff --git a/src/language/validation/other/types/typeArgumentLists.ts b/src/language/validation/other/types/typeArgumentLists.ts deleted file mode 100644 index aae56f5ce..000000000 --- a/src/language/validation/other/types/typeArgumentLists.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { SdsTypeArgumentList } from '../../../generated/ast.js'; -import { ValidationAcceptor } from 'langium'; - -export const CODE_TYPE_ARGUMENT_LIST_POSITIONAL_AFTER_NAMED = 'type-argument-list/positional-after-named'; - -export const typeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments = ( - node: SdsTypeArgumentList, - accept: ValidationAcceptor, -): void => { - let foundNamed = false; - for (const typeArgument of node.typeArguments) { - if (typeArgument.typeParameter) { - foundNamed = true; - } else if (foundNamed) { - accept('error', 'After the first named type argument all type arguments must be named.', { - node: typeArgument, - code: CODE_TYPE_ARGUMENT_LIST_POSITIONAL_AFTER_NAMED, - }); - } - } -}; diff --git a/src/language/validation/safe-ds-validator.ts b/src/language/validation/safe-ds-validator.ts index 2cdf0ad7a..a41c4bb9d 100644 --- a/src/language/validation/safe-ds-validator.ts +++ b/src/language/validation/safe-ds-validator.ts @@ -34,7 +34,12 @@ import { } from './style.js'; import { templateStringMustHaveExpressionBetweenTwoStringParts } from './other/expressions/templateStrings.js'; import { assignmentAssigneeMustGetValue, yieldMustNotBeUsedInPipeline } from './other/statements/assignments.js'; -import { attributeMustHaveTypeHint, parameterMustHaveTypeHint, resultMustHaveTypeHint } from './types.js'; +import { + attributeMustHaveTypeHint, + namedTypeMustSetAllTypeParameters, + parameterMustHaveTypeHint, + resultMustHaveTypeHint, +} from './types.js'; import { moduleDeclarationsMustMatchFileKind, moduleWithDeclarationsMustStatePackage } from './other/modules.js'; import { typeParameterConstraintLeftOperandMustBeOwnTypeParameter } from './other/declarations/typeParameterConstraints.js'; import { parameterListMustNotHaveRequiredParametersAfterOptionalParameters } from './other/declarations/parameterLists.js'; @@ -43,7 +48,6 @@ import { callableTypeMustNotHaveOptionalParameters, callableTypeParameterMustNotHaveConstModifier, } from './other/types/callableTypes.js'; -import { typeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments } from './other/types/typeArgumentLists.js'; import { argumentListMustNotHavePositionalArgumentsAfterNamedArguments } from './other/argumentLists.js'; import { referenceMustNotBeFunctionPointer, @@ -77,6 +81,11 @@ import { import { memberAccessMustBeNullSafeIfReceiverIsNullable } from './other/expressions/memberAccesses.js'; import { importPackageMustExist, importPackageShouldNotBeEmpty } from './other/imports.js'; import { singleUseAnnotationsMustNotBeRepeated } from './builtins/repeatable.js'; +import { + namedTypeMustNotHaveTooManyTypeArguments, + namedTypeMustNotSetTypeParameterMultipleTimes, + namedTypeTypeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments, +} from './other/types/namedTypes.js'; /** * Register custom validation checks. @@ -135,7 +144,11 @@ export const registerValidationChecks = function (services: SafeDsServices) { SdsNamedType: [ namedTypeDeclarationShouldNotBeDeprecated(services), namedTypeDeclarationShouldNotBeExperimental(services), + namedTypeMustNotHaveTooManyTypeArguments, + namedTypeMustNotSetTypeParameterMultipleTimes(services), + namedTypeMustSetAllTypeParameters(services), namedTypeTypeArgumentListShouldBeNeeded, + namedTypeTypeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments, ], SdsParameter: [ parameterMustHaveTypeHint, @@ -159,7 +172,6 @@ export const registerValidationChecks = function (services: SafeDsServices) { segmentResultListShouldNotBeEmpty, ], SdsTemplateString: [templateStringMustHaveExpressionBetweenTwoStringParts], - SdsTypeArgumentList: [typeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments], SdsTypeParameterConstraint: [typeParameterConstraintLeftOperandMustBeOwnTypeParameter], SdsTypeParameterList: [typeParameterListShouldNotBeEmpty], SdsUnionType: [unionTypeMustHaveTypeArguments, unionTypeShouldNotHaveASingularTypeArgument], diff --git a/src/language/validation/types.ts b/src/language/validation/types.ts index f459b5d7f..af0ccb477 100644 --- a/src/language/validation/types.ts +++ b/src/language/validation/types.ts @@ -1,8 +1,53 @@ import { getContainerOfType, ValidationAcceptor } from 'langium'; -import { isSdsCallable, isSdsLambda, SdsAttribute, SdsParameter, SdsResult } from '../generated/ast.js'; +import { isSdsCallable, isSdsLambda, SdsAttribute, SdsNamedType, SdsParameter, SdsResult } from '../generated/ast.js'; +import { typeArgumentsOrEmpty, typeParametersOrEmpty } from '../helpers/nodeProperties.js'; +import { isEmpty } from 'radash'; +import { SafeDsServices } from '../safe-ds-module.js'; +import { pluralize } from '../helpers/stringUtils.js'; +export const CODE_TYPE_MISSING_TYPE_ARGUMENTS = 'type/missing-type-arguments'; export const CODE_TYPE_MISSING_TYPE_HINT = 'type/missing-type-hint'; +// ----------------------------------------------------------------------------- +// Missing type arguments +// ----------------------------------------------------------------------------- + +export const namedTypeMustSetAllTypeParameters = + (services: SafeDsServices) => + (node: SdsNamedType, accept: ValidationAcceptor): void => { + const expectedTypeParameters = typeParametersOrEmpty(node.declaration.ref); + if (isEmpty(expectedTypeParameters)) { + return; + } + + if (node.typeArgumentList) { + const actualTypeParameters = typeArgumentsOrEmpty(node.typeArgumentList).map((it) => + services.helpers.NodeMapper.typeArgumentToTypeParameterOrUndefined(it), + ); + + const missingTypeParameters = expectedTypeParameters.filter((it) => !actualTypeParameters.includes(it)); + if (!isEmpty(missingTypeParameters)) { + const kind = pluralize(missingTypeParameters.length, 'type parameter'); + const missingTypeParametersString = missingTypeParameters.map((it) => `'${it.name}'`).join(', '); + + accept('error', `The ${kind} ${missingTypeParametersString} must be set here.`, { + node, + property: 'typeArgumentList', + code: CODE_TYPE_MISSING_TYPE_ARGUMENTS, + }); + } + } else { + accept( + 'error', + `The type '${node.declaration.$refText}' is parameterized, so a type argument list must be added.`, + { + node, + code: CODE_TYPE_MISSING_TYPE_ARGUMENTS, + }, + ); + } + }; + // ----------------------------------------------------------------------------- // Missing type hints // ----------------------------------------------------------------------------- diff --git a/tests/language/helpers/stringUtils.test.ts b/tests/language/helpers/stringUtils.test.ts new file mode 100644 index 000000000..e01a25e1f --- /dev/null +++ b/tests/language/helpers/stringUtils.test.ts @@ -0,0 +1,42 @@ +import { describe, expect, it } from 'vitest'; +import { pluralize } from '../../../src/language/helpers/stringUtils.js'; + +describe('pluralize', () => { + it.each([ + { + count: 0, + singular: 'apple', + plural: 'apples', + expected: 'apples', + }, + { + count: 1, + singular: 'apple', + plural: 'apple', + expected: 'apple', + }, + { + count: 2, + singular: 'apple', + plural: 'apples', + expected: 'apples', + }, + { + count: 0, + singular: 'apple', + expected: 'apples', + }, + { + count: 1, + singular: 'apple', + expected: 'apple', + }, + { + count: 2, + singular: 'apple', + expected: 'apples', + }, + ])('should return the singular or plural form based on the count', ({ count, singular, plural, expected }) => { + expect(pluralize(count, singular, plural)).toBe(expected); + }); +}); diff --git a/tests/resources/validation/other/statements/assignments/nothing assigned/main.sdstest b/tests/resources/validation/other/statements/assignments/nothing assigned/main.sdstest index 3cee2dd40..dcd48f85f 100644 --- a/tests/resources/validation/other/statements/assignments/nothing assigned/main.sdstest +++ b/tests/resources/validation/other/statements/assignments/nothing assigned/main.sdstest @@ -58,7 +58,6 @@ segment mySegment() -> ( // $TEST$ error "No value is assigned to this assignee." »val k«, »val l« = unresolved(); - // $TEST$ error "No value is assigned to this assignee." »yield r1« = noResults(); // $TEST$ no error "No value is assigned to this assignee." diff --git a/tests/resources/validation/other/types/type argument lists/duplicate type parameter/main.sdstest b/tests/resources/validation/other/types/type argument lists/duplicate type parameter/main.sdstest new file mode 100644 index 000000000..0cbf53fec --- /dev/null +++ b/tests/resources/validation/other/types/type argument lists/duplicate type parameter/main.sdstest @@ -0,0 +1,36 @@ +package tests.validation.other.types.typeArgumentLists.duplicateTypeParameters + +class MyClass + +fun myFunction( + f: MyClass< + // $TEST$ no error r"The type parameter '\w+' is already set\." + »Int«, + // $TEST$ error "The type parameter 'A' is already set." + »A = Int« + >, + g: MyClass< + // $TEST$ no error r"The type parameter '\w+' is already set\." + »B = Int«, + // $TEST$ error "The type parameter 'B' is already set." + »B = Int« + >, + h: MyClass< + // $TEST$ no error r"The type parameter '\w+' is already set\." + »A = Int«, + // $TEST$ no error r"The type parameter '\w+' is already set\." + »B = Int« + >, + i: MyClass< + // $TEST$ no error r"The type parameter '\w+' is already set\." + »Int«, + // $TEST$ no error r"The type parameter '\w+' is already set\." + »Int« + >, + j: MyClass< + // $TEST$ no error r"The type parameter '\w+' is already set\." + »Unresolved = Int«, + // $TEST$ no error r"The type parameter '\w+' is already set\." + »Unresolved = Int« + > +) diff --git a/tests/resources/validation/other/types/type argument lists/too many type arguments/main.sdstest b/tests/resources/validation/other/types/type argument lists/too many type arguments/main.sdstest new file mode 100644 index 000000000..6b86740a4 --- /dev/null +++ b/tests/resources/validation/other/types/type argument lists/too many type arguments/main.sdstest @@ -0,0 +1,17 @@ +package tests.validation.other.types.typeArgumentLists.tooManyTypeArguments + +class MyClass1 +class MyClass2 + +fun myFunction( + // $TEST$ no error r"Expected \d* type arguments? but got \d*\." + f: MyClass1»<>«, + // $TEST$ no error r"Expected \d* type arguments? but got \d*\." + g: MyClass1»«, + // $TEST$ error "Expected 1 type argument but got 2." + h: MyClass1»«, + // $TEST$ error "Expected 2 type arguments but got 3." + i: MyClass2»«, + // $TEST$ no error r"Expected \d* type arguments? but got \d*\." + j: Unresolved»« +) diff --git a/tests/resources/validation/types/named types/missing type argument list/main.sdstest b/tests/resources/validation/types/named types/missing type argument list/main.sdstest new file mode 100644 index 000000000..602ee7d9b --- /dev/null +++ b/tests/resources/validation/types/named types/missing type argument list/main.sdstest @@ -0,0 +1,38 @@ +package tests.validation.types.namedTypes.missingTypeArgumentList + +class MyClassWithoutTypeParameters +class MyClassWithTypeParameters + +enum MyEnum { + MyEnumVariantWithoutTypeParameters + MyEnumVariantWithTypeParameters +} + +fun myFunction( + // $TEST$ no error r"The type '\w*' is parameterized, so a type argument list must be added\." + a1: »MyClassWithoutTypeParameters«, + // $TEST$ no error r"The type '\w*' is parameterized, so a type argument list must be added\." + b1: »MyClassWithoutTypeParameters«<>, + + // $TEST$ error "The type 'MyClassWithTypeParameters' is parameterized, so a type argument list must be added." + c1: »MyClassWithTypeParameters«, + // $TEST$ no error r"The type '\w*' is parameterized, so a type argument list must be added\." + d1: »MyClassWithTypeParameters«<>, + + + // $TEST$ no error r"The type '\w*' is parameterized, so a type argument list must be added\." + a2: MyEnum.»MyEnumVariantWithoutTypeParameters«, + // $TEST$ no error r"The type '\w*' is parameterized, so a type argument list must be added\." + b2: MyEnum.»MyEnumVariantWithoutTypeParameters«<>, + + // $TEST$ error "The type 'MyEnumVariantWithTypeParameters' is parameterized, so a type argument list must be added." + c2: MyEnum.»MyEnumVariantWithTypeParameters«, + // $TEST$ no error r"The type '\w*' is parameterized, so a type argument list must be added\." + d2: MyEnum.»MyEnumVariantWithTypeParameters«<>, + + + // $TEST$ no error r"The type '\w*' is parameterized, so a type argument list must be added\." + e: »UnresolvedClass«, + // $TEST$ no error r"The type '\w*' is parameterized, so a type argument list must be added\." + f: »UnresolvedClass«<>, +) diff --git a/tests/resources/validation/types/named types/omitted type parameter/main.sdstest b/tests/resources/validation/types/named types/omitted type parameter/main.sdstest new file mode 100644 index 000000000..39580c97f --- /dev/null +++ b/tests/resources/validation/types/named types/omitted type parameter/main.sdstest @@ -0,0 +1,65 @@ + +package tests.validation.types.namedTypes.omittedTypeParameter + +class MyClassWithoutTypeParameter +class MyClassWithOneTypeParameter +class MyClassWithTwoTypeParameters + +enum MyEnum { + MyEnumVariantWithoutTypeParameter + MyEnumVariantWithOneTypeParameter + MyEnumVariantWithTwoTypeParameters +} + +fun myFunction( + // $TEST$ no error r"The type parameters? '\w*' must be set here\." + p1: MyClassWithoutTypeParameter»<>«, + // $TEST$ no error r"The type parameters? '\w*' must be set here\." + p2: MyClassWithoutTypeParameter»«, + // $TEST$ no error r"The type parameters? '\w*' must be set here\." + p3: MyClassWithoutTypeParameter»«, + + // $TEST$ error "The type parameter 'T' must be set here." + p4: MyClassWithOneTypeParameter»<>«, + // $TEST$ no error r"The type parameters? '\w*' must be set here\." + p5: MyClassWithOneTypeParameter»«, + // $TEST$ no error r"The type parameters? '\w*' must be set here\." + p6: MyClassWithOneTypeParameter»«, + + // $TEST$ error "The type parameters 'K', 'V' must be set here." + p7: MyClassWithTwoTypeParameters»<>«, + // $TEST$ error "The type parameter 'V' must be set here." + p8: MyClassWithTwoTypeParameters»«, + // $TEST$ no error r"The type parameters? '\w*' must be set here\." + p9: MyClassWithTwoTypeParameters»«, + + + // $TEST$ no error r"The type parameters? '\w*' must be set here\." + q1: MyEnum.MyEnumVariantWithoutTypeParameter»<>«, + // $TEST$ no error r"The type parameters? '\w*' must be set here\." + q2: MyEnum.MyEnumVariantWithoutTypeParameter»«, + // $TEST$ no error r"The type parameters? '\w*' must be set here\." + q3: MyEnum.MyEnumVariantWithoutTypeParameter»«, + + // $TEST$ error "The type parameter 'T' must be set here." + q4: MyEnum.MyEnumVariantWithOneTypeParameter»<>«, + // $TEST$ no error r"The type parameters? '\w*' must be set here\." + q5: MyEnum.MyEnumVariantWithOneTypeParameter»«, + // $TEST$ no error r"The type parameters? '\w*' must be set here\." + q6: MyEnum.MyEnumVariantWithOneTypeParameter»«, + + // $TEST$ error "The type parameters 'K', 'V' must be set here." + q7: MyEnum.MyEnumVariantWithTwoTypeParameters»<>«, + // $TEST$ error "The type parameter 'V' must be set here." + q8: MyEnum.MyEnumVariantWithTwoTypeParameters»«, + // $TEST$ no error r"The type parameters? '\w*' must be set here\." + q9: MyEnum.MyEnumVariantWithTwoTypeParameters»«, + + + // $TEST$ no error r"The type parameters? '\w*' must be set here\." + a1: Unresolved»<>«, + // $TEST$ no error r"The type parameters? '\w*' must be set here\." + a2: Unresolved»«, + // $TEST$ no error r"The type parameters? '\w*' must be set here\." + a3: Unresolved»«, +)