Skip to content

Commit

Permalink
feat: check position of usages of variant type parameters (#852)
Browse files Browse the repository at this point in the history
Closes #743

### Summary of Changes

* Covariant type parameters must only be used in covariant positions.
* Contravariant type parameters must only be used in contravariant
positions.

This PR adds checks for this.
  • Loading branch information
lars-reimann authored Feb 2, 2024
1 parent 3362b6c commit a2672d7
Show file tree
Hide file tree
Showing 8 changed files with 388 additions and 18 deletions.
12 changes: 12 additions & 0 deletions packages/safe-ds-lang/src/language/helpers/nodeProperties.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,18 @@ export namespace TypeParameter {
export const isRequired = (node: SdsTypeParameter | undefined): boolean => {
return isSdsTypeParameter(node) && !node.defaultValue;
};

export const isContravariant = (node: SdsTypeParameter | undefined): boolean => {
return isSdsTypeParameter(node) && node.variance === 'in';
};

export const isCovariant = (node: SdsTypeParameter | undefined): boolean => {
return isSdsTypeParameter(node) && node.variance === 'out';
};

export const isInvariant = (node: SdsTypeParameter | undefined): boolean => {
return isSdsTypeParameter(node) && !node.variance;
};
}

// -------------------------------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,17 @@ import {
isSdsClassMember,
isSdsDeclaration,
isSdsNamedTypeDeclaration,
isSdsParameter,
isSdsParameterList,
isSdsTypeArgument,
isSdsUnionType,
SdsClass,
SdsDeclaration,
SdsTypeParameter,
} from '../../../generated/ast.js';
import { isStatic } from '../../../helpers/nodeProperties.js';
import { isStatic, TypeParameter } from '../../../helpers/nodeProperties.js';
import { SafeDsServices } from '../../../safe-ds-module.js';
import { SafeDsNodeMapper } from '../../../helpers/safe-ds-node-mapper.js';

export const CODE_TYPE_PARAMETER_INSUFFICIENT_CONTEXT = 'type-parameter/insufficient-context';
export const CODE_TYPE_PARAMETER_USAGE = 'type-parameter/usage';
Expand Down Expand Up @@ -52,25 +57,63 @@ export const typeParameterMustHaveSufficientContext = (node: SdsTypeParameter, a
}
};

