Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: check position of usages of variant type parameters #852

Merged
merged 2 commits into from
Feb 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading