UNPKG

langium

Version:

A language engineering tool for the Language Server Protocol

384 lines (340 loc) 14.7 kB
/****************************************************************************** * 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 { TokenType } from 'chevrotain'; import type { URI } from './utils/uri-utils.js'; import type { AbstractElement } from './languages/generated/ast.js'; import type { DocumentSegment, LangiumDocument } from './workspace/documents.js'; /** * A node in the Abstract Syntax Tree (AST). */ export interface AstNode { /** Every AST node has a type corresponding to what was specified in the grammar declaration. */ readonly $type: string; /** The container node in the AST; every node except the root node has a container. */ readonly $container?: AstNode; /** The property of the `$container` node that contains this node. This is either a direct reference or an array. */ readonly $containerProperty?: string; /** In case `$containerProperty` is an array, the array index is stored here. */ readonly $containerIndex?: number; /** The Concrete Syntax Tree (CST) node of the text range from which this node was parsed. */ readonly $cstNode?: CstNode; /** The document containing the AST; only the root node has a direct reference to the document. */ readonly $document?: LangiumDocument; } export function isAstNode(obj: unknown): obj is AstNode { return typeof obj === 'object' && obj !== null && typeof (obj as AstNode).$type === 'string'; } export interface GenericAstNode extends AstNode { [key: string]: unknown } type SpecificNodeProperties<N extends AstNode> = keyof Omit<N, keyof AstNode | number | symbol>; /** * The property names of a given AST node type. */ export type Properties<N extends AstNode> = SpecificNodeProperties<N> extends never ? string : SpecificNodeProperties<N> /** * A cross-reference in the AST. Cross-references may or may not be successfully resolved. */ export interface Reference<T extends AstNode = AstNode> { /** * The target AST node of this reference. Accessing this property may trigger cross-reference * resolution by the `Linker` in case it has not been done yet. If the reference cannot be resolved, * the value is `undefined`. */ readonly ref: T | undefined; /** If any problem occurred while resolving the reference, it is described by this property. */ readonly error?: LinkingError; /** The CST node from which the reference was parsed */ readonly $refNode?: CstNode; /** The actual text used to look up in the surrounding scope */ readonly $refText: string; /** The node description for the AstNode returned by `ref` */ readonly $nodeDescription?: AstNodeDescription; } export interface MultiReference<T extends AstNode = AstNode> { /** The CST node from which the reference was parsed */ readonly $refNode?: CstNode; /** The actual text used to look up in the surrounding scope */ readonly $refText: string; /** * The resolved references. Accessing this property may trigger cross-reference * resolution by the `Linker` in case it has not been done yet. * If no references can be found, the array is empty (but not `undefined`) * and the `error` property is set. */ readonly items: Array<MultiReferenceItem<T>>; /** If any problem occurred while resolving the reference, it is described by this property. */ readonly error?: LinkingError; } /** * Represents a single resolved reference of a {@link MultiReference} instance. */ export interface MultiReferenceItem<T extends AstNode = AstNode> { /** The node description for the AstNode returned by `ref` */ readonly $nodeDescription?: AstNodeDescription; /** * The target AST node of this reference. */ readonly ref: T; } export function isReference(obj: unknown): obj is Reference { return typeof obj === 'object' && obj !== null && typeof (obj as Reference).$refText === 'string' && 'ref' in obj; } export function isMultiReference(obj: unknown): obj is MultiReference { return typeof obj === 'object' && obj !== null && typeof (obj as Reference).$refText === 'string' && 'items' in obj; } export type ResolvedReference<T extends AstNode = AstNode> = Reference<T> & { readonly ref: T; } /** * A description of an AST node is used when constructing scopes and looking up cross-reference targets. */ export interface AstNodeDescription { /** The target node; should be present only for local references (linking to the same document). */ node?: AstNode; /** * The document segment that represents the range of the name of the AST node. */ nameSegment?: DocumentSegment; /** * The document segment that represents the full range of the AST node. */ selectionSegment?: DocumentSegment; /** `$type` property value of the AST node */ type: string; /** Name of the AST node; this is usually determined by the `NameProvider` service. */ name: string; /** URI to the document containing the AST node */ documentUri: URI; /** Navigation path inside the document */ path: string; } export function isAstNodeDescription(obj: unknown): obj is AstNodeDescription { return typeof obj === 'object' && obj !== null && typeof (obj as AstNodeDescription).name === 'string' && typeof (obj as AstNodeDescription).type === 'string' && typeof (obj as AstNodeDescription).path === 'string'; } /** * Information about a cross-reference. This is used when traversing references in an AST or to describe * unresolved references. */ export interface ReferenceInfo { reference: Reference | MultiReference container: AstNode property: string index?: number } /** * Used to collect information when the `Linker` service fails to resolve a cross-reference. */ export interface LinkingError { message: string; info: ReferenceInfo; targetDescription?: AstNodeDescription; } export function isLinkingError(obj: unknown): obj is LinkingError { return typeof obj === 'object' && obj !== null && typeof (obj as LinkingError).info === 'object' && typeof (obj as LinkingError).message === 'string'; } /** * Service used for generic access to the structure of the AST. This service is shared between * all involved languages, so it operates on the superset of types of these languages. */ export interface AstReflection { readonly types: AstMetaData getAllTypes(): string[] getReferenceType(refInfo: ReferenceInfo): string getTypeMetaData(type: string): TypeMetaData isInstance(node: unknown, type: string): boolean isSubtype(subtype: string, supertype: string): boolean getAllSubTypes(type: string): string[] } /** * An abstract implementation of the {@link AstReflection} interface. * Serves to cache subtype computation results to improve performance throughout different parts of Langium. */ export abstract class AbstractAstReflection implements AstReflection { readonly types: AstMetaData; protected subtypes: Record<string, Record<string, boolean | undefined>> = {}; protected allSubtypes: Record<string, string[] | undefined> = {}; getAllTypes(): string[] { return Object.keys(this.types); } getReferenceType(refInfo: ReferenceInfo): string { const metaData = this.types[refInfo.container.$type]; if (!metaData) { throw new Error(`Type ${refInfo.container.$type || 'undefined'} not found.`); } const referenceType = metaData.properties[refInfo.property]?.referenceType; if (!referenceType) { throw new Error(`Property ${refInfo.property || 'undefined'} of type ${refInfo.container.$type} is not a reference.`); } return referenceType; } getTypeMetaData(type: string): TypeMetaData { const result = this.types[type]; if (!result) { return { name: type, properties: {}, superTypes: [] }; } return result; } isInstance(node: unknown, type: string): boolean { return isAstNode(node) && this.isSubtype(node.$type, type); } isSubtype(subtype: string, supertype: string): boolean { if (subtype === supertype) { return true; } let nested = this.subtypes[subtype]; if (!nested) { nested = this.subtypes[subtype] = {}; } const existing = nested[supertype]; if (existing !== undefined) { return existing; } else { const metaData = this.types[subtype]; const result = metaData ? metaData.superTypes.some(s => this.isSubtype(s, supertype)) : false; nested[supertype] = result; return result; } } getAllSubTypes(type: string): string[] { const existing = this.allSubtypes[type]; if (existing) { return existing; } else { const allTypes = this.getAllTypes(); const types: string[] = []; for (const possibleSubType of allTypes) { if (this.isSubtype(possibleSubType, type)) { types.push(possibleSubType); } } this.allSubtypes[type] = types; return types; } } } /** * A map of all AST node types and their meta data. */ export interface AstMetaData { [type: string]: TypeMetaData } /** * Represents runtime meta data about an AST node type. */ export interface TypeMetaData { /** The name of this AST node type. Corresponds to the `AstNode.$type` value. */ name: string /** A list of properties with their relevant meta data. */ properties: { [name: string]: PropertyMetaData } /** The super types of this AST node type. */ superTypes: string[] } /** * Describes the meta data of a property of an AST node. */ export interface PropertyMetaData { /** The name of this property. */ name: string /** * Indicates that the property is mandatory in the AST node. * For example, if an AST node contains an array, but no elements of this array have been parsed, * we still expect an empty array instead of `undefined`. */ defaultValue?: PropertyType /** * If the property is a reference, this is the type of the reference target. */ referenceType?: string } /** * Represents a default value for an AST property. */ export type PropertyType = number | string | boolean | PropertyType[]; /** * A node in the Concrete Syntax Tree (CST). */ export interface CstNode extends DocumentSegment { /** The container node in the CST */ readonly container?: CompositeCstNode; /** The actual text */ readonly text: string; /** The root CST node */ readonly root: RootCstNode; /** The grammar element from which this node was parsed */ readonly grammarSource?: AbstractElement; /** The AST node created from this CST node */ readonly astNode: AstNode; /** Whether the token is hidden, i.e. not explicitly part of the containing grammar rule */ readonly hidden: boolean; } /** * A composite CST node contains other nodes, but no directly associated token. */ export interface CompositeCstNode extends CstNode { readonly content: CstNode[]; } export function isCompositeCstNode(node: unknown): node is CompositeCstNode { return typeof node === 'object' && node !== null && Array.isArray((node as CompositeCstNode).content); } /** * A leaf CST node corresponds to a token in the input token stream. */ export interface LeafCstNode extends CstNode { readonly tokenType: TokenType; } export function isLeafCstNode(node: unknown): node is LeafCstNode { return typeof node === 'object' && node !== null && typeof (node as LeafCstNode).tokenType === 'object'; } export interface RootCstNode extends CompositeCstNode { readonly fullText: string } export function isRootCstNode(node: unknown): node is RootCstNode { return isCompositeCstNode(node) && typeof (node as RootCstNode).fullText === 'string'; } /** * Describes a union type including only names(!) of properties of type T whose property value is of a certain type K, * or 'never' in case of no such properties. * It evaluates the value type regardless of the property being optional or not by converting T to Required<T>. * Note the '-?' in '[I in keyof T]-?:' that is required to map all optional but un-intended properties to 'never'. * Without that, optional props like those inherited from 'AstNode' would be mapped to 'never|undefined', * and the subsequent value mapping ('...[keyof T]') would yield 'undefined' instead of 'never' for AstNode types * not having any property matching type K, which in turn yields follow-up errors. */ type ExtractKeysOfValueType<T, K> = { [I in keyof T]-?: Required<T>[I] extends K ? I : never }[keyof T]; /** * Describes a union type including only names(!) of the cross-reference properties of the given AstNode type. * Enhances compile-time validation of cross-reference distinctions, e.g. in scope providers * in combination with `assertUnreachable(context.property)`. */ export type CrossReferencesOfAstNodeType<N extends AstNode> = ExtractKeysOfValueType<N, Reference | Reference[]>; /** * Represents the enumeration-like type, that lists all AstNode types of your grammar. */ export type AstTypeList<T> = Record<keyof T, AstNode>; /** * Describes a union type including of all AstNode types containing cross-references. * A is meant to be the interface `XXXAstType` fromm your generated `ast.ts` file. * Enhances compile-time validation of cross-reference distinctions, e.g. in scope providers * in combination with `assertUnreachable(context.container)`. */ export type AstNodeTypesWithCrossReferences<A extends AstTypeList<A>> = { [T in keyof A]-?: CrossReferencesOfAstNodeType<A[T]> extends never ? never : A[T] }[keyof A]; export type Mutable<T> = { -readonly [P in keyof T]: T[P] };