Skip to content

Commit 36663ca

Browse files
authored
fix: prevent overwriting core declarations (#761)
### Summary of Changes Update the scope provider, so core declarations can never be overwritten by own declarations.
1 parent 23b340e commit 36663ca

File tree

5 files changed

+134
-17
lines changed

5 files changed

+134
-17
lines changed

packages/safe-ds-lang/src/language/helpers/nodeProperties.ts

+14
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,20 @@ export const getParentTypes = (node: SdsClass | undefined): SdsType[] => {
237237
return node?.parentTypeList?.parentTypes ?? [];
238238
};
239239

240+
export const getQualifiedName = (node: SdsDeclaration | undefined): string | undefined => {
241+
const segments = [];
242+
243+
let current: SdsDeclaration | undefined = node;
244+
while (current) {
245+
if (current.name) {
246+
segments.unshift(current.name);
247+
}
248+
current = getContainerOfType(current.$container, isSdsDeclaration);
249+
}
250+
251+
return segments.join('.');
252+
};
253+
240254
export const streamPlaceholders = (node: SdsBlock | undefined): Stream<SdsPlaceholder> => {
241255
return stream(getStatements(node)).filter(isSdsAssignment).flatMap(getAssignees).filter(isSdsPlaceholder);
242256
};

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

+36-7
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
getDocument,
99
ReferenceInfo,
1010
Scope,
11+
WorkspaceCache,
1112
} from 'langium';
1213
import {
1314
isSdsAbstractCall,
@@ -36,6 +37,7 @@ import {
3637
isSdsTypeArgument,
3738
isSdsWildcardImport,
3839
isSdsYield,
40+
SdsAnnotation,
3941
SdsArgument,
4042
type SdsCallable,
4143
SdsDeclaration,
@@ -82,6 +84,8 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {
8284
private readonly packageManager: SafeDsPackageManager;
8385
private readonly typeComputer: SafeDsTypeComputer;
8486

87+
private readonly coreDeclarationCache: WorkspaceCache<string, AstNodeDescription[]>;
88+
8589
constructor(services: SafeDsServices) {
8690
super(services);
8791

@@ -90,26 +94,30 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {
9094
this.nodeMapper = services.helpers.NodeMapper;
9195
this.packageManager = services.workspace.PackageManager;
9296
this.typeComputer = services.types.TypeComputer;
97+
98+
this.coreDeclarationCache = new WorkspaceCache(services.shared);
9399
}
94100

95101
override getScope(context: ReferenceInfo): Scope {
96102
const node = context.container;
97103

98-
if (isSdsArgument(node) && context.property === 'parameter') {
104+
if (isSdsAnnotationCall(node) && context.property === 'annotation') {
105+
return this.getScopeForAnnotationCallAnnotation(context);
106+
} else if (isSdsArgument(node) && context.property === 'parameter') {
99107
return this.getScopeForArgumentParameter(node);
100108
} else if (isSdsImportedDeclaration(node) && context.property === 'declaration') {
101109
return this.getScopeForImportedDeclarationDeclaration(node);
102110
} else if (isSdsNamedType(node) && context.property === 'declaration') {
103111
if (isSdsMemberType(node.$container) && node.$containerProperty === 'member') {
104112
return this.getScopeForMemberTypeMember(node.$container);
105113
} else {
106-
return super.getScope(context);
114+
return this.getScopeForNamedTypeDeclaration(context);
107115
}
108116
} else if (isSdsReference(node) && context.property === 'target') {
109117
if (isSdsMemberAccess(node.$container) && node.$containerProperty === 'member') {
110118
return this.getScopeForMemberAccessMember(node.$container);
111119
} else {
112-
return this.getScopeForDirectReferenceTarget(node, context);
120+
return this.getScopeForReferenceTarget(node, context);
113121
}
114122
} else if (isSdsTypeArgument(node) && context.property === 'typeParameter') {
115123
return this.getScopeForTypeArgumentTypeParameter(node);
@@ -120,6 +128,10 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {
120128
}
121129
}
122130

131+
private getScopeForAnnotationCallAnnotation(context: ReferenceInfo) {
132+
return this.coreDeclarations(SdsAnnotation, super.getScope(context));
133+
}
134+
123135
private getScopeForArgumentParameter(node: SdsArgument): Scope {
124136
const containingAbstractCall = getContainerOfType(node, isSdsAbstractCall);
125137
const callable = this.nodeMapper.callToCallable(containingAbstractCall);
@@ -141,7 +153,7 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {
141153
}
142154

143155
const declarationsInPackage = this.packageManager.getDeclarationsInPackage(containingQualifiedImport.package, {
144-
nodeType: 'SdsDeclaration',
156+
nodeType: SdsDeclaration,
145157
hideInternal: containingQualifiedImport.package !== ownPackageName,
146158
});
147159
return this.createScope(declarationsInPackage);
@@ -181,6 +193,10 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {
181193
}
182194
}
183195

196+
private getScopeForNamedTypeDeclaration(context: ReferenceInfo): Scope {
197+
return this.coreDeclarations(SdsNamedTypeDeclaration, super.getScope(context));
198+
}
199+
184200
private getScopeForMemberAccessMember(node: SdsMemberAccess): Scope {
185201
// Static access
186202
const declaration = this.getUniqueReferencedDeclarationForExpression(node.receiver);
@@ -249,9 +265,9 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {
249265
}
250266
}
251267

252-
private getScopeForDirectReferenceTarget(node: SdsReference, context: ReferenceInfo): Scope {
268+
private getScopeForReferenceTarget(node: SdsReference, context: ReferenceInfo): Scope {
253269
// Declarations in other files
254-
let currentScope = this.getGlobalScope('SdsDeclaration', context);
270+
let currentScope = this.getGlobalScope(SdsDeclaration, context);
255271

256272
// Declarations in this file
257273
currentScope = this.globalDeclarationsInSameFile(node, currentScope);
@@ -260,7 +276,10 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {
260276
currentScope = this.containingDeclarations(node, currentScope);
261277

262278
// Declarations in containing blocks
263-
return this.localDeclarations(node, currentScope);
279+
currentScope = this.localDeclarations(node, currentScope);
280+
281+
// Core declarations
282+
return this.coreDeclarations(SdsDeclaration, currentScope);
264283
}
265284

266285
private containingDeclarations(node: AstNode, outerScope: Scope): Scope {
@@ -465,4 +484,14 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {
465484

466485
return result;
467486
}
487+
488+
private coreDeclarations(referenceType: string, outerScope: Scope): Scope {
489+
const descriptions = this.coreDeclarationCache.get(referenceType, () =>
490+
this.packageManager.getDeclarationsInPackage('safeds.lang', {
491+
nodeType: referenceType,
492+
hideInternal: true,
493+
}),
494+
);
495+
return this.createScope(descriptions, outerScope);
496+
}
468497
}

packages/safe-ds-lang/src/language/validation/inheritance.ts

+13-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import { expandToStringWithNL, ValidationAcceptor } from 'langium';
1+
import { expandToStringWithNL, getContainerOfType, ValidationAcceptor } from 'langium';
22
import { isEmpty } from '../../helpers/collectionUtils.js';
3-
import { SdsClass, type SdsClassMember } from '../generated/ast.js';
4-
import { getParentTypes } from '../helpers/nodeProperties.js';
3+
import { isSdsClass, SdsClass, type SdsClassMember } from '../generated/ast.js';
4+
import { getParentTypes, getQualifiedName } from '../helpers/nodeProperties.js';
55
import { SafeDsServices } from '../safe-ds-module.js';
66
import { ClassType, UnknownType } from '../typing/model.js';
77

@@ -40,6 +40,11 @@ export const classMemberMustMatchOverriddenMemberAndShouldBeNeeded = (services:
4040
},
4141
);
4242
} else if (typeChecker.isAssignableTo(overriddenMemberType, ownMemberType)) {
43+
// Prevents the info from showing when editing the builtin files
44+
if (isInSafedsLangAnyClass(node)) {
45+
return;
46+
}
47+
4348
accept('info', 'Overriding member is identical to overridden member and can be removed.', {
4449
node,
4550
property: 'name',
@@ -49,6 +54,11 @@ export const classMemberMustMatchOverriddenMemberAndShouldBeNeeded = (services:
4954
};
5055
};
5156

57+
const isInSafedsLangAnyClass = (node: SdsClassMember): boolean => {
58+
const containingClass = getContainerOfType(node, isSdsClass);
59+
return isSdsClass(containingClass) && getQualifiedName(containingClass) === 'safeds.lang.Any';
60+
};
61+
5262
export const classMustOnlyInheritASingleClass = (services: SafeDsServices) => {
5363
const typeComputer = services.types.TypeComputer;
5464
const computeType = typeComputer.computeType.bind(typeComputer);

packages/safe-ds-lang/tests/language/scoping/scoping.test.ts

+65-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { AssertionError } from 'assert';
2-
import { DocumentValidator, LangiumDocument, Reference, URI } from 'langium';
2+
import { AstNode, DocumentValidator, getDocument, LangiumDocument, Reference, URI } from 'langium';
33
import { NodeFileSystem } from 'langium/node';
44
import { clearDocuments, isRangeEqual, validationHelper } from 'langium/test';
55
import { afterEach, beforeEach, describe, expect, it } from 'vitest';
@@ -8,8 +8,13 @@ import { createSafeDsServices } from '../../../src/language/index.js';
88
import { isLocationEqual, locationToString } from '../../helpers/location.js';
99
import { loadDocuments } from '../../helpers/testResources.js';
1010
import { createScopingTests, ExpectedReference } from './creator.js';
11+
import { getNodeOfType } from '../../helpers/nodeFinder.js';
12+
import { isSdsAnnotationCall, isSdsNamedType, isSdsReference } from '../../../src/language/generated/ast.js';
1113

1214
const services = createSafeDsServices(NodeFileSystem).SafeDs;
15+
const builtinAnnotations = services.builtins.Annotations;
16+
const builtinEnums = services.builtins.Enums;
17+
const builtinClasses = services.builtins.Classes;
1318

1419
describe('scoping', async () => {
1520
beforeEach(async () => {
@@ -69,15 +74,45 @@ describe('scoping', async () => {
6974
}
7075
});
7176

77+
it('should not replace core declarations (annotation call)', async () => {
78+
const code = `
79+
annotation PythonName(name: String)
80+
81+
@PythonName(name: String)
82+
segment mySegment() {}
83+
`;
84+
const annotationCall = await getNodeOfType(services, code, isSdsAnnotationCall);
85+
expectSameDocument(annotationCall.annotation?.ref, builtinAnnotations.PythonName);
86+
});
87+
88+
it('should not replace core declarations (named type)', async () => {
89+
const code = `
90+
class Any
91+
92+
segment mySegment(p: Any) {}
93+
`;
94+
const namedType = await getNodeOfType(services, code, isSdsNamedType);
95+
expectSameDocument(namedType.declaration?.ref, builtinClasses.Any);
96+
});
97+
98+
it('should not replace core declarations (reference)', async () => {
99+
const code = `
100+
enum AnnotationTarget
101+
102+
@Target([AnnotationTarget.Annotation])
103+
annotation MyAnnotation
104+
`;
105+
const reference = await getNodeOfType(services, code, isSdsReference);
106+
expectSameDocument(reference.target?.ref, builtinEnums.AnnotationTarget);
107+
});
108+
72109
it('should resolve members on literals', async () => {
73110
const code = `
74111
pipeline myPipeline {
75112
1.toString();
76113
}
77114
`;
78-
const { diagnostics } = await validationHelper(services)(code);
79-
const linkingError = diagnostics.filter((d) => d.data?.code === DocumentValidator.LinkingError);
80-
expect(linkingError).toStrictEqual([]);
115+
await expectNoLinkingErrors(code);
81116
});
82117

83118
it('should resolve members on literal types', async () => {
@@ -86,9 +121,7 @@ describe('scoping', async () => {
86121
p.toString();
87122
}
88123
`;
89-
const { diagnostics } = await validationHelper(services)(code);
90-
const linkingError = diagnostics.filter((d) => d.data?.code === DocumentValidator.LinkingError);
91-
expect(linkingError).toStrictEqual([]);
124+
await expectNoLinkingErrors(code);
92125
});
93126
});
94127

@@ -146,3 +179,28 @@ const findActualReference = (document: LangiumDocument, expectedReference: Expec
146179
}
147180
return actualReference;
148181
};
182+
183+
/**
184+
* Both nodes should be defined and in the same document or an `AssertionError` is thrown.
185+
*/
186+
const expectSameDocument = (node1: AstNode | undefined, node2: AstNode | undefined): void => {
187+
if (!node1) {
188+
throw new AssertionError({ message: `node1 is undefined.` });
189+
} else if (!node2) {
190+
throw new AssertionError({ message: `node2 is undefined.` });
191+
}
192+
193+
const document1 = getDocument(node1);
194+
const document2 = getDocument(node2);
195+
196+
expect(document1.uri.toString()).toStrictEqual(document2.uri.toString());
197+
};
198+
199+
/**
200+
* The given code should have no linking errors or an `AssertionError` is thrown.
201+
*/
202+
const expectNoLinkingErrors = async (code: string) => {
203+
const { diagnostics } = await validationHelper(services)(code);
204+
const linkingError = diagnostics.filter((d) => d.data?.code === DocumentValidator.LinkingError);
205+
expect(linkingError).toStrictEqual([]);
206+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package safeds.lang
2+
3+
class Any {
4+
// $TEST$ no info "Overriding member is identical to overridden member and can be removed."
5+
fun »toString«() -> s: String
6+
}

0 commit comments

Comments
 (0)