From 33e1f85112d9a449a3b40f10c8dd7e7ffbbc8eba Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Thu, 12 Oct 2023 10:48:12 +0200 Subject: [PATCH 1/6] =?UTF-8?q?fix:=20double-click=20selection=20no=20long?= =?UTF-8?q?er=20includes=20=C2=BB=C2=AB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index c7ff558fc..7d81ea72b 100644 --- a/package.json +++ b/package.json @@ -69,7 +69,12 @@ "scopeName": "source.safe-ds", "path": "./syntaxes/safe-ds.tmLanguage.json" } - ] + ], + "configurationDefaults": { + "[safe-ds]": { + "editor.wordSeparators": "`~!@#$%^&*()-=+[]{}\\|;:'\",.<>/?»«" + } + } }, "type": "module", "main": "out/extension/main.cjs", From 65c3de05d2b2e50573fc92e069aea0d445fa4a8f Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Thu, 12 Oct 2023 11:10:43 +0200 Subject: [PATCH 2/6] feat: ensure that a class only inherits a single class --- src/language/builtins/safe-ds-annotations.ts | 2 +- src/language/helpers/nodeProperties.ts | 5 +++ src/language/safe-ds-module.ts | 3 ++ .../typing/safe-ds-class-hierarchy.ts | 40 ++++++++++++++++++ src/language/typing/safe-ds-type-computer.ts | 20 ++++----- ...ure.ts => experimentalLanguageFeatures.ts} | 0 src/language/validation/inheritance.ts | 41 +++++++++++++++++++ src/language/validation/safe-ds-validator.ts | 8 +++- .../inheritance/must be acyclic/main.sdstest | 17 ++++++++ .../class with parent types.sdstest | 34 +++++++++++++++ .../class without parent types.sdstest | 4 ++ .../class with parent types.sdstest | 20 +++++++++ .../class without parent types.sdstest | 4 ++ 13 files changed, 185 insertions(+), 13 deletions(-) create mode 100644 src/language/typing/safe-ds-class-hierarchy.ts rename src/language/validation/{experimentalLanguageFeature.ts => experimentalLanguageFeatures.ts} (100%) create mode 100644 src/language/validation/inheritance.ts create mode 100644 tests/resources/validation/inheritance/must be acyclic/main.sdstest create mode 100644 tests/resources/validation/inheritance/must inherit only classes/class with parent types.sdstest create mode 100644 tests/resources/validation/inheritance/must inherit only classes/class without parent types.sdstest create mode 100644 tests/resources/validation/inheritance/no multiple inheritance/class with parent types.sdstest create mode 100644 tests/resources/validation/inheritance/no multiple inheritance/class without parent types.sdstest diff --git a/src/language/builtins/safe-ds-annotations.ts b/src/language/builtins/safe-ds-annotations.ts index aeabecd95..3ce85e66a 100644 --- a/src/language/builtins/safe-ds-annotations.ts +++ b/src/language/builtins/safe-ds-annotations.ts @@ -1,4 +1,4 @@ -import { isSdsAnnotation, SdsAnnotatedObject, SdsAnnotation, SdsParameter } from '../generated/ast.js'; +import {isSdsAnnotation, SdsAnnotatedObject, SdsAnnotation, SdsAnnotationCall, SdsParameter} from '../generated/ast.js'; import { annotationCallsOrEmpty } from '../helpers/nodeProperties.js'; import { SafeDsModuleMembers } from './safe-ds-module-members.js'; import { resourceNameToUri } from '../../helpers/resources.js'; diff --git a/src/language/helpers/nodeProperties.ts b/src/language/helpers/nodeProperties.ts index fe08f4268..e6009e10a 100644 --- a/src/language/helpers/nodeProperties.ts +++ b/src/language/helpers/nodeProperties.ts @@ -43,6 +43,7 @@ import { SdsResult, SdsResultList, SdsStatement, + SdsType, SdsTypeArgument, SdsTypeArgumentList, SdsTypeParameter, @@ -165,6 +166,10 @@ export const parametersOrEmpty = (node: SdsCallable | undefined): SdsParameter[] return node?.parameterList?.parameters ?? []; }; +export const parentTypesOrEmpty = (node: SdsClass | undefined): SdsType[] => { + return node?.parentTypeList?.parentTypes ?? []; +}; + export const placeholdersOrEmpty = (node: SdsBlock | undefined): SdsPlaceholder[] => { return stream(statementsOrEmpty(node)) .filter(isSdsAssignment) diff --git a/src/language/safe-ds-module.ts b/src/language/safe-ds-module.ts index d8cd13302..fe542f0ba 100644 --- a/src/language/safe-ds-module.ts +++ b/src/language/safe-ds-module.ts @@ -21,6 +21,7 @@ import { SafeDsClasses } from './builtins/safe-ds-classes.js'; import { SafeDsPackageManager } from './workspace/safe-ds-package-manager.js'; import { SafeDsNodeMapper } from './helpers/safe-ds-node-mapper.js'; import { SafeDsAnnotations } from './builtins/safe-ds-annotations.js'; +import {SafeDsClassHierarchy} from "./typing/safe-ds-class-hierarchy.js"; /** * Declaration of custom services - add your own service classes here. @@ -34,6 +35,7 @@ export type SafeDsAddedServices = { NodeMapper: SafeDsNodeMapper; }; types: { + ClassHierarchy: SafeDsClassHierarchy; TypeComputer: SafeDsTypeComputer; }; workspace: { @@ -71,6 +73,7 @@ export const SafeDsModule: Module new SafeDsScopeProvider(services), }, types: { + ClassHierarchy: (services) => new SafeDsClassHierarchy(services), TypeComputer: (services) => new SafeDsTypeComputer(services), }, workspace: { diff --git a/src/language/typing/safe-ds-class-hierarchy.ts b/src/language/typing/safe-ds-class-hierarchy.ts new file mode 100644 index 000000000..bd361868a --- /dev/null +++ b/src/language/typing/safe-ds-class-hierarchy.ts @@ -0,0 +1,40 @@ +import { SafeDsServices } from '../safe-ds-module.js'; +import { SafeDsClasses } from '../builtins/safe-ds-classes.js'; +import { SdsClass } from '../generated/ast.js'; +import { stream, Stream } from 'langium'; + +export class SafeDsClassHierarchy { + private readonly builtinClasses: SafeDsClasses; + + constructor(services: SafeDsServices) { + this.builtinClasses = services.builtins.Classes; + } + + superClasses(node: SdsClass): Stream { + const generator = function* () { + const visited = new Set(); + + // let current = node.parentClass; + }; + + return stream(generator()); + // val visited = mutableSetOf() + // + // // TODO: multiple parent classes + // var current = parentClassOrNull() + // while (current != null && current !in visited) { + // yield(current) + // visited += current + // current = current.parentClassOrNull() + // } + // + // val anyClass = this@superClasses.getStdlibClassOrNull(StdlibClasses.Any) + // if (anyClass != null && this@superClasses != anyClass && visited.lastOrNull() != anyClass) { + // yield(anyClass) + // } + } + + private firstParentClassOrUndefined() { + + } +} diff --git a/src/language/typing/safe-ds-type-computer.ts b/src/language/typing/safe-ds-type-computer.ts index cb854922a..a12f2c2a1 100644 --- a/src/language/typing/safe-ds-type-computer.ts +++ b/src/language/typing/safe-ds-type-computer.ts @@ -85,14 +85,14 @@ import { export class SafeDsTypeComputer { private readonly astNodeLocator: AstNodeLocator; - private readonly coreClasses: SafeDsClasses; + private readonly builtinClasses: SafeDsClasses; private readonly nodeMapper: SafeDsNodeMapper; private readonly typeCache: WorkspaceCache; constructor(services: SafeDsServices) { this.astNodeLocator = services.workspace.AstNodeLocator; - this.coreClasses = services.builtins.Classes; + this.builtinClasses = services.builtins.Classes; this.nodeMapper = services.helpers.NodeMapper; this.typeCache = new WorkspaceCache(services.shared); @@ -507,35 +507,35 @@ export class SafeDsTypeComputer { // ----------------------------------------------------------------------------------------------------------------- private AnyOrNull(): Type { - return this.createCoreType(this.coreClasses.Any, true); + return this.createCoreType(this.builtinClasses.Any, true); } private Boolean(): Type { - return this.createCoreType(this.coreClasses.Boolean); + return this.createCoreType(this.builtinClasses.Boolean); } private Float(): Type { - return this.createCoreType(this.coreClasses.Float); + return this.createCoreType(this.builtinClasses.Float); } private Int(): Type { - return this.createCoreType(this.coreClasses.Int); + return this.createCoreType(this.builtinClasses.Int); } private List(): Type { - return this.createCoreType(this.coreClasses.List); + return this.createCoreType(this.builtinClasses.List); } private Map(): Type { - return this.createCoreType(this.coreClasses.Map); + return this.createCoreType(this.builtinClasses.Map); } private NothingOrNull(): Type { - return this.createCoreType(this.coreClasses.Nothing, true); + return this.createCoreType(this.builtinClasses.Nothing, true); } private String(): Type { - return this.createCoreType(this.coreClasses.String); + return this.createCoreType(this.builtinClasses.String); } private createCoreType(coreClass: SdsClass | undefined, isNullable: boolean = false): Type { diff --git a/src/language/validation/experimentalLanguageFeature.ts b/src/language/validation/experimentalLanguageFeatures.ts similarity index 100% rename from src/language/validation/experimentalLanguageFeature.ts rename to src/language/validation/experimentalLanguageFeatures.ts diff --git a/src/language/validation/inheritance.ts b/src/language/validation/inheritance.ts new file mode 100644 index 000000000..6eda28d6a --- /dev/null +++ b/src/language/validation/inheritance.ts @@ -0,0 +1,41 @@ +import { ValidationAcceptor } from 'langium'; +import { SdsClass } from '../generated/ast.js'; +import { parentTypesOrEmpty } from '../helpers/nodeProperties.js'; +import { isEmpty } from 'radash'; +import { SafeDsServices } from '../safe-ds-module.js'; +import { ClassType, UnknownType } from '../typing/model.js'; + +export const CODE_INHERITANCE_CYCLE = 'inheritance/cycle'; +export const CODE_INHERITANCE_MULTIPLE_INHERITANCE = 'inheritance/multiple-inheritance'; +export const CODE_INHERITANCE_NOT_A_CLASS = 'inheritance/not-a-class'; + +export const classMustOnlyInheritASingleClass = (services: SafeDsServices) => { + const typeComputer = services.types.TypeComputer; + const computeType = typeComputer.computeType.bind(typeComputer); + + return (node: SdsClass, accept: ValidationAcceptor): void => { + const parentTypes = parentTypesOrEmpty(node); + if (isEmpty(parentTypes)) { + return; + } + + const [firstParentType, ...otherParentTypes] = parentTypes; + + // First parent type must be a class + const computedType = computeType(firstParentType); + if (computedType !== UnknownType && !(computedType instanceof ClassType)) { + accept('error', 'A class must only inherit classes.', { + node: firstParentType, + code: CODE_INHERITANCE_NOT_A_CLASS, + }); + } + + // Must have only one parent type + for (const parentType of otherParentTypes) { + accept('error', 'Multiple inheritance is not supported. Only the first parent type will be considered.', { + node: parentType, + code: CODE_INHERITANCE_MULTIPLE_INHERITANCE, + }); + } + }; +}; diff --git a/src/language/validation/safe-ds-validator.ts b/src/language/validation/safe-ds-validator.ts index a41c4bb9d..e6fb3435c 100644 --- a/src/language/validation/safe-ds-validator.ts +++ b/src/language/validation/safe-ds-validator.ts @@ -71,7 +71,7 @@ import { import { placeholderShouldBeUsed, placeholdersMustNotBeAnAlias } from './other/declarations/placeholders.js'; import { segmentParameterShouldBeUsed, segmentResultMustBeAssignedExactlyOnce } from './other/declarations/segments.js'; import { lambdaParameterMustNotHaveConstModifier } from './other/expressions/lambdas.js'; -import { indexedAccessesShouldBeUsedWithCaution } from './experimentalLanguageFeature.js'; +import { indexedAccessesShouldBeUsedWithCaution } from './experimentalLanguageFeatures.js'; import { requiredParameterMustNotBeExpert } from './builtins/expert.js'; import { callableTypeParametersMustNotBeAnnotated, @@ -86,6 +86,7 @@ import { namedTypeMustNotSetTypeParameterMultipleTimes, namedTypeTypeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments, } from './other/types/namedTypes.js'; +import {classMustOnlyInheritASingleClass} from "./inheritance.js"; /** * Register custom validation checks. @@ -124,7 +125,10 @@ export const registerValidationChecks = function (services: SafeDsServices) { callableTypeParameterMustNotHaveConstModifier, callableTypeResultsMustNotBeAnnotated, ], - SdsClass: [classMustContainUniqueNames], + SdsClass: [ + classMustContainUniqueNames, + classMustOnlyInheritASingleClass(services), + ], SdsClassBody: [classBodyShouldNotBeEmpty], SdsConstraintList: [constraintListShouldNotBeEmpty], SdsDeclaration: [nameMustNotStartWithBlockLambdaPrefix, nameShouldHaveCorrectCasing], diff --git a/tests/resources/validation/inheritance/must be acyclic/main.sdstest b/tests/resources/validation/inheritance/must be acyclic/main.sdstest new file mode 100644 index 000000000..024c4e586 --- /dev/null +++ b/tests/resources/validation/inheritance/must be acyclic/main.sdstest @@ -0,0 +1,17 @@ +package tests.validation.inheritance.mustBeAcyclic + +// $TEST$ error "A class must not directly or indirectly be a subtype of itself." +class MyClass1 sub »MyClass3« +// $TEST$ error "A class must not directly or indirectly be a subtype of itself." +class MyClass2 sub »MyClass1« +// $TEST$ error "A class must not directly or indirectly be a subtype of itself." +class MyClass3 sub »MyClass2« + +class MyClass4 +// $TEST$ no error "A class must not directly or indirectly be a subtype of itself." +class MyClass5 sub »MyClass4« + +// $TEST$ no error "A class must not directly or indirectly be a subtype of itself." +class MyClass6 sub »Unresolved« +// $TEST$ no error "A class must not directly or indirectly be a subtype of itself." +class MyClass7 sub »MyClass6« diff --git a/tests/resources/validation/inheritance/must inherit only classes/class with parent types.sdstest b/tests/resources/validation/inheritance/must inherit only classes/class with parent types.sdstest new file mode 100644 index 000000000..9312e2b24 --- /dev/null +++ b/tests/resources/validation/inheritance/must inherit only classes/class with parent types.sdstest @@ -0,0 +1,34 @@ +package tests.validation.inheritance.mustInheritOnlyClasses + +class MyClass { + class MyNestedClass +} +enum MyEnum { + MyEnumVariant +} + +// $TEST$ no error "A class must only inherit classes." +// $TEST$ no error "A class must only inherit classes." +// $TEST$ no error "A class must only inherit classes." +// $TEST$ no error "A class must only inherit classes." +// $TEST$ no error "A class must only inherit classes." +class TestClass sub »MyClass«, + »MyClass.MyNestedClass«, + »MyEnum«, + »MyEnum.MyEnumVariant«, + »Unresolved« + +// $TEST$ no error "A class must only inherit classes." +class TestClass sub »MyClass« + +// $TEST$ no error "A class must only inherit classes." +class TestClass sub »MyClass.MyNestedClass« + +// $TEST$ error "A class must only inherit classes." +class TestClass sub »MyEnum« + +// $TEST$ error "A class must only inherit classes." +class TestClass sub »MyEnum.MyEnumVariant« + +// $TEST$ no error "A class must only inherit classes." +class TestClass sub »Unresolved« diff --git a/tests/resources/validation/inheritance/must inherit only classes/class without parent types.sdstest b/tests/resources/validation/inheritance/must inherit only classes/class without parent types.sdstest new file mode 100644 index 000000000..e0b4c3574 --- /dev/null +++ b/tests/resources/validation/inheritance/must inherit only classes/class without parent types.sdstest @@ -0,0 +1,4 @@ +package tests.validation.inheritance.mustInheritOnlyClasses + +// $TEST$ no error "A class must only inherit classes." +class TestClass diff --git a/tests/resources/validation/inheritance/no multiple inheritance/class with parent types.sdstest b/tests/resources/validation/inheritance/no multiple inheritance/class with parent types.sdstest new file mode 100644 index 000000000..2ae2e368a --- /dev/null +++ b/tests/resources/validation/inheritance/no multiple inheritance/class with parent types.sdstest @@ -0,0 +1,20 @@ +package tests.validation.inheritance.noMultipleInheritance + +class MyClass { + class MyNestedClass +} +enum MyEnum { + MyEnumVariant +} + +// $TEST$ no error "Multiple inheritance is not supported. Only the first parent type will be considered." +// $TEST$ error "Multiple inheritance is not supported. Only the first parent type will be considered." +// $TEST$ error "Multiple inheritance is not supported. Only the first parent type will be considered." +// $TEST$ error "Multiple inheritance is not supported. Only the first parent type will be considered." +// $TEST$ error "Multiple inheritance is not supported. Only the first parent type will be considered." +// $TEST$ error "Multiple inheritance is not supported. Only the first parent type will be considered." +class TestClass sub »MyClass«, + »MyClass.MyNestedClass«, + »MyEnum«, + »MyEnum.MyEnumVariant«, + »Unresolved« diff --git a/tests/resources/validation/inheritance/no multiple inheritance/class without parent types.sdstest b/tests/resources/validation/inheritance/no multiple inheritance/class without parent types.sdstest new file mode 100644 index 000000000..baf590697 --- /dev/null +++ b/tests/resources/validation/inheritance/no multiple inheritance/class without parent types.sdstest @@ -0,0 +1,4 @@ +package tests.validation.inheritance.noMultipleInheritance + +// $TEST$ no error "Multiple inheritance is not supported. Only the first parent type will be considered." +class TestClass From d3242f3a3196f747eba1f1ad73e9091151329b42 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Thu, 12 Oct 2023 11:44:37 +0200 Subject: [PATCH 3/6] feat: error if class inherits itself --- .../typing/safe-ds-class-hierarchy.ts | 53 ++++++++++++------- src/language/validation/inheritance.ts | 14 +++++ src/language/validation/safe-ds-validator.ts | 3 +- 3 files changed, 51 insertions(+), 19 deletions(-) diff --git a/src/language/typing/safe-ds-class-hierarchy.ts b/src/language/typing/safe-ds-class-hierarchy.ts index bd361868a..34fd275b5 100644 --- a/src/language/typing/safe-ds-class-hierarchy.ts +++ b/src/language/typing/safe-ds-class-hierarchy.ts @@ -2,39 +2,56 @@ import { SafeDsServices } from '../safe-ds-module.js'; import { SafeDsClasses } from '../builtins/safe-ds-classes.js'; import { SdsClass } from '../generated/ast.js'; import { stream, Stream } from 'langium'; +import { parentTypesOrEmpty } from '../helpers/nodeProperties.js'; +import { SafeDsTypeComputer } from './safe-ds-type-computer.js'; +import { ClassType } from './model.js'; export class SafeDsClassHierarchy { private readonly builtinClasses: SafeDsClasses; + private readonly typeComputer: SafeDsTypeComputer; constructor(services: SafeDsServices) { this.builtinClasses = services.builtins.Classes; + this.typeComputer = services.types.TypeComputer; } - superClasses(node: SdsClass): Stream { + streamSuperClasses(node: SdsClass | undefined): Stream { + /* c8 ignore start */ + if (!node) { + return stream(); + } + /* c8 ignore stop */ + + const capturedThis = this; const generator = function* () { const visited = new Set(); + let current = capturedThis.parentClassOrUndefined(node); + while (current && !visited.has(current)) { + yield current; + visited.add(current); + current = capturedThis.parentClassOrUndefined(current); + } - // let current = node.parentClass; + const anyClass = capturedThis.builtinClasses.Any; + if (anyClass && node !== anyClass && !visited.has(anyClass)) { + yield anyClass; + } }; - return stream(generator()); - // val visited = mutableSetOf() - // - // // TODO: multiple parent classes - // var current = parentClassOrNull() - // while (current != null && current !in visited) { - // yield(current) - // visited += current - // current = current.parentClassOrNull() - // } - // - // val anyClass = this@superClasses.getStdlibClassOrNull(StdlibClasses.Any) - // if (anyClass != null && this@superClasses != anyClass && visited.lastOrNull() != anyClass) { - // yield(anyClass) - // } + return stream((generator)()); } - private firstParentClassOrUndefined() { + /** + * Returns the parent class of the given class, or undefined if there is no parent class. Only the first parent + * type is considered, i.e. multiple inheritance is not supported. + */ + private parentClassOrUndefined(node: SdsClass | undefined): SdsClass | undefined { + const [firstParentType] = parentTypesOrEmpty(node); + const computedType = this.typeComputer.computeType(firstParentType); + if (computedType instanceof ClassType) { + return computedType.sdsClass; + } + return undefined; } } diff --git a/src/language/validation/inheritance.ts b/src/language/validation/inheritance.ts index 6eda28d6a..1ad5cef1b 100644 --- a/src/language/validation/inheritance.ts +++ b/src/language/validation/inheritance.ts @@ -39,3 +39,17 @@ export const classMustOnlyInheritASingleClass = (services: SafeDsServices) => { } }; }; + +export const classMustNotInheritItself = (services: SafeDsServices) => { + const classHierarchy = services.types.ClassHierarchy; + + return (node: SdsClass, accept: ValidationAcceptor): void => { + const superClasses = classHierarchy.streamSuperClasses(node); + if (superClasses.includes(node)) { + accept('error', "A class must not directly or indirectly be a subtype of itself.", { + node: parentTypesOrEmpty(node)[0], + code: CODE_INHERITANCE_CYCLE, + }); + } + } +} diff --git a/src/language/validation/safe-ds-validator.ts b/src/language/validation/safe-ds-validator.ts index e6fb3435c..2b31bae8a 100644 --- a/src/language/validation/safe-ds-validator.ts +++ b/src/language/validation/safe-ds-validator.ts @@ -86,7 +86,7 @@ import { namedTypeMustNotSetTypeParameterMultipleTimes, namedTypeTypeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments, } from './other/types/namedTypes.js'; -import {classMustOnlyInheritASingleClass} from "./inheritance.js"; +import {classMustNotInheritItself, classMustOnlyInheritASingleClass} from "./inheritance.js"; /** * Register custom validation checks. @@ -128,6 +128,7 @@ export const registerValidationChecks = function (services: SafeDsServices) { SdsClass: [ classMustContainUniqueNames, classMustOnlyInheritASingleClass(services), + classMustNotInheritItself(services), ], SdsClassBody: [classBodyShouldNotBeEmpty], SdsConstraintList: [constraintListShouldNotBeEmpty], From 75699d4f1a2eb45a45296fcef2872048b4715f16 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Thu, 12 Oct 2023 12:07:06 +0200 Subject: [PATCH 4/6] test: unit tests for `streamSuperclasses` --- .../typing/safe-ds-class-hierarchy.ts | 9 +- src/language/typing/safe-ds-type-computer.ts | 3 + src/language/validation/inheritance.ts | 2 +- tests/language/helpers/stringUtils.test.ts | 2 +- .../typing/safe-ds-class-hierarchy.test.ts | 88 +++++++++++++++++++ .../inheritance/must be acyclic/main.sdstest | 9 +- 6 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 tests/language/typing/safe-ds-class-hierarchy.test.ts diff --git a/src/language/typing/safe-ds-class-hierarchy.ts b/src/language/typing/safe-ds-class-hierarchy.ts index 34fd275b5..71c50151c 100644 --- a/src/language/typing/safe-ds-class-hierarchy.ts +++ b/src/language/typing/safe-ds-class-hierarchy.ts @@ -15,12 +15,15 @@ export class SafeDsClassHierarchy { this.typeComputer = services.types.TypeComputer; } - streamSuperClasses(node: SdsClass | undefined): Stream { - /* c8 ignore start */ + /** + * Returns a stream of all superclasses of the given class. The class itself is not included in the stream unless + * there is a cycle in the inheritance hierarchy. Direct ancestors are returned first, followed by their ancestors + * and so on. + */ + streamSuperclasses(node: SdsClass | undefined): Stream { if (!node) { return stream(); } - /* c8 ignore stop */ const capturedThis = this; const generator = function* () { diff --git a/src/language/typing/safe-ds-type-computer.ts b/src/language/typing/safe-ds-type-computer.ts index a12f2c2a1..afdfd055c 100644 --- a/src/language/typing/safe-ds-type-computer.ts +++ b/src/language/typing/safe-ds-type-computer.ts @@ -98,6 +98,9 @@ export class SafeDsTypeComputer { this.typeCache = new WorkspaceCache(services.shared); } + /** + * Computes the type of the given node. + */ computeType(node: AstNode | undefined): Type { if (!node) { return UnknownType; diff --git a/src/language/validation/inheritance.ts b/src/language/validation/inheritance.ts index 1ad5cef1b..ffb8cf552 100644 --- a/src/language/validation/inheritance.ts +++ b/src/language/validation/inheritance.ts @@ -44,7 +44,7 @@ export const classMustNotInheritItself = (services: SafeDsServices) => { const classHierarchy = services.types.ClassHierarchy; return (node: SdsClass, accept: ValidationAcceptor): void => { - const superClasses = classHierarchy.streamSuperClasses(node); + const superClasses = classHierarchy.streamSuperclasses(node); if (superClasses.includes(node)) { accept('error', "A class must not directly or indirectly be a subtype of itself.", { node: parentTypesOrEmpty(node)[0], diff --git a/tests/language/helpers/stringUtils.test.ts b/tests/language/helpers/stringUtils.test.ts index e01a25e1f..ef8ac55dc 100644 --- a/tests/language/helpers/stringUtils.test.ts +++ b/tests/language/helpers/stringUtils.test.ts @@ -36,7 +36,7 @@ describe('pluralize', () => { singular: 'apple', expected: 'apples', }, - ])('should return the singular or plural form based on the count', ({ count, singular, plural, expected }) => { + ])('should return the singular or plural form based on the count (%#)', ({ count, singular, plural, expected }) => { expect(pluralize(count, singular, plural)).toBe(expected); }); }); diff --git a/tests/language/typing/safe-ds-class-hierarchy.test.ts b/tests/language/typing/safe-ds-class-hierarchy.test.ts new file mode 100644 index 000000000..b607d6ba0 --- /dev/null +++ b/tests/language/typing/safe-ds-class-hierarchy.test.ts @@ -0,0 +1,88 @@ +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; +import { createSafeDsServices } from '../../../src/language/safe-ds-module.js'; +import { NodeFileSystem } from 'langium/node'; +import { clearDocuments } from 'langium/test'; +import { isSdsClass, SdsClass } from '../../../src/language/generated/ast.js'; +import { getNodeOfType } from '../../helpers/nodeFinder.js'; + +const services = createSafeDsServices(NodeFileSystem).SafeDs; +const classHierarchy = services.types.ClassHierarchy; + +describe('SafeDsClassHierarchy', async () => { + beforeEach(async () => { + // Load the builtin library + await services.shared.workspace.WorkspaceManager.initializeWorkspace([]); + }); + + afterEach(async () => { + await clearDocuments(services); + }); + + describe('streamSuperclasses', () => { + const superclassNames = (node: SdsClass | undefined) => + classHierarchy + .streamSuperclasses(node) + .map((clazz) => clazz.name) + .toArray(); + + it('should return an empty stream if passed undefined', () => { + expect(superclassNames(undefined)).toStrictEqual([]); + }); + + const testCases = [ + { + testName: 'should return "Any" if the class has no parent types', + code: ` + class A + `, + expected: ['Any'], + }, + { + testName: 'should return "Any" if the first parent type is not a class', + code: ` + class A sub E + enum E + `, + expected: ['Any'], + }, + { + testName: 'should return the superclasses of a class (no cycle, implicit any)', + code: ` + class A sub B + class B + `, + expected: ['B', 'Any'], + }, + { + testName: 'should return the superclasses of a class (no cycle, explicit any)', + code: ` + class A sub Any + `, + expected: ['Any'], + }, + { + testName: 'should return the superclasses of a class (cycle)', + code: ` + class A sub B + class B sub C + class C sub A + `, + expected: ['B', 'C', 'A', 'Any'], + }, + { + testName: 'should only consider the first parent type', + code: ` + class A sub B, C + class B + class C + `, + expected: ['B', 'Any'], + }, + ]; + + it.each(testCases)('$testName', async ({ code, expected }) => { + const firstClass = await getNodeOfType(services, code, isSdsClass); + expect(superclassNames(firstClass)).toStrictEqual(expected); + }); + }); +}); diff --git a/tests/resources/validation/inheritance/must be acyclic/main.sdstest b/tests/resources/validation/inheritance/must be acyclic/main.sdstest index 024c4e586..84def22e8 100644 --- a/tests/resources/validation/inheritance/must be acyclic/main.sdstest +++ b/tests/resources/validation/inheritance/must be acyclic/main.sdstest @@ -12,6 +12,11 @@ class MyClass4 class MyClass5 sub »MyClass4« // $TEST$ no error "A class must not directly or indirectly be a subtype of itself." -class MyClass6 sub »Unresolved« +class MyClass6 sub »MyClass7« // $TEST$ no error "A class must not directly or indirectly be a subtype of itself." -class MyClass7 sub »MyClass6« +class MyClass7 sub Any, »MyClass6« + +// $TEST$ no error "A class must not directly or indirectly be a subtype of itself." +class MyClass8 sub »Unresolved« +// $TEST$ no error "A class must not directly or indirectly be a subtype of itself." +class MyClass9 sub »MyClass8« From 1e24104f612576a303eda1be421d2761a6ca5136 Mon Sep 17 00:00:00 2001 From: Lars Reimann Date: Thu, 12 Oct 2023 12:21:12 +0200 Subject: [PATCH 5/6] style: fix linter error --- src/language/builtins/safe-ds-annotations.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/language/builtins/safe-ds-annotations.ts b/src/language/builtins/safe-ds-annotations.ts index 3ce85e66a..aeabecd95 100644 --- a/src/language/builtins/safe-ds-annotations.ts +++ b/src/language/builtins/safe-ds-annotations.ts @@ -1,4 +1,4 @@ -import {isSdsAnnotation, SdsAnnotatedObject, SdsAnnotation, SdsAnnotationCall, SdsParameter} from '../generated/ast.js'; +import { isSdsAnnotation, SdsAnnotatedObject, SdsAnnotation, SdsParameter } from '../generated/ast.js'; import { annotationCallsOrEmpty } from '../helpers/nodeProperties.js'; import { SafeDsModuleMembers } from './safe-ds-module-members.js'; import { resourceNameToUri } from '../../helpers/resources.js'; From 289f1e7e92611ec07d5e1e710d7dfba0d3335718 Mon Sep 17 00:00:00 2001 From: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com> Date: Thu, 12 Oct 2023 10:23:48 +0000 Subject: [PATCH 6/6] style: apply automated linter fixes --- src/language/safe-ds-module.ts | 2 +- src/language/typing/safe-ds-class-hierarchy.ts | 2 +- src/language/validation/inheritance.ts | 6 +++--- src/language/validation/safe-ds-validator.ts | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/language/safe-ds-module.ts b/src/language/safe-ds-module.ts index fe542f0ba..b4ffc13a3 100644 --- a/src/language/safe-ds-module.ts +++ b/src/language/safe-ds-module.ts @@ -21,7 +21,7 @@ import { SafeDsClasses } from './builtins/safe-ds-classes.js'; import { SafeDsPackageManager } from './workspace/safe-ds-package-manager.js'; import { SafeDsNodeMapper } from './helpers/safe-ds-node-mapper.js'; import { SafeDsAnnotations } from './builtins/safe-ds-annotations.js'; -import {SafeDsClassHierarchy} from "./typing/safe-ds-class-hierarchy.js"; +import { SafeDsClassHierarchy } from './typing/safe-ds-class-hierarchy.js'; /** * Declaration of custom services - add your own service classes here. diff --git a/src/language/typing/safe-ds-class-hierarchy.ts b/src/language/typing/safe-ds-class-hierarchy.ts index 71c50151c..cf7608add 100644 --- a/src/language/typing/safe-ds-class-hierarchy.ts +++ b/src/language/typing/safe-ds-class-hierarchy.ts @@ -41,7 +41,7 @@ export class SafeDsClassHierarchy { } }; - return stream((generator)()); + return stream(generator()); } /** diff --git a/src/language/validation/inheritance.ts b/src/language/validation/inheritance.ts index ffb8cf552..8419443e3 100644 --- a/src/language/validation/inheritance.ts +++ b/src/language/validation/inheritance.ts @@ -46,10 +46,10 @@ export const classMustNotInheritItself = (services: SafeDsServices) => { return (node: SdsClass, accept: ValidationAcceptor): void => { const superClasses = classHierarchy.streamSuperclasses(node); if (superClasses.includes(node)) { - accept('error', "A class must not directly or indirectly be a subtype of itself.", { + accept('error', 'A class must not directly or indirectly be a subtype of itself.', { node: parentTypesOrEmpty(node)[0], code: CODE_INHERITANCE_CYCLE, }); } - } -} + }; +}; diff --git a/src/language/validation/safe-ds-validator.ts b/src/language/validation/safe-ds-validator.ts index 2b31bae8a..3737afa72 100644 --- a/src/language/validation/safe-ds-validator.ts +++ b/src/language/validation/safe-ds-validator.ts @@ -86,7 +86,7 @@ import { namedTypeMustNotSetTypeParameterMultipleTimes, namedTypeTypeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments, } from './other/types/namedTypes.js'; -import {classMustNotInheritItself, classMustOnlyInheritASingleClass} from "./inheritance.js"; +import { classMustNotInheritItself, classMustOnlyInheritASingleClass } from './inheritance.js'; /** * Register custom validation checks.