/******************************************************************************
 * 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 { TextDocument } from 'vscode-languageserver-textdocument' ; import { URI , Utils } from 'vscode-uri' ; import * as ast from '../grammar/generated/ast' ; import { CompositeCstNodeImpl } from '../parser/cst-node-builder' ; import { LangiumServices } from '../services' ; import { AstNode , AstNodeDescription , CstNode } from '../syntax-tree' ; import { getContainerOfType , getDocument , Mutable , streamAllContents } from '../utils/ast-util' ; import { MultiMap } from '../utils/collections' ; import { streamCst } from '../utils/cst-util' ; import { escapeRegExp } from '../utils/regex-util' ; import { documentFromText , LangiumDocuments , PrecomputedScopes } from '../workspace/documents' ; import { createLangiumGrammarServices } from './langium-grammar-module' ; export type Cardinality = '?' | '*' | '+' | undefined ; export type Operator = '=' | '+=' | '?=' | undefined ; export function isOptional ( cardinality ? ) { return cardinality === '?' || cardinality === '*' ; } export function isArray ( cardinality ? ) { return cardinality === '*' || cardinality === '+' ; } export function isArrayOperator ( operator ? ) { return operator === '+=' ; } export function isDataTypeRule ( rule ) { return isDataTypeRuleInternal ( rule , new Set ( ) ) ; } function isDataTypeRuleInternal ( rule , visited ) { if ( visited . has ( rule ) ) { return true ; } visited . add ( rule ) ; for ( const { node } of streamAllContents ( rule ) ) { if ( ast . isRuleCall ( node ) && ast . isParserRule ( node . rule . ref ) ) { if ( ! isDataTypeRuleInternal ( node . rule . ref , visited ) ) { return false ; } } else if ( ast . isAssignment ( node ) ) { return false ; } else if ( ast . isAction ( node ) ) { return false ; } } return true ; } export function findNameAssignment ( rule ) { for ( const { node } of streamAllContents ( rule ) ) { if ( ast . isAssignment ( node ) && node . feature . toLowerCase ( ) === 'name' ) { return node ; } else if ( ast . isRuleCall ( node ) && ast . isParserRule ( node . rule . ref ) ) { const parentAssignment = getContainerOfType ( node , ast . isAssignment ) ; if ( ! parentAssignment ) { const childNameAssignment = findNameAssignment ( node . rule . ref ) ; if ( childNameAssignment ) { return childNameAssignment ; } } } } return undefined ; } export function isCommentTerminal ( terminalRule ) { return terminalRule . hidden && ! ' ' . match ( terminalRegex ( terminalRule ) ) ; } export function findNodeForFeature ( node , feature , index ? ) { const nodes = findNodesForFeature ( node , feature ) ; if ( nodes . length === 0 ) { return undefined ; } if ( index !== undefined ) { index = Math . max ( 0 , Math . min ( index , nodes . length - 1 ) ) ; } else { index = 0 ; } return nodes [ index ] ; } export function findKeywordNode ( node , keyword ) { if ( node && ast . isKeyword ( node . feature ) && node . feature . value === keyword ) { return node ; } return findKeywordNodeInternal ( node , keyword , node ?. element ) ; } export function findKeywordNodeInternal ( node , keyword , element ) { if ( ! node || ! element ) { return undefined ; } const treeIterator = streamCst ( node ) . iterator ( ) ; let result ; do { result = treeIterator . next ( ) ; if ( ! result . done ) { const childNode = result . value ; if ( childNode . element === element ) { if ( ast . isKeyword ( childNode . feature ) && childNode . feature . value === keyword ) { return childNode ; } } else { treeIterator . prune ( ) ; } } } while ( ! result . done ) ; return undefined ; } param node param feature param element param first returns function findNodesForFeatureInternal ( node , feature , element , first ) { if ( ! node || ! feature || node . element !== element ) { return [ ] ; } const nodeFeature = getContainerOfType ( node . feature , ast . isAssignment ) ; if ( ! first && nodeFeature && nodeFeature . feature === feature ) { return [ node ] ; } else if ( node instanceof CompositeCstNodeImpl ) { return node . children . flatMap ( e => findNodesForFeatureInternal ( e , feature , element , false ) ) ; } return [ ] ; } export function findNodesForFeature ( node , feature ) { return findNodesForFeatureInternal ( node , feature , node ?. element , true ) ; } export function findAssignment ( cstNode ) { let n = cstNode ; do { const assignment = getContainerOfType ( n . feature , ast . isAssignment ) ; if ( assignment ) { return assignment ; } n = n . parent ; } while ( n ) ; return undefined ; } export function getTypeNameAtElement ( rule , element ) { const action = getActionAtElement ( element ) ; return action ?. type ?? getTypeName ( rule ) ; } export function getActionAtElement ( element ) { const parent = element . $container ; if ( ast . isGroup ( parent ) ) { const elements = parent . elements ; const index = elements . indexOf ( element ) ; for ( let i = index - 1 ; i >= 0 ; i -- ) { const item = elements [ i ] ; if ( ast . isAction ( item ) ) { return item ; } else { let action ; streamAllContents ( elements [ i ] ) . forEach ( e => { if ( ast . isAction ( e . node ) ) { action = e . node ; } } ) ; if ( action ) { return action ; } } } } if ( ast . isAbstractElement ( parent ) ) { return getActionAtElement ( parent ) ; } else { return undefined ; } } export function terminalRegex ( terminalRule ) { return abstractElementToRegex ( terminalRule . terminal ) ; } const WILDCARD = /[\s\S]/ . source ; function abstractElementToRegex ( element ) { if ( ast . isTerminalAlternatives ( element ) ) { return terminalAlternativesToRegex ( element ) ; } else if ( ast . isTerminalGroup ( element ) ) { return terminalGroupToRegex ( element ) ; } else if ( ast . isCharacterRange ( element ) ) { return characterRangeToRegex ( element ) ; } else if ( ast . isTerminalRuleCall ( element ) ) { const rule = element . rule . ref ; if ( ! rule ) { throw new Error ( 'Missing rule reference.' ) ; } return withCardinality ( terminalRegex ( rule ) , element . cardinality , true ) ; } else if ( ast . isNegatedToken ( element ) ) { return negateTokenToRegex ( element ) ; } else if ( ast . isUntilToken ( element ) ) { return untilTokenToRegex ( element ) ; } else if ( ast . isRegexToken ( element ) ) { return withCardinality ( element . regex , element . cardinality , true ) ; } else if ( ast . isWildcard ( element ) ) { return withCardinality ( WILDCARD , element . cardinality ) ; } else { throw new Error ( 'Invalid terminal element.' ) ; } } function terminalAlternativesToRegex ( alternatives ) { return withCardinality ( `(${ alternatives . elements . map ( abstractElementToRegex ) . join ( '|' ) })` , alternatives . cardinality ) ; } function terminalGroupToRegex ( group ) { return withCardinality ( group . elements . map ( abstractElementToRegex ) . join ( '' ) , group . cardinality ) ; } function untilTokenToRegex ( until ) { return withCardinality ( `${ WILDCARD }*?${ abstractElementToRegex ( until . terminal ) }` , until . cardinality ) ; } function negateTokenToRegex ( negate ) { return withCardinality ( `(?!${ abstractElementToRegex ( negate . terminal ) })${ WILDCARD }*?` , negate . cardinality , true ) ; } function characterRangeToRegex ( range ) { if ( range . right ) { return withCardinality ( `[${ keywordToRegex ( range . left ) }-${ keywordToRegex ( range . right ) }]` , range . cardinality ) ; } return withCardinality ( keywordToRegex ( range . left ) , range . cardinality , true ) ; } function keywordToRegex ( keyword ) { return escapeRegExp ( keyword . value ) ; } function withCardinality ( regex , cardinality ? , wrap = false ) { if ( cardinality ) { if ( wrap ) { regex = `(${ regex })` ; } return `${ regex }${ cardinality }` ; } return regex ; } export function getTypeName ( rule ) { if ( rule ) { return rule . type ?? rule . name ; } else { throw new Error ( 'Unknown rule type' ) ; } } export function getRuleType ( rule ) { if ( ast . isParserRule ( rule ) && isDataTypeRule ( rule ) || ast . isTerminalRule ( rule ) ) { return rule . type ?? 'string' ; } return getTypeName ( rule ) ; } export function getEntryRule ( grammar ) { return grammar . rules . find ( e => ast . isParserRule ( e ) && e . entry ) as ast . ParserRule ; } export function resolveImport ( documents , imp ) { if ( imp . path === undefined || imp . path . length === 0 ) { return undefined ; } const uri = Utils . dirname ( getDocument ( imp ) . uri ) ; let grammarPath = imp . path ; if ( ! grammarPath . endsWith ( '.langium' ) ) { grammarPath += '.langium' ; } const resolvedUri = Utils . resolvePath ( uri , grammarPath ) ; try { const resolvedDocument = documents . getOrCreateDocument ( resolvedUri ) ; const node = resolvedDocument . parseResult . value ; if ( ast . isGrammar ( node ) ) { return node ; } } catch { } return undefined ; } export function resolveTransitiveImports ( documents , grammar ) { if ( ast . isGrammarImport ( grammar ) ) { const resolvedGrammar = resolveImport ( documents , grammar ) ; if ( resolvedGrammar ) { const transitiveGrammars = resolveTransitiveImportsInternal ( documents , resolvedGrammar ) ; transitiveGrammars . push ( resolvedGrammar ) ; return transitiveGrammars ; } return [ ] ; } else { return resolveTransitiveImportsInternal ( documents , grammar ) ; } } function resolveTransitiveImportsInternal ( documents , grammar , initialGrammar = grammar , visited = new Set ( ) , grammars = new Set ( ) ) { const doc = getDocument ( grammar ) ; if ( initialGrammar !== grammar ) { grammars . add ( grammar ) ; } if ( ! visited . has ( doc . uri ) ) { visited . add ( doc . uri ) ; for ( const imp of grammar . imports ) { const importedGrammar = resolveImport ( documents , imp ) ; if ( importedGrammar ) { resolveTransitiveImportsInternal ( documents , importedGrammar , initialGrammar , visited , grammars ) ; } } } return Array . from ( grammars ) ; } export function loadGrammar ( json ) { const services = createLangiumGrammarServices ( ) . grammar ; const astNode = services . serializer . JsonSerializer . deserialize ( json ) ; if ( ! ast . isGrammar ( astNode ) ) { throw new Error ( 'Could not load grammar from specified json input.' ) ; } const grammar = astNode as Mutable < ast . Grammar > ; const textDocument = TextDocument . create ( 'memory://grammar.langium' , 'langium' , 0 , '' ) ; const document = documentFromText ( textDocument , { lexerErrors : [ ] , parserErrors : [ ] , value : grammar } ) ; grammar . $document = document ; document . precomputedScopes = computeGrammarScope ( services , grammar ) ; return grammar ; } export function computeGrammarScope ( services , grammar ) { const nameProvider = services . references . NameProvider ; const descriptions = services . index . AstNodeDescriptionProvider ; const document = getDocument ( grammar ) ; const scopes = new MultiMap < AstNode , AstNodeDescription > ( ) ; for ( const content of streamAllContents ( grammar ) ) { const { node } = content ; const container = node . $container ; if ( container ) { const name = nameProvider . getName ( node ) ; if ( name ) { const description = descriptions . createDescription ( node , name , document ) ; scopes . add ( container , description ) ; } } } return scopes ; }