Skip to content

Commit

Permalink
feat: resolve references to declarations in other files (#586)
Browse files Browse the repository at this point in the history
Closes partially #540

### Summary of Changes

References to declarations in other files now get resolved properly if
they
1. are explicitly imported,
2. reside in the same package,
3. are part of the builtin library.

---------

Co-authored-by: megalinter-bot <129584137+megalinter-bot@users.noreply.github.com>
  • Loading branch information
lars-reimann and megalinter-bot authored Sep 30, 2023
1 parent 3da8dd0 commit 6b30de5
Show file tree
Hide file tree
Showing 147 changed files with 1,524 additions and 115 deletions.
13 changes: 12 additions & 1 deletion src/language/helpers/shortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
isSdsAssignment,
isSdsBlockLambdaResult,
isSdsDeclaration,
isSdsModule,
isSdsPlaceholder,
SdsAnnotatedObject,
SdsAnnotationCall,
Expand All @@ -14,8 +15,10 @@ import {
SdsClassMember,
SdsEnum,
SdsEnumVariant,
SdsImport,
SdsLiteral,
SdsLiteralType,
SdsModule,
SdsParameter,
SdsParameterList,
SdsPlaceholder,
Expand All @@ -27,7 +30,7 @@ import {
SdsTypeParameter,
SdsTypeParameterList,
} from '../generated/ast.js';
import { stream } from 'langium';
import { AstNode, getContainerOfType, stream } from 'langium';

export const annotationCallsOrEmpty = function (node: SdsAnnotatedObject | undefined): SdsAnnotationCall[] {
if (!node) {
Expand Down Expand Up @@ -70,6 +73,14 @@ export const enumVariantsOrEmpty = function (node: SdsEnum | undefined): SdsEnum
return node?.body?.variants ?? [];
};

export const importsOrEmpty = function (node: SdsModule | undefined): SdsImport[] {
return node?.imports ?? [];
};

export const packageNameOrNull = function (node: AstNode | undefined): string | null {
return getContainerOfType(node, isSdsModule)?.name ?? null;
};

export const parametersOrEmpty = function (node: SdsParameterList | undefined): SdsParameter[] {
return node?.parameters ?? [];
};
Expand Down
19 changes: 18 additions & 1 deletion src/language/scoping/safe-ds-scope-computation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import {
isSdsEnumVariant,
isSdsFunction,
isSdsModule,
isSdsPipeline,
isSdsSegment,
isSdsTypeParameter,
isSdsTypeParameterList,
SdsClass,
Expand All @@ -22,7 +24,22 @@ import {
} from '../generated/ast.js';

export class SafeDsScopeComputation extends DefaultScopeComputation {
override processNode(node: AstNode, document: LangiumDocument, scopes: PrecomputedScopes): void {
protected override exportNode(node: AstNode, exports: AstNodeDescription[], document: LangiumDocument): void {
// Pipelines and private segments cannot be referenced from other documents
if (isSdsPipeline(node) || (isSdsSegment(node) && node.visibility === 'private')) {
return;
}

// Modules that don't state their package don't export anything
const containingModule = getContainerOfType(node, isSdsModule);
if (!containingModule || !containingModule.name) {
return;
}

super.exportNode(node, exports, document);
}

protected override processNode(node: AstNode, document: LangiumDocument, scopes: PrecomputedScopes): void {
if (isSdsClass(node)) {
this.processSdsClass(node, document, scopes);
} else if (isSdsEnum(node)) {
Expand Down
221 changes: 181 additions & 40 deletions src/language/scoping/safe-ds-scope-provider.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,15 @@
import {
AstNode,
AstNodeDescription,
AstNodeDescriptionProvider,
AstNodeLocator,
DefaultScopeProvider,
EMPTY_SCOPE,
getContainerOfType,
getDocument,
LangiumDocuments,
LangiumServices,
MultiMap,
ReferenceInfo,
Scope,
} from 'langium';
Expand All @@ -12,6 +18,7 @@ import {
isSdsBlock,
isSdsCallable,
isSdsClass,
isSdsDeclaration,
isSdsEnum,
isSdsEnumVariant,
isSdsLambda,
Expand All @@ -28,6 +35,7 @@ import {
isSdsYield,
SdsDeclaration,
SdsExpression,
SdsImport,
SdsMemberAccess,
SdsMemberType,
SdsNamedTypeDeclaration,
Expand All @@ -42,15 +50,29 @@ import {
assigneesOrEmpty,
classMembersOrEmpty,
enumVariantsOrEmpty,
importsOrEmpty,
packageNameOrNull,
parametersOrEmpty,
resultsOrEmpty,
statementsOrEmpty,
typeParametersOrEmpty,
} from '../helpers/shortcuts.js';
import { isContainedIn } from '../helpers/ast.js';
import { isStatic } from '../helpers/checks.js';
import { isStatic, isWildcardImport } from '../helpers/checks.js';

export class SafeDsScopeProvider extends DefaultScopeProvider {
readonly documents: LangiumDocuments;
readonly astNodeDescriptionProvider: AstNodeDescriptionProvider;
readonly astNodeLocator: AstNodeLocator;

constructor(services: LangiumServices) {
super(services);

this.documents = services.shared.workspace.LangiumDocuments;
this.astNodeDescriptionProvider = services.workspace.AstNodeDescriptionProvider;
this.astNodeLocator = services.workspace.AstNodeLocator;
}

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

Expand All @@ -64,7 +86,7 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {
if (isSdsMemberAccess(node.$container) && node.$containerProperty === 'member') {
return this.getScopeForMemberAccessMember(node.$container);
} else {
return this.getScopeForDirectReferenceTarget(node);
return this.getScopeForDirectReferenceTarget(node, context);
}
} else if (isSdsTypeArgument(node) && context.property === 'typeParameter') {
return this.getScopeForTypeArgumentTypeParameter(node);
Expand Down Expand Up @@ -174,19 +196,12 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {
}
}

private getScopeForDirectReferenceTarget(node: SdsReference): Scope {
// val resource = context.eResource()
// val packageName = context.containingCompilationUnitOrNull()?.qualifiedNameOrNull()
//
// // Declarations in other files
// var result: IScope = FilteringScope(
// super.delegateGetScope(context, SafeDSPackage.Literals.SDS_REFERENCE__DECLARATION),
// ) {
// it.isReferencableExternalDeclaration(resource, packageName)
// }
private getScopeForDirectReferenceTarget(node: SdsReference, context: ReferenceInfo): Scope {
// Declarations in other files
let currentScope = this.getGlobalScope('SdsDeclaration', context);

// Declarations in this file
const currentScope = this.globalDeclarationsInSameFile(node, EMPTY_SCOPE);
currentScope = this.globalDeclarationsInSameFile(node, currentScope);

// // Declarations in containing classes
// context.containingClassOrNull()?.let {
Expand All @@ -208,33 +223,6 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {
// }
// }

// /**
// * Removes declarations in this [Resource], [SdsAnnotation]s, and internal [SdsStep]s located in other
// * [SdsCompilationUnit]s.
// */
// private fun IEObjectDescription?.isReferencableExternalDeclaration(
// fromResource: Resource,
// fromPackageWithQualifiedName: QualifiedName?,
// ): Boolean {
// // Resolution failed in delegate scope
// if (this == null) return false
//
// val obj = this.eObjectOrProxy
//
// // Local declarations are added later using custom scoping rules
// if (obj.eResource() == fromResource) return false
//
// // Annotations cannot be referenced
// if (obj is SdsAnnotation) return false
//
// // Internal steps in another package cannot be referenced
// return !(
// obj is SdsStep &&
// obj.visibility() == SdsVisibility.Internal &&
// obj.containingCompilationUnitOrNull()?.qualifiedNameOrNull() != fromPackageWithQualifiedName
// )
// }

private globalDeclarationsInSameFile(node: AstNode, outerScope: Scope): Scope {
const module = getContainerOfType(node, isSdsModule);
if (!module) {
Expand Down Expand Up @@ -324,4 +312,157 @@ export class SafeDsScopeProvider extends DefaultScopeProvider {

return this.createScopeForNodes(resultsOrEmpty(containingSegment.resultList));
}

protected override getGlobalScope(referenceType: string, context: ReferenceInfo): Scope {
const node = context.container;
const key = `${getDocument(node).uri}~${referenceType}`;
return this.globalScopeCache.get(key, () => this.getGlobalScopeForNode(referenceType, node));
}

private getGlobalScopeForNode(referenceType: string, node: AstNode): Scope {
// Gather information about the containing module
const containingModule = getContainerOfType(node, isSdsModule);
const ownUri = getDocument(node).uri.toString();
const ownPackageName = containingModule?.name;

// Data structures to collect reachable declarations
const explicitlyImportedDeclarations = new ImportedDeclarations(importsOrEmpty(containingModule));
const declarationsInSamePackage: AstNodeDescription[] = [];
const builtinDeclarations: AstNodeDescription[] = [];

// Loop over all declarations in the index
const candidates = this.indexManager.allElements(referenceType);
for (const candidate of candidates) {
// Skip declarations in the same file
const candidateUri = candidate.documentUri.toString();
if (candidateUri === ownUri) {
continue;
}

// Skip declarations that cannot be found and modules
const candidateNode = this.loadAstNode(candidate);
if (!candidateNode || isSdsModule(candidateNode)) {
continue;
}

// Skip declarations in a module without a package name
const candidatePackageName = packageNameOrNull(candidateNode);
if (candidatePackageName === null) {
/* c8 ignore next */
continue;
}

// Handle internal segments, which are only reachable in the same package
if (isSdsSegment(candidateNode) && candidateNode.visibility === 'internal') {
if (candidatePackageName === ownPackageName) {
declarationsInSamePackage.push(candidate);
}
continue;
}

// Handle explicitly imported declarations
explicitlyImportedDeclarations.addIfImported(candidate, candidateNode, candidatePackageName);

// Handle other declarations in the same package
if (candidatePackageName === ownPackageName) {
declarationsInSamePackage.push(candidate);
continue;
}

// Handle builtin declarations
if (this.isBuiltinPackage(candidatePackageName)) {
builtinDeclarations.push(candidate);
}
}

// Order of precedence:
// Highest: Explicitly imported declarations
// Middle: Declarations in the same package
// Lowest: Builtin declarations
return this.createScope(
explicitlyImportedDeclarations.getDescriptions(),
this.createScope(declarationsInSamePackage, this.createScope(builtinDeclarations, EMPTY_SCOPE)),
);
}

private loadAstNode(nodeDescription: AstNodeDescription): AstNode | undefined {
if (nodeDescription.node) {
/* c8 ignore next 2 */
return nodeDescription.node;
}
const document = this.documents.getOrCreateDocument(nodeDescription.documentUri);
return this.astNodeLocator.getAstNode(document.parseResult.value, nodeDescription.path);
}

private isBuiltinPackage(packageName: string) {
return packageName.startsWith('safeds');
}
}

/**
* Collects descriptions of imported declarations in the same order as the imports.
*/
class ImportedDeclarations {
private readonly descriptionsByImport = new MultiMap<SdsImport, AstNodeDescription>();

constructor(imports: SdsImport[]) {
// Remember the imports and their order
for (const imp of imports) {
this.descriptionsByImport.addAll(imp, []);
}
}

/**
* Adds the node if it is imported.
*
* @param description The description of the node to add.
* @param node The node to add.
* @param packageName The package name of the containing module.
*/
addIfImported(description: AstNodeDescription, node: AstNode, packageName: string): void {
if (!isSdsDeclaration(node)) {
/* c8 ignore next 2 */
return;
}

const firstMatchingImport = this.findFirstMatchingImport(node, packageName);
if (!firstMatchingImport) {
return;
}

const updatedDescription = this.updateDescription(description, firstMatchingImport);
this.descriptionsByImport.add(firstMatchingImport, updatedDescription);
}

private findFirstMatchingImport(node: SdsDeclaration, packageName: string): SdsImport | undefined {
return this.descriptionsByImport.keys().find((imp) => this.importMatches(imp, node, packageName));
}

private importMatches(imp: SdsImport, node: SdsDeclaration, packageName: string): boolean {
if (isWildcardImport(imp)) {
const importedPackageName = imp.importedNamespace.replaceAll(/\.?\*$/gu, '');
return importedPackageName === packageName;
} else {
const segments = imp.importedNamespace.split('.');
const importedPackageName = segments.slice(0, segments.length - 1).join('.');
const importedDeclarationName = segments[segments.length - 1];
return importedPackageName === packageName && importedDeclarationName === node.name;
}
}

private updateDescription(description: AstNodeDescription, firstMatchingImport: SdsImport): AstNodeDescription {
if (isWildcardImport(firstMatchingImport) || !firstMatchingImport.alias) {
return description;
} else {
// Declaration is available under an alias
return { ...description, name: firstMatchingImport.alias.name };
}
}

/**
* Returns descriptions of all imported declarations in the order of the imports.
*/
getDescriptions(): AstNodeDescription[] {
return this.descriptionsByImport.values().toArray();
}
}
2 changes: 2 additions & 0 deletions tests/language/scoping/creator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { URI } from 'vscode-uri';
import { getSyntaxErrors, SyntaxErrorsInCodeError } from '../../helpers/diagnostics.js';
import { EmptyFileSystem } from 'langium';
import { createSafeDsServices } from '../../../src/language/safe-ds-module.js';
import { clearDocuments } from 'langium/test';

const services = createSafeDsServices(EmptyFileSystem).SafeDs;
const root = 'scoping';
Expand Down Expand Up @@ -46,6 +47,7 @@ const createScopingTest = async (
new SyntaxErrorsInCodeError(syntaxErrors),
);
}
await clearDocuments(services);

const checksResult = findTestChecks(code, uri, { failIfFewerRangesThanComments: true });

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package test.scoping.annotationCalls.onAnnotation
package tests.scoping.annotationCalls.onAnnotation

// $TEST$ target before
annotation »Before«
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package test.scoping.annotationCalls.onAttribute
package tests.scoping.annotationCalls.onAttribute

// $TEST$ target before
annotation »Before«
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package test.scoping.annotationCalls.onClass
package tests.scoping.annotationCalls.onClass

// $TEST$ target before
annotation »Before«
Expand Down
Loading

0 comments on commit 6b30de5

Please sign in to comment.