/******************************************************************************
 * 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 , Diagnostic , DiagnosticSeverity } from 'vscode-languageserver' ; import { Range } from 'vscode-languageserver-textdocument' ; import { tokenToRange } from '..' ; import { findNodeForFeature } from '../grammar/grammar-util' ; import { LangiumServices } from '../services' ; import { AstNode } from '../syntax-tree' ; import { streamAllContents } from '../utils/ast-util' ; import { interruptAndCheck } from '../utils/promise-util' ; import { LangiumDocument } from '../workspace/documents' ; import { DiagnosticInfo , ValidationAcceptor , ValidationRegistry } from './validation-registry' ; export interface DocumentValidator { param document param cancelToken throws validateDocument ( document , cancelToken ? ) : Promise < Diagnostic [ ] > ; } export class DefaultDocumentValidator { protected readonly validationRegistry : ValidationRegistry ; constructor ( services ) { this . validationRegistry = services . validation . ValidationRegistry ; } async validateDocument ( document , cancelToken = CancellationToken . None ) { const parseResult = document . parseResult ; const diagnostics = [ ] ; await interruptAndCheck ( cancelToken ) ; for ( const lexerError of parseResult . lexerErrors ) { const diagnostic = { severity : DiagnosticSeverity . Error , range : { start : { line : lexerError . line - 1 , character : lexerError . column - 1 } , end : { line : lexerError . line - 1 , character : lexerError . column + lexerError . length - 1 } } , message : lexerError . message } ; diagnostics . push ( diagnostic ) ; } for ( const parserError of parseResult . parserErrors ) { const token = parserError . token ; const diagnostic = { severity : DiagnosticSeverity . Error , range : tokenToRange ( token ) , message : parserError . message } ; diagnostics . push ( diagnostic ) ; } for ( const reference of document . references ) { const linkingError = reference . error ; if ( linkingError ) { const info = { node : linkingError . container , property : linkingError . property , index : linkingError . index } ; diagnostics . push ( this . toDiagnostic ( 'error' , linkingError . message , info ) ) ; } } try { diagnostics . push ( ... await this . validateAst ( parseResult . value , document , cancelToken ) ) ; } catch ( err ) { console . error ( 'An error occurred during validation:' , err ) ; } await interruptAndCheck ( cancelToken ) ; return diagnostics ; } protected async validateAst ( rootNode , document , cancelToken = CancellationToken . None ) { const validationItems = [ ] ; const acceptor = < N extends AstNode > ( severity , message , info ) => { validationItems . push ( this . toDiagnostic ( severity , message , info ) ) ; } ; const runChecks = async ( node ) => { const checks = this . validationRegistry . getChecks ( node . $type ) ; for ( const check of checks ) { await check ( node , acceptor , cancelToken ) ; } } ; await runChecks ( rootNode ) ; await Promise . all ( streamAllContents ( rootNode ) . map ( async ( c ) => { await interruptAndCheck ( cancelToken ) ; await runChecks ( c . node ) ; } ) ) ; return validationItems ; } protected toDiagnostic < N extends AstNode > ( severity , message , info ) { return { message , range : getDiagnosticRange ( info ) , severity : toDiagnosticSeverity ( severity ) , code : info . code , codeDescription : info . codeDescription , tags : info . tags , relatedInformation : info . relatedInformation , data : info . data } ; } } export function getDiagnosticRange < N extends AstNode > ( info ) { if ( info . range ) { return info . range ; } if ( info . property !== undefined && typeof info . property !== 'string' ) { throw new Error ( 'Invalid property: ' + info . property ) ; } const cstNode = findNodeForFeature ( info . node . $cstNode , info . property , info . index ) ?? info . node . $cstNode ; if ( ! cstNode ) { return { start : { line : 0 , character : 0 } , end : { line : 0 , character : 0 } } ; } return cstNode . range ; } export function toDiagnosticSeverity ( severity ) { switch ( severity ) { case 'error' : return DiagnosticSeverity . Error ; case 'warning' : return DiagnosticSeverity . Warning ; case 'info' : return DiagnosticSeverity . Information ; case 'hint' : return DiagnosticSeverity . Hint ; default : throw new Error ( 'Invalid diagnostic severity: ' + severity ) ; } }