From bcf1a4b8fc2cd8119ca1f13b3afd3c7208f5a879 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Mon, 12 Feb 2024 16:31:06 +0100 Subject: [PATCH] feat: check whether type parameter bounds are acyclic (#886) Closes #874 ### Summary of Changes Add a check to ensure type parameter bounds are acyclic. --- .../other/declarations/typeParameterBounds.ts | 19 +++ .../other/declarations/typeParameters.ts | 139 ++++++++++++------ .../language/validation/safe-ds-validator.ts | 16 +- .../tests/helpers/testAssertions.ts | 12 ++ .../tests/language/typing/model.test.ts | 22 ++- .../typing/safe-ds-class-hierarchy.test.ts | 3 +- .../typing/type computer/computeBound.test.ts | 134 +++++++++++++++++ .../computeClassTypeForLiteralType.test.ts | 8 +- .../lowestCommonSupertype.test.ts | 6 +- .../main.sdstest | 14 +- .../main.sdstest | 0 .../bounds must be acyclic/main.sdstest | 27 ++++ 12 files changed, 332 insertions(+), 68 deletions(-) create mode 100644 packages/safe-ds-lang/tests/helpers/testAssertions.ts create mode 100644 packages/safe-ds-lang/tests/language/typing/type computer/computeBound.test.ts rename packages/safe-ds-lang/tests/resources/validation/other/declarations/{type parameters/bound must be named type => constraints/type parameter bounds/right operand must be named type}/main.sdstest (84%) rename packages/safe-ds-lang/tests/resources/validation/other/declarations/{type parameters => type parameter lists}/must not have required after optional/main.sdstest (100%) create mode 100644 packages/safe-ds-lang/tests/resources/validation/other/declarations/type parameters/bounds must be acyclic/main.sdstest diff --git a/packages/safe-ds-lang/src/language/validation/other/declarations/typeParameterBounds.ts b/packages/safe-ds-lang/src/language/validation/other/declarations/typeParameterBounds.ts index 1ae3c46bb..4e4800e6a 100644 --- a/packages/safe-ds-lang/src/language/validation/other/declarations/typeParameterBounds.ts +++ b/packages/safe-ds-lang/src/language/validation/other/declarations/typeParameterBounds.ts @@ -1,7 +1,10 @@ import { isSdsDeclaration, SdsTypeParameterBound } from '../../../generated/ast.js'; import { getContainerOfType, ValidationAcceptor } from 'langium'; +import { NamedType, UnknownType } from '../../../typing/model.js'; +import { SafeDsServices } from '../../../safe-ds-module.js'; export const CODE_TYPE_PARAMETER_BOUND_LEFT_OPERAND = 'type-parameter-bound/left-operand'; +export const CODE_TYPE_PARAMETER_BOUND_RIGHT_OPERAND = 'type-parameter-bound/right-operand'; export const typeParameterBoundLeftOperandMustBeOwnTypeParameter = ( node: SdsTypeParameterBound, @@ -23,3 +26,19 @@ export const typeParameterBoundLeftOperandMustBeOwnTypeParameter = ( }); } }; + +export const typeParameterBoundRightOperandMustBeNamedType = (services: SafeDsServices) => { + const typeComputer = services.types.TypeComputer; + + return (node: SdsTypeParameterBound, accept: ValidationAcceptor) => { + const boundType = typeComputer.computeType(node.rightOperand); + + if (boundType !== UnknownType && !(boundType instanceof NamedType)) { + accept('error', 'Bounds of type parameters must be named types.', { + node, + property: 'rightOperand', + code: CODE_TYPE_PARAMETER_BOUND_RIGHT_OPERAND, + }); + } + }; +}; diff --git a/packages/safe-ds-lang/src/language/validation/other/declarations/typeParameters.ts b/packages/safe-ds-lang/src/language/validation/other/declarations/typeParameters.ts index 04734fd99..3b661c2d4 100644 --- a/packages/safe-ds-lang/src/language/validation/other/declarations/typeParameters.ts +++ b/packages/safe-ds-lang/src/language/validation/other/declarations/typeParameters.ts @@ -4,10 +4,12 @@ import { isSdsClass, isSdsClassMember, isSdsDeclaration, + isSdsNamedType, isSdsNamedTypeDeclaration, isSdsParameter, isSdsParameterList, isSdsTypeArgument, + isSdsTypeParameter, isSdsUnionType, SdsClass, SdsDeclaration, @@ -17,16 +19,93 @@ import { import { isStatic, TypeParameter } from '../../../helpers/nodeProperties.js'; import { SafeDsServices } from '../../../safe-ds-module.js'; import { SafeDsNodeMapper } from '../../../helpers/safe-ds-node-mapper.js'; -import { NamedType, UnknownType } from '../../../typing/model.js'; -import { SafeDsTypeComputer } from '../../../typing/safe-ds-type-computer.js'; +import { UnknownType } from '../../../typing/model.js'; +export const CODE_TYPE_PARAMETER_CYCLIC_BOUND = 'type-parameter/cyclic-bound'; export const CODE_TYPE_PARAMETER_INCOMPATIBLE_BOUNDS = 'type-parameter/incompatible-bounds'; export const CODE_TYPE_PARAMETER_INSUFFICIENT_CONTEXT = 'type-parameter/insufficient-context'; -export const CODE_TYPE_PARAMETER_INVALID_BOUND = 'type-parameter/invalid-bound'; export const CODE_TYPE_PARAMETER_MULTIPLE_BOUNDS = 'type-parameter/multiple-bounds'; export const CODE_TYPE_PARAMETER_USAGE = 'type-parameter/usage'; export const CODE_TYPE_PARAMETER_VARIANCE = 'type-parameter/variance'; +export const typeParameterBoundMustBeAcyclic = (node: SdsTypeParameter, accept: ValidationAcceptor) => { + const lowerBound = TypeParameter.getLowerBounds(node)[0]; + if (lowerBound && !lowerTypeParameterBoundIsAcyclic(lowerBound)) { + accept('error', 'Bounds of type parameters must be acyclic.', { + node: lowerBound, + code: CODE_TYPE_PARAMETER_CYCLIC_BOUND, + }); + } + + const upperBound = TypeParameter.getUpperBounds(node)[0]; + if (upperBound && !upperTypeParameterBoundIsAcyclic(upperBound)) { + accept('error', 'Bounds of type parameters must be acyclic.', { + node: upperBound, + code: CODE_TYPE_PARAMETER_CYCLIC_BOUND, + }); + } +}; + +const lowerTypeParameterBoundIsAcyclic = (node: SdsTypeParameterBound): boolean => { + const visited = new Set(); + let current: SdsTypeParameterBound | undefined = node; + + while (current) { + const typeParameter = getBoundingTypeParameter(current, 'sub'); + if (!typeParameter) { + return true; + } else if (visited.has(typeParameter)) { + return false; + } + + visited.add(typeParameter); + current = TypeParameter.getLowerBounds(typeParameter)[0]; + } + + return true; +}; + +const upperTypeParameterBoundIsAcyclic = (node: SdsTypeParameterBound): boolean => { + const visited = new Set(); + let current: SdsTypeParameterBound | undefined = node; + + while (current) { + const typeParameter = getBoundingTypeParameter(current, 'super'); + if (!typeParameter) { + return true; + } else if (visited.has(typeParameter)) { + return false; + } + + visited.add(typeParameter); + current = TypeParameter.getUpperBounds(typeParameter)[0]; + } + + return true; +}; + +/** + * Returns the next type parameter to be visited when checking for cycles. + * + * @param node + * The current type parameter bound. + * + * @param invertedOperator + * The operator for the inverted bound direction ('sub' for lower bounds, 'super' for upper bounds). + */ +const getBoundingTypeParameter = ( + node: SdsTypeParameterBound, + invertedOperator: string, +): SdsTypeParameter | undefined => { + if (node.operator === invertedOperator) { + return node.leftOperand?.ref; + } else if (isSdsNamedType(node.rightOperand) && isSdsTypeParameter(node.rightOperand.declaration?.ref)) { + return node.rightOperand.declaration?.ref; + } else { + return undefined; + } +}; + export const typeParameterBoundsMustBeCompatible = (services: SafeDsServices) => { const typeChecker = services.types.TypeChecker; const typeComputer = services.types.TypeComputer; @@ -88,48 +167,24 @@ export const typeParameterMustHaveSufficientContext = (node: SdsTypeParameter, a } }; -export const typeParameterMustHaveOneValidLowerAndUpperBound = (services: SafeDsServices) => { - const typeComputer = services.types.TypeComputer; - - return (node: SdsTypeParameter, accept: ValidationAcceptor) => { - TypeParameter.getLowerBounds(node).forEach((it, index) => { - if (index === 0) { - checkIfBoundIsValid(it, typeComputer, accept); - } else { - accept('error', `The type parameter '${node.name}' can only have a single lower bound.`, { - node: it, - code: CODE_TYPE_PARAMETER_MULTIPLE_BOUNDS, - }); - } - }); - - TypeParameter.getUpperBounds(node).forEach((it, index) => { - if (index === 0) { - checkIfBoundIsValid(it, typeComputer, accept); - } else { - accept('error', `The type parameter '${node.name}' can only have a single upper bound.`, { - node: it, - code: CODE_TYPE_PARAMETER_MULTIPLE_BOUNDS, - }); - } +export const typeParameterMustNotHaveMultipleBounds = (node: SdsTypeParameter, accept: ValidationAcceptor) => { + TypeParameter.getLowerBounds(node) + .slice(1) + .forEach((it) => { + accept('error', `The type parameter '${node.name}' can only have a single lower bound.`, { + node: it, + code: CODE_TYPE_PARAMETER_MULTIPLE_BOUNDS, + }); }); - }; -}; -const checkIfBoundIsValid = ( - node: SdsTypeParameterBound, - typeComputer: SafeDsTypeComputer, - accept: ValidationAcceptor, -) => { - const boundType = typeComputer.computeType(node.rightOperand); - - if (boundType !== UnknownType && !(boundType instanceof NamedType)) { - accept('error', 'Bounds of type parameters must be named types.', { - node, - property: 'rightOperand', - code: CODE_TYPE_PARAMETER_INVALID_BOUND, + TypeParameter.getUpperBounds(node) + .slice(1) + .forEach((it) => { + accept('error', `The type parameter '${node.name}' can only have a single upper bound.`, { + node: it, + code: CODE_TYPE_PARAMETER_MULTIPLE_BOUNDS, + }); }); - } }; export const typeParameterMustBeUsedInCorrectPosition = (services: SafeDsServices) => { 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 9edbbb2e4..5636b91f8 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 @@ -78,12 +78,16 @@ import { segmentResultMustBeAssignedExactlyOnce, segmentShouldBeUsed, } from './other/declarations/segments.js'; -import { typeParameterBoundLeftOperandMustBeOwnTypeParameter } from './other/declarations/typeParameterBounds.js'; import { + typeParameterBoundLeftOperandMustBeOwnTypeParameter, + typeParameterBoundRightOperandMustBeNamedType, +} from './other/declarations/typeParameterBounds.js'; +import { + typeParameterBoundMustBeAcyclic, typeParameterBoundsMustBeCompatible, typeParameterMustBeUsedInCorrectPosition, - typeParameterMustHaveOneValidLowerAndUpperBound, typeParameterMustHaveSufficientContext, + typeParameterMustNotHaveMultipleBounds, typeParameterMustOnlyBeVariantOnClass, } from './other/declarations/typeParameters.js'; import { callArgumentMustBeConstantIfParameterIsConstant, callMustNotBeRecursive } from './other/expressions/calls.js'; @@ -349,13 +353,17 @@ export const registerValidationChecks = function (services: SafeDsServices) { SdsTemplateString: [templateStringMustHaveExpressionBetweenTwoStringParts], SdsTypeCast: [typeCastExpressionMustHaveUnknownType(services)], SdsTypeParameter: [ + typeParameterBoundMustBeAcyclic, typeParameterBoundsMustBeCompatible(services), typeParameterMustBeUsedInCorrectPosition(services), typeParameterMustHaveSufficientContext, - typeParameterMustHaveOneValidLowerAndUpperBound(services), + typeParameterMustNotHaveMultipleBounds, typeParameterMustOnlyBeVariantOnClass, ], - SdsTypeParameterBound: [typeParameterBoundLeftOperandMustBeOwnTypeParameter], + SdsTypeParameterBound: [ + typeParameterBoundLeftOperandMustBeOwnTypeParameter, + typeParameterBoundRightOperandMustBeNamedType(services), + ], SdsTypeParameterList: [ typeParameterListMustNotHaveRequiredTypeParametersAfterOptionalTypeParameters, typeParameterListShouldNotBeEmpty(services), diff --git a/packages/safe-ds-lang/tests/helpers/testAssertions.ts b/packages/safe-ds-lang/tests/helpers/testAssertions.ts new file mode 100644 index 000000000..820d0a92a --- /dev/null +++ b/packages/safe-ds-lang/tests/helpers/testAssertions.ts @@ -0,0 +1,12 @@ +import { Type } from '../../src/language/typing/model.js'; +import { AssertionError } from 'assert'; + +export const expectEqualTypes = (actual: Type, expected: Type) => { + if (!actual.equals(expected)) { + throw new AssertionError({ + message: `Expected type '${actual.toString()}' to equal type '${expected.toString()}'.`, + actual: actual.toString(), + expected: expected.toString(), + }); + } +}; diff --git a/packages/safe-ds-lang/tests/language/typing/model.test.ts b/packages/safe-ds-lang/tests/language/typing/model.test.ts index 1303be06d..2ea2b4aa5 100644 --- a/packages/safe-ds-lang/tests/language/typing/model.test.ts +++ b/packages/safe-ds-lang/tests/language/typing/model.test.ts @@ -25,6 +25,7 @@ import { } from '../../../src/language/typing/model.js'; import { getNodeOfType } from '../../helpers/nodeFinder.js'; import type { EqualsTest, ToStringTest } from '../../helpers/testDescription.js'; +import { expectEqualTypes } from '../../helpers/testAssertions.js'; const services = (await createSafeDsServicesWithBuiltins(NodeFileSystem)).SafeDs; const code = ` @@ -327,11 +328,13 @@ describe('type model', async () => { ]; describe.each(substituteTypeParametersTest)('substituteTypeParameters', ({ type, substitutions, expectedType }) => { it(`should return the expected value (${type.constructor.name} -- ${type})`, () => { - expect(type.substituteTypeParameters(substitutions).equals(expectedType)).toBeTruthy(); + const actual = type.substituteTypeParameters(substitutions); + expectEqualTypes(actual, expectedType); }); it(`should not change if no substitutions are passed (${type.constructor.name} -- ${type})`, () => { - expect(type.substituteTypeParameters(new Map()).equals(type)).toBeTruthy(); + const actual = type.substituteTypeParameters(new Map()); + expectEqualTypes(actual, type); }); }); @@ -427,7 +430,8 @@ describe('type model', async () => { ]; describe.each(unwrapTests)('unwrap', ({ type, expectedType }) => { it(`should remove any unnecessary containers (${type.constructor.name} -- ${type})`, () => { - expect(type.unwrap()).toSatisfy((actualType: Type) => actualType.equals(expectedType)); + const actual = type.unwrap(); + expectEqualTypes(actual, expectedType); }); }); @@ -571,7 +575,8 @@ describe('type model', async () => { ]; describe.each(updateNullabilityTests)('updateNullability', ({ type, isNullable, expectedType }) => { it(`should return the expected value (${type.constructor.name} -- ${type})`, () => { - expect(type.updateNullability(isNullable).equals(expectedType)).toBeTruthy(); + const actual = type.updateNullability(isNullable); + expectEqualTypes(actual, expectedType); }); }); @@ -596,7 +601,8 @@ describe('type model', async () => { expectedType: new ClassType(class1, new Map(), false), }, ])('should return the type of the parameter at the given index (%#)', ({ type, index, expectedType }) => { - expect(type.getParameterTypeByIndex(index).equals(expectedType)).toBeTruthy(); + const actual = type.getParameterTypeByIndex(index); + expectEqualTypes(actual, expectedType); }); }); }); @@ -615,7 +621,8 @@ describe('type model', async () => { expectedType: new UnionType(), }, ])('should return the type of the parameter at the given index (%#)', ({ type, index, expectedType }) => { - expect(type.getTypeParameterTypeByIndex(index).equals(expectedType)).toBeTruthy(); + const actual = type.getTypeParameterTypeByIndex(index); + expectEqualTypes(actual, expectedType); }); }); }); @@ -636,7 +643,8 @@ describe('type model', async () => { expectedType: new ClassType(class1, new Map(), false), }, ])('should return the entry at the given index (%#)', ({ type, index, expectedType }) => { - expect(type.getTypeOfEntryByIndex(index).equals(expectedType)).toBeTruthy(); + const actual = type.getTypeOfEntryByIndex(index); + expectEqualTypes(actual, expectedType); }); }); }); diff --git a/packages/safe-ds-lang/tests/language/typing/safe-ds-class-hierarchy.test.ts b/packages/safe-ds-lang/tests/language/typing/safe-ds-class-hierarchy.test.ts index 000f65b66..27621fccd 100644 --- a/packages/safe-ds-lang/tests/language/typing/safe-ds-class-hierarchy.test.ts +++ b/packages/safe-ds-lang/tests/language/typing/safe-ds-class-hierarchy.test.ts @@ -7,8 +7,7 @@ import { SdsClass, type SdsClassMember, } from '../../../src/language/generated/ast.js'; -import { getClassMembers } from '../../../src/language/helpers/nodeProperties.js'; -import { createSafeDsServicesWithBuiltins } from '../../../src/language/index.js'; +import { createSafeDsServicesWithBuiltins, getClassMembers } from '../../../src/language/index.js'; import { getNodeOfType } from '../../helpers/nodeFinder.js'; const services = (await createSafeDsServicesWithBuiltins(NodeFileSystem)).SafeDs; diff --git a/packages/safe-ds-lang/tests/language/typing/type computer/computeBound.test.ts b/packages/safe-ds-lang/tests/language/typing/type computer/computeBound.test.ts new file mode 100644 index 000000000..fdd344491 --- /dev/null +++ b/packages/safe-ds-lang/tests/language/typing/type computer/computeBound.test.ts @@ -0,0 +1,134 @@ +import { NodeFileSystem } from 'langium/node'; +import { describe, it } from 'vitest'; +import { isSdsClass, isSdsModule, SdsTypeParameter } from '../../../../src/language/generated/ast.js'; +import { + createSafeDsServicesWithBuiltins, + getModuleMembers, + getTypeParameters, +} from '../../../../src/language/index.js'; +import { Type, UnknownType } from '../../../../src/language/typing/model.js'; +import { getNodeOfType } from '../../../helpers/nodeFinder.js'; +import { expectEqualTypes } from '../../../helpers/testAssertions.js'; + +const services = (await createSafeDsServicesWithBuiltins(NodeFileSystem)).SafeDs; +const coreTypes = services.types.CoreTypes; +const typeComputer = services.types.TypeComputer; + +const code = ` + class MyClass< + Unbounded, + LegalDirectBounds, + LegalIndirectBounds, + UnnamedBounds, + UnresolvedBounds, + CyclicBounds, + > where { + LegalDirectBounds super Int, + LegalDirectBounds sub Number, + + LegalIndirectBounds super LegalDirectBounds, + LegalIndirectBounds sub LegalDirectBounds, + + UnnamedBounds super literal<1>, + UnnamedBounds sub literal<2>, + + UnresolvedBounds super unknown, + UnresolvedBounds sub unknown, + + CyclicBounds sub CyclicBounds, + } +`; +const module = await getNodeOfType(services, code, isSdsModule); + +const classes = getModuleMembers(module).filter(isSdsClass); +const typeParameters = getTypeParameters(classes[0]); + +const unbounded = typeParameters[0]!; +const legalDirectBounds = typeParameters[1]!; +const legalIndirectBounds = typeParameters[2]!; +const UnnamedBounds = typeParameters[3]!; +const UnresolvedBounds = typeParameters[4]!; +const CyclicBounds = typeParameters[5]!; + +const computeLowerBoundTests: ComputeBoundTest[] = [ + { + typeParameter: unbounded, + expected: coreTypes.Nothing, + }, + { + typeParameter: legalDirectBounds, + expected: coreTypes.Int, + }, + { + typeParameter: legalIndirectBounds, + expected: coreTypes.Int, + }, + { + typeParameter: UnnamedBounds, + expected: UnknownType, + }, + { + typeParameter: UnresolvedBounds, + expected: UnknownType, + }, + { + typeParameter: CyclicBounds, + expected: UnknownType, + }, +]; + +describe.each(computeLowerBoundTests)('computeLowerBound', ({ typeParameter, expected }) => { + it(`should return the lower bound (${typeParameter.name})`, () => { + const actual = typeComputer.computeLowerBound(typeParameter); + expectEqualTypes(actual, expected); + }); +}); + +const computeUpperBoundTests: ComputeBoundTest[] = [ + { + typeParameter: unbounded, + expected: coreTypes.AnyOrNull, + }, + { + typeParameter: legalDirectBounds, + expected: coreTypes.Number, + }, + { + typeParameter: legalIndirectBounds, + expected: coreTypes.Number, + }, + { + typeParameter: UnnamedBounds, + expected: UnknownType, + }, + { + typeParameter: UnresolvedBounds, + expected: UnknownType, + }, + { + typeParameter: CyclicBounds, + expected: UnknownType, + }, +]; + +describe.each(computeUpperBoundTests)('computeUpperBound', ({ typeParameter, expected }) => { + it(`should return the upper bound (${typeParameter.name})`, () => { + const actual = typeComputer.computeUpperBound(typeParameter); + expectEqualTypes(actual, expected); + }); +}); + +/** + * A test case for {@link TypeComputer.computeLowerBound} and {@link TypeComputer.computeUpperBound}. + */ +interface ComputeBoundTest { + /** + * The type parameter to get the bound for. + */ + typeParameter: SdsTypeParameter; + + /** + * The expected bound + */ + expected: Type; +} diff --git a/packages/safe-ds-lang/tests/language/typing/type computer/computeClassTypeForLiteralType.test.ts b/packages/safe-ds-lang/tests/language/typing/type computer/computeClassTypeForLiteralType.test.ts index 8c422af27..c1f853e41 100644 --- a/packages/safe-ds-lang/tests/language/typing/type computer/computeClassTypeForLiteralType.test.ts +++ b/packages/safe-ds-lang/tests/language/typing/type computer/computeClassTypeForLiteralType.test.ts @@ -1,5 +1,5 @@ import { NodeFileSystem } from 'langium/node'; -import { describe, expect, it } from 'vitest'; +import { describe, it } from 'vitest'; import { createSafeDsServicesWithBuiltins } from '../../../../src/language/index.js'; import { BooleanConstant, @@ -9,6 +9,7 @@ import { StringConstant, } from '../../../../src/language/partialEvaluation/model.js'; import { LiteralType, Type } from '../../../../src/language/typing/model.js'; +import { expectEqualTypes } from '../../../helpers/testAssertions.js'; const services = (await createSafeDsServicesWithBuiltins(NodeFileSystem)).SafeDs; const coreTypes = services.types.CoreTypes; @@ -86,9 +87,8 @@ const tests: ComputeClassTypeForLiteralTypeTest[] = [ describe.each(tests)('computeClassTypeForLiteralType', ({ literalType, expected }) => { it(`should return the class type for a literal type (${literalType})`, () => { - expect(typeComputer.computeClassTypeForLiteralType(literalType)).toSatisfy((actual: Type) => - actual.equals(expected), - ); + const actual = typeComputer.computeClassTypeForLiteralType(literalType); + expectEqualTypes(actual, expected); }); }); diff --git a/packages/safe-ds-lang/tests/language/typing/type computer/lowestCommonSupertype.test.ts b/packages/safe-ds-lang/tests/language/typing/type computer/lowestCommonSupertype.test.ts index 88f6cebe5..f1e212e64 100644 --- a/packages/safe-ds-lang/tests/language/typing/type computer/lowestCommonSupertype.test.ts +++ b/packages/safe-ds-lang/tests/language/typing/type computer/lowestCommonSupertype.test.ts @@ -1,6 +1,6 @@ import { streamAllContents } from 'langium'; import { NodeFileSystem } from 'langium/node'; -import { describe, expect, it } from 'vitest'; +import { describe, it } from 'vitest'; import { isSdsClass, isSdsEnum, @@ -20,6 +20,7 @@ import { UnknownType, } from '../../../../src/language/typing/model.js'; import { getNodeOfType } from '../../../helpers/nodeFinder.js'; +import { expectEqualTypes } from '../../../helpers/testAssertions.js'; const services = (await createSafeDsServicesWithBuiltins(NodeFileSystem)).SafeDs; const coreTypes = services.types.CoreTypes; @@ -317,7 +318,8 @@ const tests: LowestCommonSupertypeTest[] = [ describe.each(tests)('lowestCommonSupertype', ({ types, expected }) => { it(`should return the lowest common supertype [${types}]`, () => { - expect(typeComputer.lowestCommonSupertype(...types)).toSatisfy((actual: Type) => actual.equals(expected)); + const actual = typeComputer.lowestCommonSupertype(...types); + expectEqualTypes(actual, expected); }); }); diff --git a/packages/safe-ds-lang/tests/resources/validation/other/declarations/type parameters/bound must be named type/main.sdstest b/packages/safe-ds-lang/tests/resources/validation/other/declarations/constraints/type parameter bounds/right operand must be named type/main.sdstest similarity index 84% rename from packages/safe-ds-lang/tests/resources/validation/other/declarations/type parameters/bound must be named type/main.sdstest rename to packages/safe-ds-lang/tests/resources/validation/other/declarations/constraints/type parameter bounds/right operand must be named type/main.sdstest index 47483893a..7bed22f10 100644 --- a/packages/safe-ds-lang/tests/resources/validation/other/declarations/type parameters/bound must be named type/main.sdstest +++ b/packages/safe-ds-lang/tests/resources/validation/other/declarations/constraints/type parameter bounds/right operand must be named type/main.sdstest @@ -1,4 +1,4 @@ -package tests.validation.other.declarations.typeParameters.boundMustBeNamedType +package tests.validation.other.declarations.typeParameters.rightOperandMustBeNamedType class C enum E { @@ -8,24 +8,24 @@ enum E { class MyClass1 where { // $TEST$ error "Bounds of type parameters must be named types." T sub »() -> ()«, - // $TEST$ no error "Bounds of type parameters must be named types." + // $TEST$ error "Bounds of type parameters must be named types." T sub »() -> ()«, // $TEST$ error "Bounds of type parameters must be named types." T super »() -> ()«, - // $TEST$ no error "Bounds of type parameters must be named types." + // $TEST$ error "Bounds of type parameters must be named types." T super »() -> ()«, } class MyClass2 where { // $TEST$ error "Bounds of type parameters must be named types." T sub »literal<1>«, - // $TEST$ no error "Bounds of type parameters must be named types." + // $TEST$ error "Bounds of type parameters must be named types." T sub »literal<1>«, // $TEST$ error "Bounds of type parameters must be named types." T super »literal<1>«, - // $TEST$ no error "Bounds of type parameters must be named types." + // $TEST$ error "Bounds of type parameters must be named types." T super »literal<1>«, } @@ -80,12 +80,12 @@ class MyClass6 where { class MyClass7 where { // $TEST$ error "Bounds of type parameters must be named types." T sub »union«, - // $TEST$ no error "Bounds of type parameters must be named types." + // $TEST$ error "Bounds of type parameters must be named types." T sub »union«, // $TEST$ error "Bounds of type parameters must be named types." T super »union«, - // $TEST$ no error "Bounds of type parameters must be named types." + // $TEST$ error "Bounds of type parameters must be named types." T super »union«, } diff --git a/packages/safe-ds-lang/tests/resources/validation/other/declarations/type parameters/must not have required after optional/main.sdstest b/packages/safe-ds-lang/tests/resources/validation/other/declarations/type parameter lists/must not have required after optional/main.sdstest similarity index 100% rename from packages/safe-ds-lang/tests/resources/validation/other/declarations/type parameters/must not have required after optional/main.sdstest rename to packages/safe-ds-lang/tests/resources/validation/other/declarations/type parameter lists/must not have required after optional/main.sdstest diff --git a/packages/safe-ds-lang/tests/resources/validation/other/declarations/type parameters/bounds must be acyclic/main.sdstest b/packages/safe-ds-lang/tests/resources/validation/other/declarations/type parameters/bounds must be acyclic/main.sdstest new file mode 100644 index 000000000..0d8f8fc90 --- /dev/null +++ b/packages/safe-ds-lang/tests/resources/validation/other/declarations/type parameters/bounds must be acyclic/main.sdstest @@ -0,0 +1,27 @@ +package tests.validation.other.declarations.typeParameters.boundsMustBeAcyclic + +class C where { + // $TEST$ no error "Bounds of type parameters must be acyclic." + »T1 super Int«, + // $TEST$ no error "Bounds of type parameters must be acyclic." + »T1 sub Number«, + // $TEST$ no error "Bounds of type parameters must be acyclic." + »T1 super T1«, + // $TEST$ no error "Bounds of type parameters must be acyclic." + »T1 sub T1«, + + // $TEST$ error "Bounds of type parameters must be acyclic." + »T2 super T2«, + // $TEST$ error "Bounds of type parameters must be acyclic." + »T3 sub T3«, + + // $TEST$ error "Bounds of type parameters must be acyclic." + »T4 super T3«, + // $TEST$ error "Bounds of type parameters must be acyclic." + »T5 sub T3«, + + // $TEST$ no error "Bounds of type parameters must be acyclic." + »T6 super unknown«, + // $TEST$ no error "Bounds of type parameters must be acyclic." + »T6 sub unknown«, +}