Skip to content

Commit 7ec746a

Browse files
feat: various checks related to inheritance (#633)
Closes partially #543 ### Summary of Changes Show an error if a class * inherits from multiple types, * inherits from something that is not a class, or * inherits from itself. --------- Co-authored-by: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com>
1 parent b72768c commit 7ec746a

15 files changed

+322
-14
lines changed

package.json

+6-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,12 @@
6969
"scopeName": "source.safe-ds",
7070
"path": "./syntaxes/safe-ds.tmLanguage.json"
7171
}
72-
]
72+
],
73+
"configurationDefaults": {
74+
"[safe-ds]": {
75+
"editor.wordSeparators": "`~!@#$%^&*()-=+[]{}\\|;:'\",.<>/?»«"
76+
}
77+
}
7378
},
7479
"type": "module",
7580
"main": "out/extension/main.cjs",

src/language/helpers/nodeProperties.ts

+5
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import {
4343
SdsResult,
4444
SdsResultList,
4545
SdsStatement,
46+
SdsType,
4647
SdsTypeArgument,
4748
SdsTypeArgumentList,
4849
SdsTypeParameter,
@@ -165,6 +166,10 @@ export const parametersOrEmpty = (node: SdsCallable | undefined): SdsParameter[]
165166
return node?.parameterList?.parameters ?? [];
166167
};
167168

169+
export const parentTypesOrEmpty = (node: SdsClass | undefined): SdsType[] => {
170+
return node?.parentTypeList?.parentTypes ?? [];
171+
};
172+
168173
export const placeholdersOrEmpty = (node: SdsBlock | undefined): SdsPlaceholder[] => {
169174
return stream(statementsOrEmpty(node))
170175
.filter(isSdsAssignment)

src/language/safe-ds-module.ts

+3
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { SafeDsClasses } from './builtins/safe-ds-classes.js';
2121
import { SafeDsPackageManager } from './workspace/safe-ds-package-manager.js';
2222
import { SafeDsNodeMapper } from './helpers/safe-ds-node-mapper.js';
2323
import { SafeDsAnnotations } from './builtins/safe-ds-annotations.js';
24+
import { SafeDsClassHierarchy } from './typing/safe-ds-class-hierarchy.js';
2425

2526
/**
2627
* Declaration of custom services - add your own service classes here.
@@ -34,6 +35,7 @@ export type SafeDsAddedServices = {
3435
NodeMapper: SafeDsNodeMapper;
3536
};
3637
types: {
38+
ClassHierarchy: SafeDsClassHierarchy;
3739
TypeComputer: SafeDsTypeComputer;
3840
};
3941
workspace: {
@@ -71,6 +73,7 @@ export const SafeDsModule: Module<SafeDsServices, PartialLangiumServices & SafeD
7173
ScopeProvider: (services) => new SafeDsScopeProvider(services),
7274
},
7375
types: {
76+
ClassHierarchy: (services) => new SafeDsClassHierarchy(services),
7477
TypeComputer: (services) => new SafeDsTypeComputer(services),
7578
},
7679
workspace: {
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { SafeDsServices } from '../safe-ds-module.js';
2+
import { SafeDsClasses } from '../builtins/safe-ds-classes.js';
3+
import { SdsClass } from '../generated/ast.js';
4+
import { stream, Stream } from 'langium';
5+
import { parentTypesOrEmpty } from '../helpers/nodeProperties.js';
6+
import { SafeDsTypeComputer } from './safe-ds-type-computer.js';
7+
import { ClassType } from './model.js';
8+
9+
export class SafeDsClassHierarchy {
10+
private readonly builtinClasses: SafeDsClasses;
11+
private readonly typeComputer: SafeDsTypeComputer;
12+
13+
constructor(services: SafeDsServices) {
14+
this.builtinClasses = services.builtins.Classes;
15+
this.typeComputer = services.types.TypeComputer;
16+
}
17+
18+
/**
19+
* Returns a stream of all superclasses of the given class. The class itself is not included in the stream unless
20+
* there is a cycle in the inheritance hierarchy. Direct ancestors are returned first, followed by their ancestors
21+
* and so on.
22+
*/
23+
streamSuperclasses(node: SdsClass | undefined): Stream<SdsClass> {
24+
if (!node) {
25+
return stream();
26+
}
27+
28+
const capturedThis = this;
29+
const generator = function* () {
30+
const visited = new Set<SdsClass>();
31+
let current = capturedThis.parentClassOrUndefined(node);
32+
while (current && !visited.has(current)) {
33+
yield current;
34+
visited.add(current);
35+
current = capturedThis.parentClassOrUndefined(current);
36+
}
37+
38+
const anyClass = capturedThis.builtinClasses.Any;
39+
if (anyClass && node !== anyClass && !visited.has(anyClass)) {
40+
yield anyClass;
41+
}
42+
};
43+
44+
return stream(generator());
45+
}
46+
47+
/**
48+
* Returns the parent class of the given class, or undefined if there is no parent class. Only the first parent
49+
* type is considered, i.e. multiple inheritance is not supported.
50+
*/
51+
private parentClassOrUndefined(node: SdsClass | undefined): SdsClass | undefined {
52+
const [firstParentType] = parentTypesOrEmpty(node);
53+
const computedType = this.typeComputer.computeType(firstParentType);
54+
if (computedType instanceof ClassType) {
55+
return computedType.sdsClass;
56+
}
57+
58+
return undefined;
59+
}
60+
}

