From a8c176051f9d2b76bd049ca1d6f2a9bdfcf845b3 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Wed, 11 Oct 2023 13:36:26 +0200 Subject: [PATCH 1/5] feat: error if type argument list is missing --- src/language/validation/safe-ds-validator.ts | 9 ++++- src/language/validation/types.ts | 32 +++++++++++++++- .../assignments/nothing assigned/main.sdstest | 1 - .../missing type argument list/main.sdstest | 38 +++++++++++++++++++ 4 files changed, 77 insertions(+), 3 deletions(-) create mode 100644 tests/resources/validation/types/named types/missing type argument list/main.sdstest diff --git a/src/language/validation/safe-ds-validator.ts b/src/language/validation/safe-ds-validator.ts index 2cdf0ad7a..d28fb6da2 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, + namedTypeMustHaveTypeArgumentListIfTypeIsParameterized, 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'; @@ -135,6 +140,8 @@ export const registerValidationChecks = function (services: SafeDsServices) { SdsNamedType: [ namedTypeDeclarationShouldNotBeDeprecated(services), namedTypeDeclarationShouldNotBeExperimental(services), + namedTypeMustHaveTypeArgumentListIfTypeIsParameterized, + namedTypeMustSetAllTypeParameters, namedTypeTypeArgumentListShouldBeNeeded, ], SdsParameter: [ diff --git a/src/language/validation/types.ts b/src/language/validation/types.ts index f459b5d7f..ecdb61a8a 100644 --- a/src/language/validation/types.ts +++ b/src/language/validation/types.ts @@ -1,8 +1,38 @@ 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 { typeParametersOrEmpty } from '../helpers/nodeProperties.js'; +import { isEmpty } from 'radash'; +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 namedTypeMustHaveTypeArgumentListIfTypeIsParameterized = ( + node: SdsNamedType, + accept: ValidationAcceptor, +): void => { + if (node.typeArgumentList) { + return; + } + + const typeParameters = typeParametersOrEmpty(node.declaration.ref); + if (!isEmpty(typeParameters)) { + accept( + 'error', + `The type '${node.declaration.$refText}' is parameterized, so a type argument list must be added.`, + { + node, + code: CODE_TYPE_MISSING_TYPE_ARGUMENTS, + }, + ); + } +}; + +export const namedTypeMustSetAllTypeParameters = (node: SdsNamedType, accept: ValidationAcceptor): void => {}; + // ----------------------------------------------------------------------------- // Missing type hints // ----------------------------------------------------------------------------- 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/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«<>, +) From 4e675f84402ce7c186c780532e73f47df52a728f Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Wed, 11 Oct 2023 14:01:27 +0200 Subject: [PATCH 2/5] feat: error if type arguments are missing --- src/language/helpers/stringUtils.ts | 6 ++ src/language/validation/safe-ds-validator.ts | 7 +- src/language/validation/types.ts | 61 +++++++++++------ tests/language/helpers/stringUtils.test.ts | 42 ++++++++++++ .../omitted type parameter/main.sdstest | 65 +++++++++++++++++++ 5 files changed, 156 insertions(+), 25 deletions(-) create mode 100644 src/language/helpers/stringUtils.ts create mode 100644 tests/language/helpers/stringUtils.test.ts create mode 100644 tests/resources/validation/types/named types/omitted type parameter/main.sdstest diff --git a/src/language/helpers/stringUtils.ts b/src/language/helpers/stringUtils.ts new file mode 100644 index 000000000..f8d7db190 --- /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/safe-ds-validator.ts b/src/language/validation/safe-ds-validator.ts index d28fb6da2..c3b00059f 100644 --- a/src/language/validation/safe-ds-validator.ts +++ b/src/language/validation/safe-ds-validator.ts @@ -36,9 +36,9 @@ import { templateStringMustHaveExpressionBetweenTwoStringParts } from './other/e import { assignmentAssigneeMustGetValue, yieldMustNotBeUsedInPipeline } from './other/statements/assignments.js'; import { attributeMustHaveTypeHint, - namedTypeMustHaveTypeArgumentListIfTypeIsParameterized, namedTypeMustSetAllTypeParameters, + namedTypeMustSetAllTypeParameters, parameterMustHaveTypeHint, - resultMustHaveTypeHint + resultMustHaveTypeHint, } from './types.js'; import { moduleDeclarationsMustMatchFileKind, moduleWithDeclarationsMustStatePackage } from './other/modules.js'; import { typeParameterConstraintLeftOperandMustBeOwnTypeParameter } from './other/declarations/typeParameterConstraints.js'; @@ -140,8 +140,7 @@ export const registerValidationChecks = function (services: SafeDsServices) { SdsNamedType: [ namedTypeDeclarationShouldNotBeDeprecated(services), namedTypeDeclarationShouldNotBeExperimental(services), - namedTypeMustHaveTypeArgumentListIfTypeIsParameterized, - namedTypeMustSetAllTypeParameters, + namedTypeMustSetAllTypeParameters(services), namedTypeTypeArgumentListShouldBeNeeded, ], SdsParameter: [ diff --git a/src/language/validation/types.ts b/src/language/validation/types.ts index ecdb61a8a..e613b9c10 100644 --- a/src/language/validation/types.ts +++ b/src/language/validation/types.ts @@ -1,7 +1,9 @@ import { getContainerOfType, ValidationAcceptor } from 'langium'; import { isSdsCallable, isSdsLambda, SdsAttribute, SdsNamedType, SdsParameter, SdsResult } from '../generated/ast.js'; -import { typeParametersOrEmpty } from '../helpers/nodeProperties.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'; @@ -10,28 +12,45 @@ export const CODE_TYPE_MISSING_TYPE_HINT = 'type/missing-type-hint'; // Missing type arguments // ----------------------------------------------------------------------------- -export const namedTypeMustHaveTypeArgumentListIfTypeIsParameterized = ( - node: SdsNamedType, - accept: ValidationAcceptor, -): void => { - if (node.typeArgumentList) { - return; - } +export const namedTypeMustSetAllTypeParameters = + (services: SafeDsServices) => + (node: SdsNamedType, accept: ValidationAcceptor): void => { + const expectedTypeParameters = typeParametersOrEmpty(node.declaration.ref); + if (isEmpty(expectedTypeParameters)) { + return; + } - const typeParameters = typeParametersOrEmpty(node.declaration.ref); - if (!isEmpty(typeParameters)) { - accept( - 'error', - `The type '${node.declaration.$refText}' is parameterized, so a type argument list must be added.`, - { - node, - code: CODE_TYPE_MISSING_TYPE_ARGUMENTS, - }, - ); - } -}; + 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', 'type parameters'); + const missingTypeParametersString = missingTypeParameters.map((it) => `'${it.name}'`).join(', '); -export const namedTypeMustSetAllTypeParameters = (node: SdsNamedType, accept: ValidationAcceptor): void => {}; + 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/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»«, +) From 0ccdeaadf005893d71fe865110da65136e4d9983 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Wed, 11 Oct 2023 14:16:41 +0200 Subject: [PATCH 3/5] feat: error if type parameter is set multiple times --- .../validation/other/types/namedTypes.ts | 48 +++++++++++++++++++ .../other/types/typeArgumentLists.ts | 21 -------- src/language/validation/safe-ds-validator.ts | 8 +++- .../duplicate type parameter/main.sdstest | 36 ++++++++++++++ 4 files changed, 90 insertions(+), 23 deletions(-) create mode 100644 src/language/validation/other/types/namedTypes.ts delete mode 100644 src/language/validation/other/types/typeArgumentLists.ts create mode 100644 tests/resources/validation/other/types/type argument lists/duplicate type parameter/main.sdstest diff --git a/src/language/validation/other/types/namedTypes.ts b/src/language/validation/other/types/namedTypes.ts new file mode 100644 index 000000000..e53e6967e --- /dev/null +++ b/src/language/validation/other/types/namedTypes.ts @@ -0,0 +1,48 @@ +import { SdsNamedType } from '../../../generated/ast.js'; +import { ValidationAcceptor } from 'langium'; +import { SafeDsServices } from '../../../safe-ds-module.js'; +import { typeArgumentsOrEmpty } from '../../../helpers/nodeProperties.js'; +import { duplicatesBy } from '../../../helpers/collectionUtils.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 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, + }); + } + } +}; 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 c3b00059f..7c2b9a396 100644 --- a/src/language/validation/safe-ds-validator.ts +++ b/src/language/validation/safe-ds-validator.ts @@ -48,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, @@ -82,6 +81,10 @@ import { import { memberAccessMustBeNullSafeIfReceiverIsNullable } from './other/expressions/memberAccesses.js'; import { importPackageMustExist, importPackageShouldNotBeEmpty } from './other/imports.js'; import { singleUseAnnotationsMustNotBeRepeated } from './builtins/repeatable.js'; +import { + namedTypeMustNotSetTypeParameterMultipleTimes, + namedTypeTypeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments +} from "./other/types/namedTypes.js"; /** * Register custom validation checks. @@ -140,8 +143,10 @@ export const registerValidationChecks = function (services: SafeDsServices) { SdsNamedType: [ namedTypeDeclarationShouldNotBeDeprecated(services), namedTypeDeclarationShouldNotBeExperimental(services), + namedTypeMustNotSetTypeParameterMultipleTimes(services), namedTypeMustSetAllTypeParameters(services), namedTypeTypeArgumentListShouldBeNeeded, + namedTypeTypeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments, ], SdsParameter: [ parameterMustHaveTypeHint, @@ -165,7 +170,6 @@ export const registerValidationChecks = function (services: SafeDsServices) { segmentResultListShouldNotBeEmpty, ], SdsTemplateString: [templateStringMustHaveExpressionBetweenTwoStringParts], - SdsTypeArgumentList: [typeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments], SdsTypeParameterConstraint: [typeParameterConstraintLeftOperandMustBeOwnTypeParameter], SdsTypeParameterList: [typeParameterListShouldNotBeEmpty], SdsUnionType: [unionTypeMustHaveTypeArguments, unionTypeShouldNotHaveASingularTypeArgument], 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« + > +) From a268830cfa95ebc866733684b44ea794057e6cc2 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Wed, 11 Oct 2023 14:27:26 +0200 Subject: [PATCH 4/5] feat: error if too many type arguments are provided --- .../validation/other/types/namedTypes.ts | 23 ++++++++++++++++++- src/language/validation/safe-ds-validator.ts | 2 ++ src/language/validation/types.ts | 2 +- .../too many type arguments/main.sdstest | 17 ++++++++++++++ 4 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 tests/resources/validation/other/types/type argument lists/too many type arguments/main.sdstest diff --git a/src/language/validation/other/types/namedTypes.ts b/src/language/validation/other/types/namedTypes.ts index e53e6967e..3b82791ef 100644 --- a/src/language/validation/other/types/namedTypes.ts +++ b/src/language/validation/other/types/namedTypes.ts @@ -1,11 +1,13 @@ import { SdsNamedType } from '../../../generated/ast.js'; import { ValidationAcceptor } from 'langium'; import { SafeDsServices } from '../../../safe-ds-module.js'; -import { typeArgumentsOrEmpty } from '../../../helpers/nodeProperties.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; @@ -46,3 +48,22 @@ export const namedTypeTypeArgumentListMustNotHavePositionalArgumentsAfterNamedAr } } }; + +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/safe-ds-validator.ts b/src/language/validation/safe-ds-validator.ts index 7c2b9a396..39432dac9 100644 --- a/src/language/validation/safe-ds-validator.ts +++ b/src/language/validation/safe-ds-validator.ts @@ -82,6 +82,7 @@ import { memberAccessMustBeNullSafeIfReceiverIsNullable } from './other/expressi import { importPackageMustExist, importPackageShouldNotBeEmpty } from './other/imports.js'; import { singleUseAnnotationsMustNotBeRepeated } from './builtins/repeatable.js'; import { + namedTypeMustNotHaveTooManyTypeArguments, namedTypeMustNotSetTypeParameterMultipleTimes, namedTypeTypeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments } from "./other/types/namedTypes.js"; @@ -143,6 +144,7 @@ export const registerValidationChecks = function (services: SafeDsServices) { SdsNamedType: [ namedTypeDeclarationShouldNotBeDeprecated(services), namedTypeDeclarationShouldNotBeExperimental(services), + namedTypeMustNotHaveTooManyTypeArguments, namedTypeMustNotSetTypeParameterMultipleTimes(services), namedTypeMustSetAllTypeParameters(services), namedTypeTypeArgumentListShouldBeNeeded, diff --git a/src/language/validation/types.ts b/src/language/validation/types.ts index e613b9c10..9894b19ef 100644 --- a/src/language/validation/types.ts +++ b/src/language/validation/types.ts @@ -27,7 +27,7 @@ export const namedTypeMustSetAllTypeParameters = const missingTypeParameters = expectedTypeParameters.filter((it) => !actualTypeParameters.includes(it)); if (!isEmpty(missingTypeParameters)) { - const kind = pluralize(missingTypeParameters.length, 'type parameter', 'type parameters'); + const kind = pluralize(missingTypeParameters.length, 'type parameter'); const missingTypeParametersString = missingTypeParameters.map((it) => `'${it.name}'`).join(', '); accept( 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»« +) From b95bfb7689f22529167fc00d46f2c2136540d61e Mon Sep 17 00:00:00 2001 From: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> Date: Wed, 11 Oct 2023 12:31:00 +0000 Subject: [PATCH 5/5] style: apply automated linter fixes --- src/language/helpers/stringUtils.ts | 2 +- .../validation/other/types/namedTypes.ts | 2 +- src/language/validation/safe-ds-validator.ts | 4 ++-- src/language/validation/types.ts | 16 ++++++---------- 4 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/language/helpers/stringUtils.ts b/src/language/helpers/stringUtils.ts index f8d7db190..a2bc03da9 100644 --- a/src/language/helpers/stringUtils.ts +++ b/src/language/helpers/stringUtils.ts @@ -3,4 +3,4 @@ */ 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 index 3b82791ef..0a0adfefc 100644 --- a/src/language/validation/other/types/namedTypes.ts +++ b/src/language/validation/other/types/namedTypes.ts @@ -52,7 +52,7 @@ export const namedTypeTypeArgumentListMustNotHavePositionalArgumentsAfterNamedAr export const namedTypeMustNotHaveTooManyTypeArguments = (node: SdsNamedType, accept: ValidationAcceptor): void => { // If the declaration is unresolved, we already show another error if (!node.declaration.ref) { - return + return; } const typeParameters = typeParametersOrEmpty(node.declaration.ref); diff --git a/src/language/validation/safe-ds-validator.ts b/src/language/validation/safe-ds-validator.ts index 39432dac9..a41c4bb9d 100644 --- a/src/language/validation/safe-ds-validator.ts +++ b/src/language/validation/safe-ds-validator.ts @@ -84,8 +84,8 @@ import { singleUseAnnotationsMustNotBeRepeated } from './builtins/repeatable.js' import { namedTypeMustNotHaveTooManyTypeArguments, namedTypeMustNotSetTypeParameterMultipleTimes, - namedTypeTypeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments -} from "./other/types/namedTypes.js"; + namedTypeTypeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments, +} from './other/types/namedTypes.js'; /** * Register custom validation checks. diff --git a/src/language/validation/types.ts b/src/language/validation/types.ts index 9894b19ef..af0ccb477 100644 --- a/src/language/validation/types.ts +++ b/src/language/validation/types.ts @@ -3,7 +3,7 @@ import { isSdsCallable, isSdsLambda, SdsAttribute, SdsNamedType, SdsParameter, S import { typeArgumentsOrEmpty, typeParametersOrEmpty } from '../helpers/nodeProperties.js'; import { isEmpty } from 'radash'; import { SafeDsServices } from '../safe-ds-module.js'; -import {pluralize} from "../helpers/stringUtils.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'; @@ -30,15 +30,11 @@ export const namedTypeMustSetAllTypeParameters = 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, - }, - ); + accept('error', `The ${kind} ${missingTypeParametersString} must be set here.`, { + node, + property: 'typeArgumentList', + code: CODE_TYPE_MISSING_TYPE_ARGUMENTS, + }); } } else { accept(