Skip to content

Commit

Permalink
feat: scoping for inherited members (#706)
Browse files Browse the repository at this point in the history
Closes #540

### Summary of Changes

Implement reference resolution to inherited class members.
  • Loading branch information
lars-reimann authored Oct 30, 2023
1 parent 96f44c7 commit 4518aee
Show file tree
Hide file tree
Showing 8 changed files with 263 additions and 638 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,16 @@ import {
getTypeParameters,
isStatic,
} from '../helpers/nodeProperties.js';
import { SafeDsNodeMapper } from '../helpers/safe-ds-node-mapper.js';
import { SafeDsServices } from '../safe-ds-module.js';
import { ClassType, EnumVariantType } from '../typing/model.js';
import type { SafeDsClassHierarchy } from '../typing/safe-ds-class-hierarchy.js';
import { SafeDsTypeComputer } from '../typing/safe-ds-type-computer.js';
import { SafeDsPackageManager } from '../workspace/safe-ds-package-manager.js';
import { SafeDsNodeMapper } from '../helpers/safe-ds-node-mapper.js';
import { ClassType, EnumVariantType } from '../typing/model.js';

export class SafeDsScopeProvider extends DefaultScopeProvider {
private readonly astReflection: AstReflection;
private readonly classHierarchy: SafeDsClassHierarchy;
private readonly nodeMapper: SafeDsNodeMapper;
private readonly packageManager: SafeDsPackageManager;
private readonly typeComputer: SafeDsTypeComputer;
Expand All @@ -79,6 +81,7 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {
super(services);

this.astReflection = services.shared.AstReflection;
this.classHierarchy = services.types.ClassHierarchy;
this.nodeMapper = services.helpers.NodeMapper;
this.packageManager = services.workspace.PackageManager;
this.typeComputer = services.types.TypeComputer;
Expand Down Expand Up @@ -178,12 +181,9 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {
const declaration = this.getUniqueReferencedDeclarationForExpression(node.receiver);
if (isSdsClass(declaration)) {
const ownStaticMembers = getMatchingClassMembers(declaration, isStatic);
// val superTypeMembers = receiverDeclaration.superClassMembers()
// .filter { it.isStatic() }
// .toList()
//
// return Scopes.scopeFor(ownStaticMembers, Scopes.scopeFor(superTypeMembers))
return this.createScopeForNodes(ownStaticMembers);
const superclassStaticMembers = this.classHierarchy.streamSuperclassMembers(declaration).filter(isStatic);

return this.createScopeForNodes(ownStaticMembers, this.createScopeForNodes(superclassStaticMembers));
} else if (isSdsEnum(declaration)) {
return this.createScopeForNodes(getEnumVariants(declaration));
}
Expand All @@ -208,12 +208,14 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {

if (receiverType instanceof ClassType) {
const ownInstanceMembers = getMatchingClassMembers(receiverType.declaration, (it) => !isStatic(it));
// val superTypeMembers = type.sdsClass.superClassMembers()
// .filter { !it.isStatic() }
// .toList()
//
// Scopes.scopeFor(members, Scopes.scopeFor(superTypeMembers, resultScope))
return this.createScopeForNodes(ownInstanceMembers, resultScope);
const superclassInstanceMembers = this.classHierarchy
.streamSuperclassMembers(receiverType.declaration)
.filter((it) => !isStatic(it));

return this.createScopeForNodes(
ownInstanceMembers,
this.createScopeForNodes(superclassInstanceMembers, resultScope),
);
} else if (receiverType instanceof EnumVariantType) {
const parameters = getParameters(receiverType.declaration);
return this.createScopeForNodes(parameters, resultScope);
Expand Down
22 changes: 11 additions & 11 deletions packages/safe-ds-lang/src/language/typing/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,17 +137,6 @@ export class NamedTupleType<T extends SdsDeclaration> extends Type {
return this.entries[index]?.type ?? UnknownType;
}

/**
* If this only has one entry, returns its type. Otherwise, returns this.
*/
override unwrap(): Type {
if (this.entries.length === 1) {
return this.entries[0].type;
}

return this;
}

override equals(other: Type): boolean {
if (other === this) {
return true;
Expand All @@ -164,6 +153,17 @@ export class NamedTupleType<T extends SdsDeclaration> extends Type {
override toString(): string {
return `(${this.entries.join(', ')})`;
}

/**
* If this only has one entry, returns its type. Otherwise, returns this.
*/
override unwrap(): Type {
if (this.entries.length === 1) {
return this.entries[0].type;
}

return this;
}
}

export class NamedTupleEntry<T extends SdsDeclaration> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { SafeDsServices } from '../safe-ds-module.js';
import { SafeDsClasses } from '../builtins/safe-ds-classes.js';
import { SdsClass } from '../generated/ast.js';
import { EMPTY_STREAM, stream, Stream } from 'langium';
import { getParentTypes } from '../helpers/nodeProperties.js';
import { SafeDsTypeComputer } from './safe-ds-type-computer.js';
import { SafeDsClasses } from '../builtins/safe-ds-classes.js';
import { SdsClass, type SdsClassMember } from '../generated/ast.js';
import { getMatchingClassMembers, getParentTypes } from '../helpers/nodeProperties.js';
import { SafeDsServices } from '../safe-ds-module.js';
import { ClassType } from './model.js';
import { SafeDsTypeComputer } from './safe-ds-type-computer.js';

export class SafeDsClassHierarchy {
private readonly builtinClasses: SafeDsClasses;
Expand Down Expand Up @@ -60,6 +60,14 @@ export class SafeDsClassHierarchy {
}
}

streamSuperclassMembers(node: SdsClass | undefined): Stream<SdsClassMember> {
if (!node) {
return EMPTY_STREAM;
}

return this.streamSuperclasses(node).flatMap(getMatchingClassMembers);
}

/**
* 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.
Expand All @@ -74,22 +82,3 @@ export class SafeDsClassHierarchy {
return undefined;
}
}

// fun SdsClass.superClassMembers() =
// this.superClasses().flatMap { it.classMembersOrEmpty().asSequence() }
//
// // TODO only static methods can be hidden
// fun SdsFunction.hiddenFunction(): SdsFunction? {
// val containingClassOrInterface = closestAncestorOrNull<SdsClass>() ?: return null
// return containingClassOrInterface.superClassMembers()
// .filterIsInstance<SdsFunction>()
// .firstOrNull { it.name == name }
// }
//
// fun SdsClass?.inheritedNonStaticMembersOrEmpty(): Set<SdsAbstractDeclaration> {
// return this?.parentClassesOrEmpty()
// ?.flatMap { it.classMembersOrEmpty() }
// ?.filter { it is SdsAttribute && !it.isStatic || it is SdsFunction && !it.isStatic }
// ?.toSet()
// .orEmpty()
// }
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
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 { afterEach, beforeEach, describe, expect, it } from 'vitest';
import { isSdsClass, SdsClass } from '../../../src/language/generated/ast.js';
import { createSafeDsServices } from '../../../src/language/index.js';
import { getNodeOfType } from '../../helpers/nodeFinder.js';

const services = createSafeDsServices(NodeFileSystem).SafeDs;
Expand Down Expand Up @@ -131,4 +131,73 @@ describe('SafeDsClassHierarchy', async () => {
expect(superclassNames(firstClass)).toStrictEqual(expected);
});
});

describe('streamSuperclassMembers', () => {
const superclassMemberNames = (node: SdsClass | undefined) =>
classHierarchy
.streamSuperclassMembers(node)
.map((member) => member.name)
.toArray();

it('should return an empty stream if passed undefined', () => {
expect(superclassMemberNames(undefined)).toStrictEqual([]);
});

const testCases = [
{
testName: 'should return the members of the parent type',
code: `
class A {
attr a: Int
fun f()
}
class B sub A
`,
index: 1,
expected: ['a', 'f'],
},
{
testName: 'should only consider members of the first parent type',
code: `
class A {
attr a: Int
fun f()
}
class B {
attr b: Int
fun g()
}
class C sub A, B
`,
index: 2,
expected: ['a', 'f'],
},
{
testName: 'should return members of all superclasses',
code: `
class A {
attr a: Int
fun f()
}
class B sub A {
attr b: Int
fun g()
}
class C sub B
`,
index: 2,
expected: ['b', 'g', 'a', 'f'],
},
];

it.each(testCases)('$testName', async ({ code, index, expected }) => {
const firstClass = await getNodeOfType(services, code, isSdsClass, index);
expect(superclassMemberNames(firstClass)).toStrictEqual(expected);
});
});
});
Loading

0 comments on commit 4518aee

Please sign in to comment.