Skip to content

Commit

Permalink
feat: type check type arguments of named types
Browse files Browse the repository at this point in the history
  • Loading branch information
lars-reimann committed Feb 25, 2024
1 parent 72a9bbf commit 2031708
Show file tree
Hide file tree
Showing 3 changed files with 69 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,7 @@ import {
listMustNotContainNamedTuples,
mapMustNotContainNamedTuples,
namedTypeMustSetAllTypeParameters,
namedTypeTypeArgumentsMustMatchBounds,
parameterDefaultValueTypeMustMatchParameterType,
parameterMustHaveTypeHint,
prefixOperationOperandMustHaveCorrectType,
Expand Down Expand Up @@ -314,6 +315,7 @@ export const registerValidationChecks = function (services: SafeDsServices) {
namedTypeMustSetAllTypeParameters(services),
namedTypeTypeArgumentListShouldBeNeeded(services),
namedTypeTypeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments,
namedTypeTypeArgumentsMustMatchBounds(services),
],
SdsParameter: [
constantParameterMustHaveConstantDefaultValue(services),
Expand Down
38 changes: 38 additions & 0 deletions packages/safe-ds-lang/src/language/validation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -252,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;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package tests.validation.types.checking.typeParameterBoundsForNamedTypes

class C1<T1>
class C2<T1 sub Number>
class C3<T1, T2 sub T1>

@Pure fun f(
// $TEST$ no error r"Expected type '.*' but got '.*'\."
a1: C1<»Any?«>,
// $TEST$ no error r"Expected type '.*' but got '.*'\."
a2: C1<T1 = »Any?«>,
// $TEST$ no error r"Expected type '.*' but got '.*'\."
a3: C1<Unknown = »Any?«>,

// $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«>,
)

0 comments on commit 2031708

Please sign in to comment.