src/language/typing/safe-ds-type-computer.ts

+13-10
Original file line numberDiff line numberDiff line change
@@ -85,19 +85,22 @@ import {
8585

8686
export class SafeDsTypeComputer {
8787
private readonly astNodeLocator: AstNodeLocator;
88-
private readonly coreClasses: SafeDsClasses;
88+
private readonly builtinClasses: SafeDsClasses;
8989
private readonly nodeMapper: SafeDsNodeMapper;
9090

9191
private readonly typeCache: WorkspaceCache<string, Type>;
9292

9393
constructor(services: SafeDsServices) {
9494
this.astNodeLocator = services.workspace.AstNodeLocator;
95-
this.coreClasses = services.builtins.Classes;
95+
this.builtinClasses = services.builtins.Classes;
9696
this.nodeMapper = services.helpers.NodeMapper;
9797

9898
this.typeCache = new WorkspaceCache(services.shared);
9999
}
100100

101+
/**
102+
* Computes the type of the given node.
103+
*/
101104
computeType(node: AstNode | undefined): Type {
102105
if (!node) {
103106
return UnknownType;
@@ -507,35 +510,35 @@ export class SafeDsTypeComputer {
507510
// -----------------------------------------------------------------------------------------------------------------
508511

509512
private AnyOrNull(): Type {
510-
return this.createCoreType(this.coreClasses.Any, true);
513+
return this.createCoreType(this.builtinClasses.Any, true);
511514
}
512515

513516
private Boolean(): Type {
514-
return this.createCoreType(this.coreClasses.Boolean);
517+
return this.createCoreType(this.builtinClasses.Boolean);
515518
}
516519

517520
private Float(): Type {
518-
return this.createCoreType(this.coreClasses.Float);
521+
return this.createCoreType(this.builtinClasses.Float);
519522
}
520523

521524
private Int(): Type {
522-
return this.createCoreType(this.coreClasses.Int);
525+
return this.createCoreType(this.builtinClasses.Int);
523526
}
524527

525528
private List(): Type {
526-
return this.createCoreType(this.coreClasses.List);
529+
return this.createCoreType(this.builtinClasses.List);
527530
}
528531

529532
private Map(): Type {
530-
return this.createCoreType(this.coreClasses.Map);
533+
return this.createCoreType(this.builtinClasses.Map);
531534
}
532535

533536
private NothingOrNull(): Type {
534-
return this.createCoreType(this.coreClasses.Nothing, true);
537+
return this.createCoreType(this.builtinClasses.Nothing, true);
535538
}
536539

537540
private String(): Type {
538-
return this.createCoreType(this.coreClasses.String);
541+
return this.createCoreType(this.builtinClasses.String);
539542
}
540543

541544
private createCoreType(coreClass: SdsClass | undefined, isNullable: boolean = false): Type {
+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { ValidationAcceptor } from 'langium';
2+
import { SdsClass } from '../generated/ast.js';
3+
import { parentTypesOrEmpty } from '../helpers/nodeProperties.js';
4+
import { isEmpty } from 'radash';
5+
import { SafeDsServices } from '../safe-ds-module.js';
6+
import { ClassType, UnknownType } from '../typing/model.js';
7+
8+
export const CODE_INHERITANCE_CYCLE = 'inheritance/cycle';
9+
export const CODE_INHERITANCE_MULTIPLE_INHERITANCE = 'inheritance/multiple-inheritance';
10+
export const CODE_INHERITANCE_NOT_A_CLASS = 'inheritance/not-a-class';
11+
12+
export const classMustOnlyInheritASingleClass = (services: SafeDsServices) => {
13+
const typeComputer = services.types.TypeComputer;
14+
const computeType = typeComputer.computeType.bind(typeComputer);
15+
16+
return (node: SdsClass, accept: ValidationAcceptor): void => {
17+
const parentTypes = parentTypesOrEmpty(node);
18+
if (isEmpty(parentTypes)) {
19+
return;
20+
}
21+
22+
const [firstParentType, ...otherParentTypes] = parentTypes;
23+
24+
// First parent type must be a class
25+
const computedType = computeType(firstParentType);
26+
if (computedType !== UnknownType && !(computedType instanceof ClassType)) {
27+
accept('error', 'A class must only inherit classes.', {
28+
node: firstParentType,
29+
code: CODE_INHERITANCE_NOT_A_CLASS,
30+
});
31+
}
32+
33+
// Must have only one parent type
34+
for (const parentType of otherParentTypes) {
35+
accept('error', 'Multiple inheritance is not supported. Only the first parent type will be considered.', {
36+
node: parentType,
37+
code: CODE_INHERITANCE_MULTIPLE_INHERITANCE,
38+
});
39+
}
40+
};
41+
};
42+
43+
export const classMustNotInheritItself = (services: SafeDsServices) => {
44+
const classHierarchy = services.types.ClassHierarchy;
45+
46+
return (node: SdsClass, accept: ValidationAcceptor): void => {
47+
const superClasses = classHierarchy.streamSuperclasses(node);
48+
if (superClasses.includes(node)) {
49+
accept('error', 'A class must not directly or indirectly be a subtype of itself.', {
50+
node: parentTypesOrEmpty(node)[0],
51+
code: CODE_INHERITANCE_CYCLE,
52+
});
53+
}
54+
};
55+
};

src/language/validation/safe-ds-validator.ts

+7-2
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ import {
7171
import { placeholderShouldBeUsed, placeholdersMustNotBeAnAlias } from './other/declarations/placeholders.js';
7272
import { segmentParameterShouldBeUsed, segmentResultMustBeAssignedExactlyOnce } from './other/declarations/segments.js';
7373
import { lambdaParameterMustNotHaveConstModifier } from './other/expressions/lambdas.js';
74-
import { indexedAccessesShouldBeUsedWithCaution } from './experimentalLanguageFeature.js';
74+
import { indexedAccessesShouldBeUsedWithCaution } from './experimentalLanguageFeatures.js';
7575
import { requiredParameterMustNotBeExpert } from './builtins/expert.js';
7676
import {
7777
callableTypeParametersMustNotBeAnnotated,
@@ -86,6 +86,7 @@ import {
8686
namedTypeMustNotSetTypeParameterMultipleTimes,
8787
namedTypeTypeArgumentListMustNotHavePositionalArgumentsAfterNamedArguments,
8888
} from './other/types/namedTypes.js';
89+
import { classMustNotInheritItself, classMustOnlyInheritASingleClass } from './inheritance.js';
8990

9091
/**
9192
* Register custom validation checks.
@@ -124,7 +125,11 @@ export const registerValidationChecks = function (services: SafeDsServices) {
124125
callableTypeParameterMustNotHaveConstModifier,
125126
callableTypeResultsMustNotBeAnnotated,
126127
],
127-
SdsClass: [classMustContainUniqueNames],
128+
SdsClass: [
129+
classMustContainUniqueNames,
130+
classMustOnlyInheritASingleClass(services),
131+
classMustNotInheritItself(services),
132+
],
128133
SdsClassBody: [classBodyShouldNotBeEmpty],
129134
SdsConstraintList: [constraintListShouldNotBeEmpty],
130135
SdsDeclaration: [nameMustNotStartWithBlockLambdaPrefix, nameShouldHaveCorrectCasing],

tests/language/helpers/stringUtils.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ describe('pluralize', () => {
3636
singular: 'apple',
3737
expected: 'apples',
3838
},
39-
])('should return the singular or plural form based on the count', ({ count, singular, plural, expected }) => {
39+
])('should return the singular or plural form based on the count (%#)', ({ count, singular, plural, expected }) => {
4040
expect(pluralize(count, singular, plural)).toBe(expected);
4141
});
4242
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2+
import { createSafeDsServices } from '../../../src/language/safe-ds-module.js';
3+
import { NodeFileSystem } from 'langium/node';
4+
import { clearDocuments } from 'langium/test';
5+
import { isSdsClass, SdsClass } from '../../../src/language/generated/ast.js';
6+
import { getNodeOfType } from '../../helpers/nodeFinder.js';
7+
8+
const services = createSafeDsServices(NodeFileSystem).SafeDs;
9+
const classHierarchy = services.types.ClassHierarchy;
10+
11+
describe('SafeDsClassHierarchy', async () => {
12+
beforeEach(async () => {
13+
// Load the builtin library
14+
await services.shared.workspace.WorkspaceManager.initializeWorkspace([]);
15+
});
16+
17+
afterEach(async () => {
18+
await clearDocuments(services);
19+
});
20+
21+
describe('streamSuperclasses', () => {
22+
const superclassNames = (node: SdsClass | undefined) =>
23+
classHierarchy
24+
.streamSuperclasses(node)
25+
.map((clazz) => clazz.name)
26+
.toArray();
27+
28+
it('should return an empty stream if passed undefined', () => {
29+
expect(superclassNames(undefined)).toStrictEqual([]);
30+
});
31+
32+
const testCases = [
33+
{
34+
testName: 'should return "Any" if the class has no parent types',
35+
code: `
36+
class A
37+
`,
38+
expected: ['Any'],
39+
},
40+
{
41+
testName: 'should return "Any" if the first parent type is not a class',
42+
code: `
43+
class A sub E
44+
enum E
45+
`,
46+
expected: ['Any'],
47+
},
48+
{
49+
testName: 'should return the superclasses of a class (no cycle, implicit any)',
50+
code: `
51+
class A sub B
52+
class B
53+
`,
54+
expected: ['B', 'Any'],
55+
},
56+
{
57+
testName: 'should return the superclasses of a class (no cycle, explicit any)',
58+
code: `
59+
class A sub Any
60+
`,
61+
expected: ['Any'],
62+
},
63+
{
64+
testName: 'should return the superclasses of a class (cycle)',
65+
code: `
66+
class A sub B
67+
class B sub C
68+
class C sub A
69+
`,
70+
expected: ['B', 'C', 'A', 'Any'],
71+
},
72+
{
73+
testName: 'should only consider the first parent type',
74+
code: `
75+
class A sub B, C
76+
class B
77+
class C
78+
`,
79+
expected: ['B', 'Any'],
80+
},
81+
];
82+
83+
it.each(testCases)('$testName', async ({ code, expected }) => {
84+
const firstClass = await getNodeOfType(services, code, isSdsClass);
85+
expect(superclassNames(firstClass)).toStrictEqual(expected);
86+
});
87+
});
88+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package tests.validation.inheritance.mustBeAcyclic
2+
3+
// $TEST$ error "A class must not directly or indirectly be a subtype of itself."
4+
class MyClass1 sub »MyClass3«
5+
// $TEST$ error "A class must not directly or indirectly be a subtype of itself."
6+
class MyClass2 sub »MyClass1«
7+
// $TEST$ error "A class must not directly or indirectly be a subtype of itself."
8+
class MyClass3 sub »MyClass2«
9+
10+
class MyClass4
11+
// $TEST$ no error "A class must not directly or indirectly be a subtype of itself."
12+
class MyClass5 sub »MyClass4«
13+
14+
// $TEST$ no error "A class must not directly or indirectly be a subtype of itself."
15+
class MyClass6 sub »MyClass7«
16+
// $TEST$ no error "A class must not directly or indirectly be a subtype of itself."
17+
class MyClass7 sub Any, »MyClass6«
18+
19+
// $TEST$ no error "A class must not directly or indirectly be a subtype of itself."
20+
class MyClass8 sub »Unresolved«
21+
// $TEST$ no error "A class must not directly or indirectly be a subtype of itself."
22+
class MyClass9 sub »MyClass8«

0 commit comments

Comments
 (0)