langium
Version:
A language engineering tool for the Language Server Protocol
411 lines (372 loc) • 18.4 kB
text/typescript
/******************************************************************************
* Copyright 2021 TypeFox GmbH
* This program and the accompanying materials are made available under the
* terms of the MIT License, which is available in the project root.
******************************************************************************/
import type { LangiumCoreServices } from '../services.js';
import type { AstNode, AstNodeDescription, AstReflection, CstNode, LinkingError, MultiReference, MultiReferenceItem, Reference, ReferenceInfo } from '../syntax-tree.js';
import type { AstNodeLocator } from '../workspace/ast-node-locator.js';
import type { LangiumDocument, LangiumDocuments } from '../workspace/documents.js';
import type { ScopeProvider } from './scope-provider.js';
import { CancellationToken } from '../utils/cancellation.js';
import { isAstNode, isAstNodeDescription, isLinkingError } from '../syntax-tree.js';
import { findRootNode, streamAst, streamReferences } from '../utils/ast-utils.js';
import { interruptAndCheck } from '../utils/promise-utils.js';
import { DocumentState } from '../workspace/documents.js';
import type { LangiumProfiler } from '../workspace/profiler.js';
/**
* Language-specific service for resolving cross-references in the AST.
*/
export interface Linker {
/**
* Links all cross-references within the specified document. The default implementation loads only target
* elements from documents that are present in the `LangiumDocuments` service. The linked references are
* stored in the document's `references` property.
*
* @param document A LangiumDocument that shall be linked.
* @param cancelToken A token for cancelling the operation.
*
* @throws `OperationCancelled` if a cancellation event is detected
*/
link(document: LangiumDocument, cancelToken?: CancellationToken): Promise<void>;
/**
* Unlinks all references within the specified document and removes them from the list of `references`.
*
* @param document A LangiumDocument that shall be unlinked.
*/
unlink(document: LangiumDocument): void;
/**
* Determines a candidate AST node description for linking the given reference.
*
* @param refInfo Information about the reference.
*/
getCandidate(refInfo: ReferenceInfo): AstNodeDescription | LinkingError;
/**
* Determines a candidate AST node description for linking the given reference.
*
* @param node The AST node containing the reference.
* @param refId The reference identifier used to build a scope.
* @param reference The actual reference to resolve.
*/
getCandidates(refInfo: ReferenceInfo): AstNodeDescription[] | LinkingError;
/**
* Creates a cross reference node being aware of its containing AstNode, the corresponding CstNode,
* the cross reference text denoting the target AstNode being already extracted of the document text,
* as well as the unique cross reference identifier.
*
* Default behavior:
* - The returned Reference's 'ref' property pointing to the target AstNode is populated lazily on its
* first visit.
* - If the target AstNode cannot be resolved on the first visit, an error indicator will be installed
* and further resolution attempts will *not* be performed.
*
* @param node The containing AST node
* @param property The AST node property being referenced
* @param refNode The corresponding CST node
* @param refText The cross reference text denoting the target AstNode
* @returns the desired Reference node, whose behavior wrt. resolving the cross reference is implementation specific.
*/
buildReference(node: AstNode, property: string, refNode: CstNode | undefined, refText: string): Reference;
buildMultiReference(node: AstNode, property: string, refNode: CstNode | undefined, refText: string): MultiReference;
}
export const RefResolving = Symbol('RefResolving');
export interface DefaultReference extends Reference {
_ref?: AstNode | LinkingError | typeof RefResolving;
_nodeDescription?: AstNodeDescription;
}
export interface DefaultMultiReference extends MultiReference {
_items: MultiReferenceItem[] | typeof RefResolving | undefined;
_linkingError?: LinkingError;
}
interface ReferenceResolver {
resolveReference(reference: DefaultReference, node: AstNode, property: string): AstNode | undefined;
resolveMultiReference(reference: DefaultMultiReference, node: AstNode, property: string): MultiReferenceItem[];
}
class DefaultReferenceImpl implements DefaultReference {
readonly $refNode: CstNode | undefined;
readonly $refText: string;
_ref: AstNode | LinkingError | typeof RefResolving | undefined = undefined;
_nodeDescription?: AstNodeDescription;
constructor(
private readonly resolver: ReferenceResolver,
private readonly node: AstNode,
private readonly property: string,
refNode: CstNode | undefined,
refText: string
) {
this.$refNode = refNode;
this.$refText = refText;
}
get ref(): AstNode | undefined {
return this.resolver.resolveReference(this, this.node, this.property);
}
get $nodeDescription(): AstNodeDescription | undefined {
return this._nodeDescription;
}
get error(): LinkingError | undefined {
return isLinkingError(this._ref) ? this._ref : undefined;
}
}
class DefaultMultiReferenceImpl implements DefaultMultiReference {
readonly $refNode: CstNode | undefined;
readonly $refText: string;
_items: MultiReferenceItem[] | typeof RefResolving | undefined = undefined;
_linkingError?: LinkingError;
constructor(
private readonly resolver: ReferenceResolver,
private readonly node: AstNode,
private readonly property: string,
refNode: CstNode | undefined,
refText: string
) {
this.$refNode = refNode;
this.$refText = refText;
}
get items(): MultiReferenceItem[] {
return this.resolver.resolveMultiReference(this, this.node, this.property);
}
get error(): LinkingError | undefined {
return this._linkingError;
}
}
export class DefaultLinker implements Linker {
protected readonly reflection: AstReflection;
protected readonly scopeProvider: ScopeProvider;
protected readonly astNodeLocator: AstNodeLocator;
protected readonly langiumDocuments: () => LangiumDocuments;
protected readonly profiler: LangiumProfiler | undefined;
protected readonly languageId: string;
protected readonly referenceResolver: ReferenceResolver;
constructor(services: LangiumCoreServices) {
this.reflection = services.shared.AstReflection;
this.langiumDocuments = () => services.shared.workspace.LangiumDocuments;
this.scopeProvider = services.references.ScopeProvider;
this.astNodeLocator = services.workspace.AstNodeLocator;
this.profiler = services.shared.profilers.LangiumProfiler;
this.languageId = services.LanguageMetaData.languageId;
this.referenceResolver = {
resolveReference: this.resolveReference.bind(this),
resolveMultiReference: this.resolveMultiReference.bind(this)
};
}
async link(document: LangiumDocument, cancelToken = CancellationToken.None): Promise<void> {
if (this.profiler?.isActive('linking')) {
const task = this.profiler.createTask('linking', this.languageId);
task.start();
try {
for (const node of streamAst(document.parseResult.value)) {
await interruptAndCheck(cancelToken);
streamReferences(node).forEach(ref => {
const name = `${node.$type}:${ref.property}`;
task.startSubTask(name);
try {
this.doLink(ref, document);
} finally {
task.stopSubTask(name);
}
});
}
} finally {
task.stop();
}
}
else {
for (const node of streamAst(document.parseResult.value)) {
await interruptAndCheck(cancelToken);
streamReferences(node).forEach(ref => this.doLink(ref, document));
}
}
}
protected doLink(refInfo: ReferenceInfo, document: LangiumDocument): void {
const ref = refInfo.reference as DefaultReference | DefaultMultiReference;
// The reference may already have been resolved lazily by accessing its `ref` property.
if ('_ref' in ref && ref._ref === undefined) {
ref._ref = RefResolving;
try {
const description = this.getCandidate(refInfo);
if (isLinkingError(description)) {
ref._ref = description;
} else {
ref._nodeDescription = description;
const linkedNode = this.loadAstNode(description);
ref._ref = linkedNode ?? this.createLinkingError(refInfo, description);
}
} catch (err) {
console.error(`An error occurred while resolving reference to '${ref.$refText}':`, err);
const errorMessage = (err as Error).message ?? String(err);
ref._ref = {
info: refInfo,
message: `An error occurred while resolving reference to '${ref.$refText}': ${errorMessage}`
};
}
document.references.push(ref);
} else if ('_items' in ref && ref._items === undefined) {
ref._items = RefResolving;
try {
const descriptions = this.getCandidates(refInfo);
const items: MultiReferenceItem[] = [];
if (isLinkingError(descriptions)) {
ref._linkingError = descriptions;
} else {
for (const description of descriptions) {
const linkedNode = this.loadAstNode(description);
if (linkedNode) {
items.push({ ref: linkedNode, $nodeDescription: description });
}
}
}
ref._items = items;
} catch (err) {
ref._linkingError = {
info: refInfo,
message: `An error occurred while resolving reference to '${ref.$refText}': ${err}`
};
ref._items = [];
}
document.references.push(ref);
}
}
unlink(document: LangiumDocument): void {
for (const ref of document.references) {
if ('_ref' in ref) {
(ref as DefaultReference)._ref = undefined;
delete (ref as DefaultReference)._nodeDescription;
} else if ('_items' in ref) {
(ref as DefaultMultiReference)._items = undefined;
delete (ref as DefaultMultiReference)._linkingError;
}
}
document.references = [];
}
getCandidate(refInfo: ReferenceInfo): AstNodeDescription | LinkingError {
const scope = this.scopeProvider.getScope(refInfo);
const description = scope.getElement(refInfo.reference.$refText);
return description ?? this.createLinkingError(refInfo);
}
getCandidates(refInfo: ReferenceInfo): AstNodeDescription[] | LinkingError {
const scope = this.scopeProvider.getScope(refInfo);
const descriptions = scope.getElements(refInfo.reference.$refText).distinct(desc => `${desc.documentUri}#${desc.path}`).toArray();
return descriptions.length > 0 ? descriptions : this.createLinkingError(refInfo);
}
buildReference(node: AstNode, property: string, refNode: CstNode | undefined, refText: string): Reference {
// See behavior description in doc of Linker, update that on changes in here.
return new DefaultReferenceImpl(this.referenceResolver, node, property, refNode, refText);
}
buildMultiReference(node: AstNode, property: string, refNode: CstNode | undefined, refText: string): MultiReference {
// See behavior description in doc of Linker, update that on changes in here.
return new DefaultMultiReferenceImpl(this.referenceResolver, node, property, refNode, refText);
}
protected resolveReference(reference: DefaultReference, node: AstNode, property: string): AstNode | undefined {
if (isAstNode(reference._ref)) {
// Most frequent case: the target is already resolved.
return reference._ref;
} else if (isAstNodeDescription(reference._nodeDescription)) {
// A candidate has been found before, but it is not loaded yet.
const linkedNode = this.loadAstNode(reference._nodeDescription);
reference._ref = linkedNode ??
this.createLinkingError({ reference, container: node, property }, reference._nodeDescription);
} else if (reference._ref === undefined) {
// The reference has not been linked yet, so do that now.
reference._ref = RefResolving;
const document = findRootNode(node).$document;
const refData = this.getLinkedNode({ reference, container: node, property });
if (refData.error && document && document.state < DocumentState.ComputedScopes) {
// Document scope is not ready, don't set `reference._ref` so linker can retry later.
return reference._ref = undefined;
}
reference._ref = refData.node ?? refData.error;
reference._nodeDescription = refData.descr;
document?.references.push(reference);
} else if (reference._ref === RefResolving) {
this.throwCyclicReferenceError(node, property, reference.$refText);
}
return isAstNode(reference._ref) ? reference._ref : undefined;
}
protected resolveMultiReference(reference: DefaultMultiReference, node: AstNode, property: string): MultiReferenceItem[] {
if (Array.isArray(reference._items)) {
return reference._items;
} else if (reference._items === undefined) {
reference._items = RefResolving;
const document = findRootNode(node).$document;
const descriptions = this.getCandidates({
reference,
container: node,
property
});
const items: MultiReferenceItem[] = [];
if (isLinkingError(descriptions)) {
reference._linkingError = descriptions;
} else {
for (const description of descriptions) {
const linkedNode = this.loadAstNode(description);
if (linkedNode) {
items.push({ ref: linkedNode, $nodeDescription: description });
}
}
if (items.length === 0) {
reference._linkingError = this.createLinkingError({ reference, container: node, property });
}
}
reference._items = items;
document?.references.push(reference);
} else if (reference._items === RefResolving) {
this.throwCyclicReferenceError(node, property, reference.$refText);
}
return Array.isArray(reference._items) ? reference._items : [];
}
protected throwCyclicReferenceError(node: AstNode, property: string, refText: string): never {
throw new Error(`Cyclic reference resolution detected: ${this.astNodeLocator.getAstNodePath(node)}/${property} (symbol '${refText}')`);
}
protected getLinkedNode(refInfo: ReferenceInfo): { node?: AstNode, descr?: AstNodeDescription, error?: LinkingError } {
try {
const description = this.getCandidate(refInfo);
if (isLinkingError(description)) {
return { error: description };
}
const linkedNode = this.loadAstNode(description);
if (linkedNode) {
return { node: linkedNode, descr: description };
}
else {
return {
descr: description,
error:
this.createLinkingError(refInfo, description)
};
}
} catch (err) {
console.error(`An error occurred while resolving reference to '${refInfo.reference.$refText}':`, err);
const errorMessage = (err as Error).message ?? String(err);
return {
error: {
info: refInfo,
message: `An error occurred while resolving reference to '${refInfo.reference.$refText}': ${errorMessage}`
}
};
}
}
protected loadAstNode(nodeDescription: AstNodeDescription): AstNode | undefined {
if (nodeDescription.node) {
return nodeDescription.node;
}
const doc = this.langiumDocuments().getDocument(nodeDescription.documentUri);
if (!doc) {
return undefined;
}
return this.astNodeLocator.getAstNode(doc.parseResult.value, nodeDescription.path);
}
protected createLinkingError(refInfo: ReferenceInfo, targetDescription?: AstNodeDescription): LinkingError {
// Check whether the document is sufficiently processed by the DocumentBuilder. If not, this is a hint for a bug
// in the language implementation.
const document = findRootNode(refInfo.container).$document;
if (document && document.state < DocumentState.ComputedScopes) {
console.warn(`Attempted reference resolution before document reached ComputedScopes state (${document.uri}).`);
}
const referenceType = this.reflection.getReferenceType(refInfo);
return {
info: refInfo,
message: `Could not resolve reference to ${referenceType} named '${refInfo.reference.$refText}'.`,
targetDescription
};
}
}