export const typeParameterMustBeUsedInCorrectContext = (node: SdsTypeParameter, accept: ValidationAcceptor) => {
// Only classes can have nested named type declarations
const declarationWithTypeParameter = getContainerOfType(node.$container, isSdsDeclaration);
if (!isSdsClass(declarationWithTypeParameter)) {
return;
}
export const typeParameterMustBeUsedInCorrectPosition = (services: SafeDsServices) => {
const nodeMapper = services.helpers.NodeMapper;

return (node: SdsTypeParameter, accept: ValidationAcceptor) => {
const declarationWithTypeParameter = getContainerOfType(node.$container, isSdsDeclaration);

findLocalReferences(node).forEach((it) => {
const reference = it.$refNode?.astNode;
if (reference && !classTypeParameterIsUsedInCorrectContext(declarationWithTypeParameter, reference)) {
accept('error', 'This type parameter of a containing class cannot be used here.', {
node: reference,
code: CODE_TYPE_PARAMETER_USAGE,
});
// Early exit
if (
!declarationWithTypeParameter ||
(!isSdsClass(declarationWithTypeParameter) && TypeParameter.isInvariant(node))
) {
return;
}
});

findLocalReferences(node).forEach((it) => {
const reference = it.$refNode?.astNode;
if (!reference) {
/* c8 ignore next 2 */
return;
}

// Check usage of class type parameters
if (
isSdsClass(declarationWithTypeParameter) &&
!classTypeParameterIsUsedInCorrectPosition(declarationWithTypeParameter, reference)
) {
accept('error', 'This type parameter of a containing class cannot be used here.', {
node: reference,
code: CODE_TYPE_PARAMETER_USAGE,
});
}

// Check usage of variant type parameters
else if (TypeParameter.isContravariant(node)) {
const position = getTypePosition(nodeMapper, declarationWithTypeParameter, reference);

if (position !== 'contravariant') {
accept('error', `A contravariant type parameter cannot be used in ${position} position.`, {
node: reference,
code: CODE_TYPE_PARAMETER_USAGE,
});
}
} else if (TypeParameter.isCovariant(node)) {
const position = getTypePosition(nodeMapper, declarationWithTypeParameter, reference);

if (position !== 'covariant') {
accept('error', `A covariant type parameter cannot be used in ${position} position.`, {
node: reference,
code: CODE_TYPE_PARAMETER_USAGE,
});
}
}
});
};
};

const classTypeParameterIsUsedInCorrectContext = (classWithTypeParameter: SdsClass, reference: AstNode) => {
const classTypeParameterIsUsedInCorrectPosition = (classWithTypeParameter: SdsClass, reference: AstNode) => {
const containingClassMember = getContainerOfType(reference, isSdsClassMember);

// Handle usage in constructor
Expand All @@ -87,3 +130,57 @@ const classTypeParameterIsUsedInCorrectContext = (classWithTypeParameter: SdsCla
const containingNamedTypeDeclaration = getContainerOfType(reference, isSdsNamedTypeDeclaration);
return !containingNamedTypeDeclaration || containingNamedTypeDeclaration === classWithTypeParameter;
};

type TypePosition = 'contravariant' | 'covariant' | 'invariant';

const getTypePosition = (
nodeMapper: SafeDsNodeMapper,
declarationWithTypeParameter: SdsDeclaration,
reference: AstNode,
): TypePosition => {
let current: AstNode | undefined = reference;
let result: TypePosition = 'covariant';

while (current && current !== declarationWithTypeParameter && result !== 'invariant') {
let step: TypePosition;

if (isSdsParameter(current)) {
step = 'contravariant';
} else if (isSdsTypeArgument(current)) {
const typeParameter = nodeMapper.typeArgumentToTypeParameter(current);

if (TypeParameter.isContravariant(typeParameter)) {
step = 'contravariant';
} else if (TypeParameter.isCovariant(typeParameter)) {
step = 'covariant';
} else {
step = 'invariant';
}
} else {
step = 'covariant';
}

result = nextTypePosition(result, step);
current = current.$container;
}

return result;
};

const nextTypePosition = (aggregator: TypePosition, step: TypePosition): TypePosition => {
// We could also get the result by mapping the following numbers to the positions and multiplying them:
// -1 = contravariant
// 0 = invariant
// 1 = covariant

if (aggregator === 'invariant' || step === 'invariant') {
return 'invariant';
} else if (aggregator === 'covariant') {
return step;
} else if (step === 'covariant') {
return aggregator;
} else {
// Both are contravariant
return 'covariant';
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ import {
} from './other/declarations/segments.js';
import { typeParameterConstraintLeftOperandMustBeOwnTypeParameter } from './other/declarations/typeParameterConstraints.js';
import {
typeParameterMustBeUsedInCorrectContext,
typeParameterMustBeUsedInCorrectPosition,
typeParameterMustHaveSufficientContext,
} from './other/declarations/typeParameters.js';
import { callArgumentMustBeConstantIfParameterIsConstant, callMustNotBeRecursive } from './other/expressions/calls.js';
Expand Down Expand Up @@ -352,7 +352,7 @@ export const registerValidationChecks = function (services: SafeDsServices) {
SdsTemplateString: [templateStringMustHaveExpressionBetweenTwoStringParts],
SdsTypeArgumentList: [typeArgumentListsShouldBeUsedWithCaution(services)],
SdsTypeCast: [typeCastExpressionMustHaveUnknownType(services)],
SdsTypeParameter: [typeParameterMustHaveSufficientContext, typeParameterMustBeUsedInCorrectContext],
SdsTypeParameter: [typeParameterMustHaveSufficientContext, typeParameterMustBeUsedInCorrectPosition(services)],
SdsTypeParameterConstraint: [typeParameterConstraintLeftOperandMustBeOwnTypeParameter],
SdsTypeParameterList: [
typeParameterListMustNotHaveRequiredTypeParametersAfterOptionalTypeParameters,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package tests.validation.other.declarations.typeParameters.usageOfVariantTypeParameters

// $TEST$ no error r"A .*variant type parameter cannot be used in .*variant position."
class MyClass1<in Contravariant>(p1: »Contravariant«) {
// $TEST$ error "A contravariant type parameter cannot be used in covariant position."
attr a1: »Contravariant«
// $TEST$ no error r"A .*variant type parameter cannot be used in .*variant position."
// $TEST$ error "A contravariant type parameter cannot be used in covariant position."
attr a2: (a1: »Contravariant«) -> (r1: »Contravariant«)
// $TEST$ error "A contravariant type parameter cannot be used in covariant position."
attr a3: Producer<»Contravariant«>
// $TEST$ error "A contravariant type parameter cannot be used in invariant position."
attr a4: Middleware<»Contravariant«>
// $TEST$ no error r"A .*variant type parameter cannot be used in .*variant position."
attr a5: Consumer<»Contravariant«>
// $TEST$ error "A contravariant type parameter cannot be used in covariant position."
attr a6: Producer<Producer<»Contravariant«>>
// $TEST$ error "A contravariant type parameter cannot be used in invariant position."
attr a7: Middleware<Producer<»Contravariant«>>
// $TEST$ no error r"A .*variant type parameter cannot be used in .*variant position."
attr a8: Consumer<Producer<»Contravariant«>>

fun f(
// $TEST$ no error r"A .*variant type parameter cannot be used in .*variant position."
p1: »Contravariant«,
// $TEST$ error "A contravariant type parameter cannot be used in covariant position."
// $TEST$ no error r"A .*variant type parameter cannot be used in .*variant position."
p2: (a1: »Contravariant«) -> (r1: »Contravariant«),
// $TEST$ no error r"A .*variant type parameter cannot be used in .*variant position."
p3: Producer<»Contravariant«>,
// $TEST$ error "A contravariant type parameter cannot be used in invariant position."
p4: Middleware<»Contravariant«>,
// $TEST$ error "A contravariant type parameter cannot be used in covariant position."
p5: Consumer<»Contravariant«>,
) -> (
// $TEST$ error "A contravariant type parameter cannot be used in covariant position."
r1: »Contravariant«,
// $TEST$ no error r"A .*variant type parameter cannot be used in .*variant position."
// $TEST$ error "A contravariant type parameter cannot be used in covariant position."
r2: (a1: »Contravariant«) -> (r1: »Contravariant«),
// $TEST$ error "A contravariant type parameter cannot be used in covariant position."
r3: Producer<»Contravariant«>,
// $TEST$ error "A contravariant type parameter cannot be used in invariant position."
r4: Middleware<»Contravariant«>,
// $TEST$ no error r"A .*variant type parameter cannot be used in .*variant position."
r5: Consumer<»Contravariant«>,
)
}

fun f1<in Contravariant>(
// $TEST$ no error r"A .*variant type parameter cannot be used in .*variant position."
p1: »Contravariant«,
// $TEST$ error "A contravariant type parameter cannot be used in covariant position."
// $TEST$ no error r"A .*variant type parameter cannot be used in .*variant position."
p2: (a1: »Contravariant«) -> (r1: »Contravariant«),
// $TEST$ no error r"A .*variant type parameter cannot be used in .*variant position."
p3: Producer<»Contravariant«>,
// $TEST$ error "A contravariant type parameter cannot be used in invariant position."
p4: Middleware<»Contravariant«>,
// $TEST$ error "A contravariant type parameter cannot be used in covariant position."
p5: Consumer<»Contravariant«>,
) -> (
// $TEST$ error "A contravariant type parameter cannot be used in covariant position."
r1: »Contravariant«,
// $TEST$ no error r"A .*variant type parameter cannot be used in .*variant position."
// $TEST$ error "A contravariant type parameter cannot be used in covariant position."
r2: (a1: »Contravariant«) -> (r1: »Contravariant«),
// $TEST$ error "A contravariant type parameter cannot be used in covariant position."
r3: Producer<»Contravariant«>,
// $TEST$ error "A contravariant type parameter cannot be used in invariant position."
r4: Middleware<»Contravariant«>,
// $TEST$ no error r"A .*variant type parameter cannot be used in .*variant position."
r5: Consumer<»Contravariant«>,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package tests.validation.other.declarations.typeParameters.usageOfVariantTypeParameters

// $TEST$ error "A covariant type parameter cannot be used in contravariant position."
class MyClass2<out Covariant>(p1: »Covariant«) {
// $TEST$ no error r"A .*variant type parameter cannot be used in .*variant position."
attr a1: »Covariant«
// $TEST$ error "A covariant type parameter cannot be used in contravariant position."
// $TEST$ no error r"A .*variant type parameter cannot be used in .*variant position."
attr a2: (a1: »Covariant«) -> (r1: »Covariant«)
// $TEST$ no error r"A .*variant type parameter cannot be used in .*variant position."
attr a3: Producer<»Covariant«>
// $TEST$ error "A covariant type parameter cannot be used in invariant position."
attr a4: Middleware<»Covariant«>
// $TEST$ error "A covariant type parameter cannot be used in contravariant position."
attr a5: Consumer<»Covariant«>
// $TEST$ no error r"A .*variant type parameter cannot be used in .*variant position."
attr a6: Producer<Producer<»Covariant«>>
// $TEST$ error "A covariant type parameter cannot be used in invariant position."
attr a7: Middleware<Producer<»Covariant«>>
// $TEST$ error "A covariant type parameter cannot be used in contravariant position."
attr a8: Consumer<Producer<»Covariant«>>

fun f(
// $TEST$ error "A covariant type parameter cannot be used in contravariant position."
p1: »Covariant«,
// $TEST$ no error r"A .*variant type parameter cannot be used in .*variant position."
// $TEST$ error "A covariant type parameter cannot be used in contravariant position."
p2: (a1: »Covariant«) -> (r1: »Covariant«),
// $TEST$ error "A covariant type parameter cannot be used in contravariant position."
p3: Producer<»Covariant«>,
// $TEST$ error "A covariant type parameter cannot be used in invariant position."
p4: Middleware<»Covariant«>,
// $TEST$ no error r"A .*variant type parameter cannot be used in .*variant position."
p5: Consumer<»Covariant«>,
) -> (
// $TEST$ no error r"A .*variant type parameter cannot be used in .*variant position."
r1: »Covariant«,
// $TEST$ error "A covariant type parameter cannot be used in contravariant position."
// $TEST$ no error r"A .*variant type parameter cannot be used in .*variant position."
r2: (a1: »Covariant«) -> (r1: »Covariant«),
// $TEST$ no error r"A .*variant type parameter cannot be used in .*variant position."
r3: Producer<»Covariant«>,
// $TEST$ error "A covariant type parameter cannot be used in invariant position."
r4: Middleware<»Covariant«>,
// $TEST$ error "A covariant type parameter cannot be used in contravariant position."
r5: Consumer<»Covariant«>,
)
}

fun f2<out Covariant>(
// $TEST$ error "A covariant type parameter cannot be used in contravariant position."
p1: »Covariant«,
// $TEST$ no error r"A .*variant type parameter cannot be used in .*variant position."
// $TEST$ error "A covariant type parameter cannot be used in contravariant position."
p2: (a1: »Covariant«) -> (r1: »Covariant«),
// $TEST$ error "A covariant type parameter cannot be used in contravariant position."
p3: Producer<»Covariant«>,
// $TEST$ error "A covariant type parameter cannot be used in invariant position."
p4: Middleware<»Covariant«>,
// $TEST$ no error r"A .*variant type parameter cannot be used in .*variant position."
p5: Consumer<»Covariant«>,
) -> (
// $TEST$ no error r"A .*variant type parameter cannot be used in .*variant position."
r1: »Covariant«,
// $TEST$ error "A covariant type parameter cannot be used in contravariant position."
// $TEST$ no error r"A .*variant type parameter cannot be used in .*variant position."
r2: (a1: »Covariant«) -> (r1: »Covariant«),
// $TEST$ no error r"A .*variant type parameter cannot be used in .*variant position."
r3: Producer<»Covariant«>,
// $TEST$ error "A covariant type parameter cannot be used in invariant position."
r4: Middleware<»Covariant«>,
// $TEST$ error "A covariant type parameter cannot be used in contravariant position."
r5: Consumer<»Covariant«>,
)
Loading

0 comments on commit a2672d7

Please sign in to comment.