Skip to content

Commit 4518aee

Browse files
authored
feat: scoping for inherited members (#706)
Closes #540 ### Summary of Changes Implement reference resolution to inherited class members.
1 parent 96f44c7 commit 4518aee

File tree

8 files changed

+263
-638
lines changed

8 files changed

+263
-638
lines changed

packages/safe-ds-lang/src/language/scoping/safe-ds-scope-provider.ts

+16-14
Original file line numberDiff line numberDiff line change
@@ -63,14 +63,16 @@ import {
6363
getTypeParameters,
6464
isStatic,
6565
} from '../helpers/nodeProperties.js';
66+
import { SafeDsNodeMapper } from '../helpers/safe-ds-node-mapper.js';
6667
import { SafeDsServices } from '../safe-ds-module.js';
68+
import { ClassType, EnumVariantType } from '../typing/model.js';
69+
import type { SafeDsClassHierarchy } from '../typing/safe-ds-class-hierarchy.js';
6770
import { SafeDsTypeComputer } from '../typing/safe-ds-type-computer.js';
6871
import { SafeDsPackageManager } from '../workspace/safe-ds-package-manager.js';
69-
import { SafeDsNodeMapper } from '../helpers/safe-ds-node-mapper.js';
70-
import { ClassType, EnumVariantType } from '../typing/model.js';
7172

7273
export class SafeDsScopeProvider extends DefaultScopeProvider {
7374
private readonly astReflection: AstReflection;
75+
private readonly classHierarchy: SafeDsClassHierarchy;
7476
private readonly nodeMapper: SafeDsNodeMapper;
7577
private readonly packageManager: SafeDsPackageManager;
7678
private readonly typeComputer: SafeDsTypeComputer;
@@ -79,6 +81,7 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {
7981
super(services);
8082

8183
this.astReflection = services.shared.AstReflection;
84+
this.classHierarchy = services.types.ClassHierarchy;
8285
this.nodeMapper = services.helpers.NodeMapper;
8386
this.packageManager = services.workspace.PackageManager;
8487
this.typeComputer = services.types.TypeComputer;
@@ -178,12 +181,9 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {
178181
const declaration = this.getUniqueReferencedDeclarationForExpression(node.receiver);
179182
if (isSdsClass(declaration)) {
180183
const ownStaticMembers = getMatchingClassMembers(declaration, isStatic);
181-
// val superTypeMembers = receiverDeclaration.superClassMembers()
182-
// .filter { it.isStatic() }
183-
// .toList()
184-
//
185-
// return Scopes.scopeFor(ownStaticMembers, Scopes.scopeFor(superTypeMembers))
186-
return this.createScopeForNodes(ownStaticMembers);
184+
const superclassStaticMembers = this.classHierarchy.streamSuperclassMembers(declaration).filter(isStatic);
185+
186+
return this.createScopeForNodes(ownStaticMembers, this.createScopeForNodes(superclassStaticMembers));
187187
} else if (isSdsEnum(declaration)) {
188188
return this.createScopeForNodes(getEnumVariants(declaration));
189189
}
@@ -208,12 +208,14 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {
208208

209209
if (receiverType instanceof ClassType) {
210210
const ownInstanceMembers = getMatchingClassMembers(receiverType.declaration, (it) => !isStatic(it));
211-
// val superTypeMembers = type.sdsClass.superClassMembers()
212-
// .filter { !it.isStatic() }
213-
// .toList()
214-
//
215-
// Scopes.scopeFor(members, Scopes.scopeFor(superTypeMembers, resultScope))
216-
return this.createScopeForNodes(ownInstanceMembers, resultScope);
211+
const superclassInstanceMembers = this.classHierarchy
212+
.streamSuperclassMembers(receiverType.declaration)
213+
.filter((it) => !isStatic(it));
214+
215+
return this.createScopeForNodes(
216+
ownInstanceMembers,
217+
this.createScopeForNodes(superclassInstanceMembers, resultScope),
218+
);
217219
} else if (receiverType instanceof EnumVariantType) {
218220
const parameters = getParameters(receiverType.declaration);
219221
return this.createScopeForNodes(parameters, resultScope);

packages/safe-ds-lang/src/language/typing/model.ts

+11-11
Original file line numberDiff line numberDiff line change
@@ -137,17 +137,6 @@ export class NamedTupleType<T extends SdsDeclaration> extends Type {
137137
return this.entries[index]?.type ?? UnknownType;
138138
}
139139

140-
/**
141-
* If this only has one entry, returns its type. Otherwise, returns this.
142-
*/
143-
override unwrap(): Type {
144-
if (this.entries.length === 1) {
145-
return this.entries[0].type;
146-
}
147-
148-
return this;
149-
}
150-
151140
override equals(other: Type): boolean {
152141
if (other === this) {
153142
return true;
@@ -164,6 +153,17 @@ export class NamedTupleType<T extends SdsDeclaration> extends Type {
164153
override toString(): string {
165154
return `(${this.entries.join(', ')})`;
166155
}
156+
157+
/**
158+
* If this only has one entry, returns its type. Otherwise, returns this.
159+
*/
160+
override unwrap(): Type {
161+
if (this.entries.length === 1) {
162+
return this.entries[0].type;
163+
}
164+
165+
return this;
166+
}
167167
}
168168

169169
export class NamedTupleEntry<T extends SdsDeclaration> {

packages/safe-ds-lang/src/language/typing/safe-ds-class-hierarchy.ts

+13-24
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
import { SafeDsServices } from '../safe-ds-module.js';
2-
import { SafeDsClasses } from '../builtins/safe-ds-classes.js';
3-
import { SdsClass } from '../generated/ast.js';
41
import { EMPTY_STREAM, stream, Stream } from 'langium';
5-
import { getParentTypes } from '../helpers/nodeProperties.js';
6-
import { SafeDsTypeComputer } from './safe-ds-type-computer.js';
2+
import { SafeDsClasses } from '../builtins/safe-ds-classes.js';
3+
import { SdsClass, type SdsClassMember } from '../generated/ast.js';
4+
import { getMatchingClassMembers, getParentTypes } from '../helpers/nodeProperties.js';
5+
import { SafeDsServices } from '../safe-ds-module.js';
76
import { ClassType } from './model.js';
7+
import { SafeDsTypeComputer } from './safe-ds-type-computer.js';
88

99
export class SafeDsClassHierarchy {
1010
private readonly builtinClasses: SafeDsClasses;
@@ -60,6 +60,14 @@ export class SafeDsClassHierarchy {
6060
}
6161
}
6262

63+
streamSuperclassMembers(node: SdsClass | undefined): Stream<SdsClassMember> {
64+
if (!node) {
65+
return EMPTY_STREAM;
66+
}
67+
68+
return this.streamSuperclasses(node).flatMap(getMatchingClassMembers);
69+
}
70+
6371
/**
6472
* Returns the parent class of the given class, or undefined if there is no parent class. Only the first parent
6573
* type is considered, i.e. multiple inheritance is not supported.
@@ -74,22 +82,3 @@ export class SafeDsClassHierarchy {
7482
return undefined;
7583
}
7684
}
77-
78-
// fun SdsClass.superClassMembers() =
79-
// this.superClasses().flatMap { it.classMembersOrEmpty().asSequence() }
80-
//
81-
// // TODO only static methods can be hidden
82-
// fun SdsFunction.hiddenFunction(): SdsFunction? {
83-
// val containingClassOrInterface = closestAncestorOrNull<SdsClass>() ?: return null
84-
// return containingClassOrInterface.superClassMembers()
85-
// .filterIsInstance<SdsFunction>()
86-
// .firstOrNull { it.name == name }
87-
// }
88-
//
89-
// fun SdsClass?.inheritedNonStaticMembersOrEmpty(): Set<SdsAbstractDeclaration> {
90-
// return this?.parentClassesOrEmpty()
91-
// ?.flatMap { it.classMembersOrEmpty() }
92-
// ?.filter { it is SdsAttribute && !it.isStatic || it is SdsFunction && !it.isStatic }
93-
// ?.toSet()
94-
// .orEmpty()
95-
// }

packages/safe-ds-lang/tests/language/typing/safe-ds-class-hierarchy.test.ts

+71-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
2-
import { createSafeDsServices } from '../../../src/language/safe-ds-module.js';
31
import { NodeFileSystem } from 'langium/node';
42
import { clearDocuments } from 'langium/test';
3+
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
54
import { isSdsClass, SdsClass } from '../../../src/language/generated/ast.js';
5+
import { createSafeDsServices } from '../../../src/language/index.js';
66
import { getNodeOfType } from '../../helpers/nodeFinder.js';
77

88
const services = createSafeDsServices(NodeFileSystem).SafeDs;
@@ -131,4 +131,73 @@ describe('SafeDsClassHierarchy', async () => {
131131
expect(superclassNames(firstClass)).toStrictEqual(expected);
132132
});
133133
});
134+
135+
describe('streamSuperclassMembers', () => {
136+
const superclassMemberNames = (node: SdsClass | undefined) =>
137+
classHierarchy
138+
.streamSuperclassMembers(node)
139+
.map((member) => member.name)
140+
.toArray();
141+
142+
it('should return an empty stream if passed undefined', () => {
143+
expect(superclassMemberNames(undefined)).toStrictEqual([]);
144+
});
145+
146+
const testCases = [
147+
{
148+
testName: 'should return the members of the parent type',
149+
code: `
150+
class A {
151+
attr a: Int
152+
fun f()
153+
}
154+
155+
class B sub A
156+
`,
157+
index: 1,
158+
expected: ['a', 'f'],
159+
},
160+
{
161+
testName: 'should only consider members of the first parent type',
162+
code: `
163+
class A {
164+
attr a: Int
165+
fun f()
166+
}
167+
168+
class B {
169+
attr b: Int
170+
fun g()
171+
}
172+
173+
class C sub A, B
174+
`,
175+
index: 2,
176+
expected: ['a', 'f'],
177+
},
178+
{
179+
testName: 'should return members of all superclasses',
180+
code: `
181+
class A {
182+
attr a: Int
183+
fun f()
184+
}
185+
186+
class B sub A {
187+
attr b: Int
188+
fun g()
189+
}
190+
191+
class C sub B
192+
`,
193+
index: 2,
194+
expected: ['b', 'g', 'a', 'f'],
195+
},
196+
];
197+
198+
it.each(testCases)('$testName', async ({ code, index, expected }) => {
199+
const firstClass = await getNodeOfType(services, code, isSdsClass, index);
200+
expect(superclassMemberNames(firstClass)).toStrictEqual(expected);
201+
});
202+
});
134203
});

0 commit comments

Comments
 (0)