diff --git a/packages/safe-ds-lang/src/language/validation/safe-ds-validator.ts b/packages/safe-ds-lang/src/language/validation/safe-ds-validator.ts index f0deb8749..1bd88d35f 100644 --- a/packages/safe-ds-lang/src/language/validation/safe-ds-validator.ts +++ b/packages/safe-ds-lang/src/language/validation/safe-ds-validator.ts @@ -166,11 +166,13 @@ import { listMustNotContainNamedTuples, mapMustNotContainNamedTuples, namedTypeMustSetAllTypeParameters, + namedTypeTypeArgumentsMustMatchBounds, parameterDefaultValueTypeMustMatchParameterType, parameterMustHaveTypeHint, prefixOperationOperandMustHaveCorrectType, resultMustHaveTypeHint, typeCastExpressionMustHaveUnknownType, + typeParameterDefaultValueMustMatchUpperBound, yieldTypeMustMatchResultType, } from './types.js'; import { statementMustDoSomething } from './other/statements/statements.js'; @@ -313,6 +315,7 @@ export const registerValidationChecks = function (services: SafeDsServices) { namedTypeMustSetAllTypeParameters(services), namedTypeTypeArgumentListShouldBeNeeded(services), namedTypeTypeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments, + namedTypeTypeArgumentsMustMatchBounds(services), ], SdsParameter: [ constantParameterMustHaveConstantDefaultValue(services), @@ -347,6 +350,7 @@ export const registerValidationChecks = function (services: SafeDsServices) { SdsTemplateString: [templateStringMustHaveExpressionBetweenTwoStringParts], SdsTypeCast: [typeCastExpressionMustHaveUnknownType(services)], SdsTypeParameter: [ + typeParameterDefaultValueMustMatchUpperBound(services), typeParameterMustBeUsedInCorrectPosition(services), typeParameterMustHaveSufficientContext, typeParameterMustOnlyBeVariantOnClass, diff --git a/packages/safe-ds-lang/src/language/validation/types.ts b/packages/safe-ds-lang/src/language/validation/types.ts index f5d751867..75ea66e0b 100644 --- a/packages/safe-ds-lang/src/language/validation/types.ts +++ b/packages/safe-ds-lang/src/language/validation/types.ts @@ -22,6 +22,7 @@ import { SdsPrefixOperation, SdsResult, SdsTypeCast, + SdsTypeParameter, SdsYield, } from '../generated/ast.js'; import { getTypeArguments, getTypeParameters, TypeParameter } from '../helpers/nodeProperties.js'; @@ -251,6 +252,44 @@ export const mapMustNotContainNamedTuples = (services: SafeDsServices) => { }; }; +export const namedTypeTypeArgumentsMustMatchBounds = (services: SafeDsServices) => { + const nodeMapper = services.helpers.NodeMapper; + const typeChecker = services.types.TypeChecker; + const typeComputer = services.types.TypeComputer; + + return (node: SdsNamedType, accept: ValidationAcceptor): void => { + const type = typeComputer.computeType(node); + if (!(type instanceof ClassType) || isEmpty(type.substitutions)) { + return; + } + + for (const typeArgument of getTypeArguments(node)) { + const typeParameter = nodeMapper.typeArgumentToTypeParameter(typeArgument); + if (!typeParameter) { + continue; + } + + const typeArgumentType = type.substitutions.get(typeParameter); + if (!typeArgumentType) { + /* c8 ignore next 2 */ + continue; + } + + const upperBound = typeComputer + .computeUpperBound(typeParameter, { stopAtTypeParameterType: true }) + .substituteTypeParameters(type.substitutions); + + if (!typeChecker.isSubtypeOf(typeArgumentType, upperBound, { strictTypeParameterTypeCheck: true })) { + accept('error', `Expected type '${upperBound}' but got '${typeArgumentType}'.`, { + node: typeArgument, + property: 'value', + code: CODE_TYPE_MISMATCH, + }); + } + } + }; +}; + export const parameterDefaultValueTypeMustMatchParameterType = (services: SafeDsServices) => { const typeChecker = services.types.TypeChecker; const typeComputer = services.types.TypeComputer; @@ -326,6 +365,28 @@ export const typeCastExpressionMustHaveUnknownType = (services: SafeDsServices) }; }; +export const typeParameterDefaultValueMustMatchUpperBound = (services: SafeDsServices) => { + const typeChecker = services.types.TypeChecker; + const typeComputer = services.types.TypeComputer; + + return (node: SdsTypeParameter, accept: ValidationAcceptor): void => { + if (!node.defaultValue || !node.upperBound) { + return; + } + + const defaultValueType = typeComputer.computeType(node.defaultValue); + const upperBoundType = typeComputer.computeUpperBound(node, { stopAtTypeParameterType: true }); + + if (!typeChecker.isSubtypeOf(defaultValueType, upperBoundType, { strictTypeParameterTypeCheck: true })) { + accept('error', `Expected type '${upperBoundType}' but got '${defaultValueType}'.`, { + node, + property: 'defaultValue', + code: CODE_TYPE_MISMATCH, + }); + } + }; +}; + export const yieldTypeMustMatchResultType = (services: SafeDsServices) => { const typeChecker = services.types.TypeChecker; const typeComputer = services.types.TypeComputer; diff --git a/packages/safe-ds-lang/tests/resources/validation/types/checking/default values/with type parameters.sdstest b/packages/safe-ds-lang/tests/resources/validation/types/checking/default values/with type parameters.sdstest new file mode 100644 index 000000000..5b48a14ae --- /dev/null +++ b/packages/safe-ds-lang/tests/resources/validation/types/checking/default values/with type parameters.sdstest @@ -0,0 +1,19 @@ +package tests.validation.types.checking.defaultValues + +class SomeClass( + // $TEST$ no error r"Expected type .* but got .*\." + p1: T1 = »true«, + // $TEST$ no error r"Expected type .* but got .*\." + p2: T1 = »1«, + // $TEST$ error "Expected type 'T2' but got 'literal<"">'." + p3: T2 = »""«, +) + +fun someFunction( + // $TEST$ no error r"Expected type .* but got .*\." + p1: T1 = »true«, + // $TEST$ no error r"Expected type .* but got .*\." + p2: T1 = »1«, + // $TEST$ error "Expected type 'T2' but got 'literal<"">'." + p3: T2 = »""«, +) diff --git a/packages/safe-ds-lang/tests/resources/validation/types/checking/type parameter bounds for default values/main.sdstest b/packages/safe-ds-lang/tests/resources/validation/types/checking/type parameter bounds for default values/main.sdstest new file mode 100644 index 000000000..d56a1d0d1 --- /dev/null +++ b/packages/safe-ds-lang/tests/resources/validation/types/checking/type parameter bounds for default values/main.sdstest @@ -0,0 +1,57 @@ +package tests.validation.types.checking.typeParameterBoundsForDefaultValues + +class C1< + T1 sub Any, + // $TEST$ error "Expected type 'Number' but got 'T1'." + T2 sub Number = »T1«, + // $TEST$ error "Expected type 'Number' but got 'Any'." + T3 sub Number = »Any«, + // $TEST$ error "Expected type 'T1' but got 'Any'." + T4 sub T1 = »Any«, +> +class C2< + T1 sub Number, + // $TEST$ no error r"Expected type '.*' but got '.*'\." + T2 sub Number = »T1«, + // $TEST$ no error r"Expected type '.*' but got '.*'\." + T3 sub Number = »Number«, + // $TEST$ no error r"Expected type '.*' but got '.*'\." + T4 sub T1 = »T1«, +> +class C3< + T1 sub Int, + // $TEST$ no error r"Expected type '.*' but got '.*'\." + T2 sub Number = »T1«, + // $TEST$ no error r"Expected type '.*' but got '.*'\." + T3 sub Number = »Int«, + // $TEST$ no error r"Expected type '.*' but got '.*'\." + T4 sub T1 = »Nothing«, +> + +@Pure fun f1< + T1 sub Any, + // $TEST$ error "Expected type 'Number' but got 'T1'." + T2 sub Number = »T1«, + // $TEST$ error "Expected type 'Number' but got 'Any'." + T3 sub Number = »Any«, + // $TEST$ error "Expected type 'T1' but got 'Any'." + T4 sub T1 = »Any«, +>() +@Pure fun f2< + T1 sub Number, + // $TEST$ no error r"Expected type '.*' but got '.*'\." + T2 sub Number = »T1«, + // $TEST$ no error r"Expected type '.*' but got '.*'\." + T3 sub Number = »Number«, + // $TEST$ no error r"Expected type '.*' but got '.*'\." + T4 sub T1 = »T1«, +>() +@Pure fun f3< + T1 sub Int, + // $TEST$ no error r"Expected type '.*' but got '.*'\." + T2 sub Number = »T1«, + // $TEST$ no error r"Expected type '.*' but got '.*'\." + T3 sub Number = »Int«, + // $TEST$ no error r"Expected type '.*' but got '.*'\." + T4 sub T1 = »Nothing«, +>() diff --git a/packages/safe-ds-lang/tests/resources/validation/types/checking/type parameter bounds for named types/main.sdstest b/packages/safe-ds-lang/tests/resources/validation/types/checking/type parameter bounds for named types/main.sdstest new file mode 100644 index 000000000..d0524371e --- /dev/null +++ b/packages/safe-ds-lang/tests/resources/validation/types/checking/type parameter bounds for named types/main.sdstest @@ -0,0 +1,29 @@ +package tests.validation.types.checking.typeParameterBoundsForNamedTypes + +class C1 +class C2 +class C3 + +@Pure fun f( + // $TEST$ no error r"Expected type '.*' but got '.*'\." + a1: C1<»Any?«>, + // $TEST$ no error r"Expected type '.*' but got '.*'\." + a2: C1, + // $TEST$ no error r"Expected type '.*' but got '.*'\." + a3: C1, + + // $TEST$ no error r"Expected type '.*' but got '.*'\." + b1: C2<»Number«>, + // $TEST$ error "Expected type 'Number' but got 'String'." + b2: C2<»String«>, + + // $TEST$ no error r"Expected type '.*' but got '.*'\." + // $TEST$ no error r"Expected type '.*' but got '.*'\." + c1: C3<»Number«, »Number«>, + // $TEST$ no error r"Expected type '.*' but got '.*'\." + // $TEST$ no error r"Expected type '.*' but got '.*'\." + c2: C3<»Number«, »Int«>, + // $TEST$ no error r"Expected type '.*' but got '.*'\." + // $TEST$ error "Expected type 'Int' but got 'Number'." + c3: C3<»Int«, »Number«>, +)