/******************************************************************************
 * 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 { CancellationToken } from 'vscode-languageserver' ; import { LangiumServices } from '../services' ; import { AstNode , AstNodeDescription , AstReflection , CstNode , LinkingError , Reference , ReferenceInfo } from '../syntax-tree' ; import { isAstNode , isAstNodeDescription , isLinkingError , streamAllContents , streamReferences } from '../utils/ast-util' ; import { interruptAndCheck } from '../utils/promise-util' ; import { AstNodeLocator } from '../workspace/ast-node-locator' ; import { DocumentState , LangiumDocument , LangiumDocuments } from '../workspace/documents' ; import { ScopeProvider } from './scope' ; /**
 * This service is responsible for resolving cross-references in the AST.
 */ export interface Linker { param document param cancelToken link ( document , cancelToken ? ) : Promise < void > ; param document unlink ( document ) : void ; param node param refId param reference getCandidate ( node , refId , reference ) : AstNodeDescription | LinkingError ; param node param refNode param refId param refText returns buildReference ( node , refNode , refId , refText ) : Reference ; } export function getReferenceId ( containerTypeName , propertyName ) { return `${ containerTypeName }:${ propertyName }` ; } export function getReferenceProperty ( referenceId ) { return referenceId . substring ( referenceId . indexOf ( ':' ) + 1 ) ; } interface DefaultReference extends Reference { _ref ? : AstNode | AstNodeDescription | LinkingError ; } export class DefaultLinker implements Linker { protected readonly reflection : AstReflection ; protected readonly scopeProvider : ScopeProvider ; protected readonly astNodeLocator : AstNodeLocator ; protected readonly langiumDocuments : ( ) => LangiumDocuments ; constructor ( services ) { this . reflection = services . shared . AstReflection ; this . langiumDocuments = ( ) => services . shared . workspace . LangiumDocuments ; this . scopeProvider = services . references . ScopeProvider ; this . astNodeLocator = services . index . AstNodeLocator ; } async link ( document , cancelToken = CancellationToken . None ) { const process = ( node ) => { streamReferences ( node ) . forEach ( ref => this . doLink ( ref , document ) ) ; } ; const rootNode = document . parseResult . value ; process ( rootNode ) ; for ( const content of streamAllContents ( rootNode ) ) { await interruptAndCheck ( cancelToken ) ; process ( content . node ) ; } document . state = DocumentState . Linked ; } protected doLink ( info , document ) { const ref = info . reference as DefaultReference ; if ( ref . _ref === undefined ) { try { const refId = getReferenceId ( info . container . $type , info . property ) ; const description = this . getCandidate ( info . container , refId , ref ) ; if ( ! isLinkingError ( description ) && this . langiumDocuments ( ) . hasDocument ( description . documentUri ) ) { const linkedNode = this . loadAstNode ( description ) ; ref . _ref = linkedNode ?? this . createLinkingError ( info , refId , description ) ; } else { ref . _ref = description ; } } catch ( err ) { ref . _ref = { ... info , message : `An error occurred while resolving reference to '${ ref . $refText }': ${ err }` } ; } } document . references . push ( ref ) ; } unlink ( document ) { for ( const ref of document . references ) { delete ( ref as DefaultReference ) . _ref ; } document . references = [ ] ; } getCandidate ( container , refId , reference ) { const scope = this . scopeProvider . getScope ( container , refId ) ; const description = scope . getElement ( reference . $refText ) ; return description ?? this . createLinkingError ( { container , property : getReferenceProperty ( refId ) , reference } , refId ) ; } buildReference ( node , refNode , refId , refText ) { const linker = this ; const reference = { $refNode : refNode , $refText : refText , get ref ( ) { if ( isAstNode ( this . _ref ) ) { return this . _ref ; } else if ( this . _ref === undefined ) { this . _ref = linker . getLinkedNode ( node , refId , reference ) ; } else if ( isAstNodeDescription ( this . _ref ) ) { const linkedNode = linker . loadAstNode ( this . _ref ) ; this . _ref = linkedNode ?? linker . createLinkingError ( { container : node , property : getReferenceProperty ( refId ) , reference } , refId , this . _ref ) ; } return isAstNode ( this . _ref ) ? this . _ref : undefined ; } , get error ( ) { return isLinkingError ( this . _ref ) ? this . _ref : undefined ; } } ; return reference ; } protected getLinkedNode ( container , refId , reference ) { try { const description = this . getCandidate ( container , refId , reference ) ; if ( isLinkingError ( description ) ) { return description ; } else { const linkedNode = this . loadAstNode ( description ) ; return linkedNode ?? this . createLinkingError ( { container , property : getReferenceProperty ( refId ) , reference } , refId , description ) ; } } catch ( err ) { return { container , property : getReferenceProperty ( refId ) , reference , message : `An error occurred while resolving reference to '${ reference . $refText }': ${ err }` } ; } } protected loadAstNode ( nodeDescription ) { if ( nodeDescription . node ) { return nodeDescription . node ; } const doc = this . langiumDocuments ( ) . getOrCreateDocument ( nodeDescription . documentUri ) ; return this . astNodeLocator . getAstNode ( doc , nodeDescription . path ) ; } protected createLinkingError ( refInfo , refId , targetDescription ? ) { const referenceType = this . reflection . getReferenceType ( refId ) ; return { ... refInfo , message : `Could not resolve reference to ${ referenceType } named '${ refInfo . reference . $refText }'.` , targetDescription } ; } }