import * as parser from "jsonc-parser" ; import * as vscode from "vscode" ; import { Constants } from "../common/constants" ; import { DiagnosticMessage , DigitalTwinConstants } from "./digitalTwinConstants" ; import { ClassNode , PropertyNode , ValueSchema } from "./digitalTwinGraph" ; import { IntelliSenseUtility , JsonNodeType , PropertyPair , ModelContent } from "./intelliSenseUtility" ; import { LANGUAGE_CODE } from "./languageCode" ; /**
 * Diagnostic problem
 */ interface Problem { offset : number ; length : number ; message : string ; } /**
 * Diagnostic provider for DigitalTwin IntelliSense
 */ export class DigitalTwinDiagnosticProvider { param propertyNode param type private static findClassNode ( propertyNode , type ) { if ( propertyNode . range ) { return propertyNode . range . find ( classNode => IntelliSenseUtility . getClassType ( classNode ) === type ) ; } return undefined ; } param jsonNode private static getNamePropertyPair ( jsonNode ) { if ( jsonNode . type !== JsonNodeType . Object || ! jsonNode . children ) { return undefined ; } let propertyPair ; for ( const child of jsonNode . children ) { propertyPair = IntelliSenseUtility . parseProperty ( child ) ; if ( ! propertyPair ) { continue ; } if ( propertyPair . name . value === DigitalTwinConstants . NAME ) { return propertyPair ; } } return undefined ; } param jsonNode param digitalTwinNode param problems private static addProblemOfInvalidType ( jsonNode , digitalTwinNode , problems ) { const validTypes = IntelliSenseUtility . getValidTypes ( digitalTwinNode ) ; const message = [ DiagnosticMessage . InvalidType , ... validTypes ] . join ( Constants . LINE_FEED ) ; DigitalTwinDiagnosticProvider . addProblem ( jsonNode , problems , message ) ; } param jsonNode param problems private static addProblemOfUnexpectedProperty ( jsonNode , problems ) { const message = `${ jsonNode . value as string } ${ DiagnosticMessage . UnexpectedProperty }` ; DigitalTwinDiagnosticProvider . addProblem ( jsonNode , problems , message ) ; } param jsonNode param problems param message param isContainer private static addProblem ( jsonNode , problems , message , isContainer ? ) { const length = isContainer ? 0 : jsonNode . length ; problems . push ( { offset : jsonNode . offset , length , message } ) ; } param version param jsonNode param digitalTwinNode param problems private static validateNode ( version , jsonNode , digitalTwinNode , problems ) { switch ( jsonNode . type ) { case JsonNodeType . Object : DigitalTwinDiagnosticProvider . validateObjectNode ( version , jsonNode , digitalTwinNode , problems ) ; break ; case JsonNodeType . Array : DigitalTwinDiagnosticProvider . validateArrayNode ( version , jsonNode , digitalTwinNode , problems ) ; break ; case JsonNodeType . String : DigitalTwinDiagnosticProvider . validateStringNode ( version , jsonNode , digitalTwinNode , problems ) ; break ; case JsonNodeType . Number : DigitalTwinDiagnosticProvider . validateNumberNode ( jsonNode , digitalTwinNode , problems ) ; break ; case JsonNodeType . Boolean : DigitalTwinDiagnosticProvider . validateBooleanNode ( jsonNode , digitalTwinNode , problems ) ; break ; default : } } param version param jsonNode param digitalTwinNode param problems private static validateObjectNode ( version , jsonNode , digitalTwinNode , problems ) { const classes = IntelliSenseUtility . getObjectClasses ( digitalTwinNode , version ) ; if ( ! classes . length ) { DigitalTwinDiagnosticProvider . addProblem ( jsonNode , problems , DiagnosticMessage . NotObjectType ) ; return ; } if ( ! jsonNode . children || ! jsonNode . children . length ) { DigitalTwinDiagnosticProvider . addProblem ( jsonNode , problems , DiagnosticMessage . EmptyObject , true ) ; return ; } const typePath = [ DigitalTwinConstants . TYPE ] ; const typeNode = parser . findNodeAtLocation ( jsonNode , typePath ) ; if ( ! typeNode && classes . length !== 1 ) { DigitalTwinDiagnosticProvider . addProblem ( jsonNode , problems , DiagnosticMessage . MissingType , true ) ; return ; } let classNode ; if ( typeNode ) { classNode = DigitalTwinDiagnosticProvider . getValidObjectType ( typeNode , digitalTwinNode , classes , problems ) ; } else { classNode = classes [ 0 ] ; } if ( ! classNode ) { return ; } if ( IntelliSenseUtility . isLanguageNode ( classNode ) ) { DigitalTwinDiagnosticProvider . validateLanguageNode ( version , jsonNode , digitalTwinNode , problems ) ; return ; } const expect = new Set < string > ( ) ; const exist = new Set < string > ( ) ; DigitalTwinDiagnosticProvider . validateProperties ( version , jsonNode , classNode , problems , expect , exist ) ; if ( classNode . constraint && classNode . constraint . required ) { const requiredProperties = classNode . constraint . required . filter ( property => { if ( ! IntelliSenseUtility . isReservedName ( property ) && ! expect . has ( property ) ) { return false ; } if ( property === DigitalTwinConstants . CONTEXT && digitalTwinNode . label === DigitalTwinConstants . SCHEMA ) { return false ; } return ! exist . has ( property ) ; } ) ; if ( requiredProperties . length ) { const message = [ DiagnosticMessage . MissingRequiredProperties , ... requiredProperties ] . join ( Constants . LINE_FEED ) ; DigitalTwinDiagnosticProvider . addProblem ( jsonNode , problems , message , true ) ; } requiredProperties . length = 0 ; } expect . clear ( ) ; exist . clear ( ) ; } param jsonNode param digitalTwinNode param classes param problems private static getValidObjectType ( jsonNode , digitalTwinNode , classes , problems ) { let classNode ; const dummyNode = { id : DigitalTwinConstants . DUMMY_NODE , range : classes } ; if ( jsonNode . type === JsonNodeType . String ) { classNode = DigitalTwinDiagnosticProvider . findClassNode ( dummyNode , jsonNode . value as string ) ; } else if ( jsonNode . type === JsonNodeType . Array && jsonNode . children && digitalTwinNode . label === DigitalTwinConstants . CONTENTS ) { let currentNode ; for ( const child of jsonNode . children ) { if ( child . type !== JsonNodeType . String ) { classNode = undefined ; break ; } currentNode = DigitalTwinDiagnosticProvider . findClassNode ( dummyNode , child . value as string ) ; if ( currentNode ) { if ( classNode ) { const message = `${ DiagnosticMessage . ConflictType } ${ classNode . label } and ${ currentNode . label }` ; DigitalTwinDiagnosticProvider . addProblem ( jsonNode , problems , message ) ; return undefined ; } else { classNode = currentNode ; } } } } if ( ! classNode ) { DigitalTwinDiagnosticProvider . addProblemOfInvalidType ( jsonNode , dummyNode , problems ) ; } return classNode ; } param version param jsonNode param classNode param problems param expect param exist private static validateProperties ( version , jsonNode , classNode , problems , expect , exist ) { if ( ! jsonNode . children ) { return ; } const expectedProperties = new Map < string , PropertyNode > ( ) ; for ( const property of IntelliSenseUtility . getPropertiesOfClassByVersion ( classNode , version ) ) { if ( property . label ) { expect . add ( property . label ) ; expectedProperties . set ( property . label , property ) ; } } let propertyName ; let propertyPair ; let propertyNode ; for ( const child of jsonNode . children ) { propertyPair = IntelliSenseUtility . parseProperty ( child ) ; if ( ! propertyPair ) { continue ; } propertyName = propertyPair . name . value as string ; exist . add ( propertyName ) ; switch ( propertyName ) { case DigitalTwinConstants . ID : propertyNode = IntelliSenseUtility . getPropertyNode ( DigitalTwinConstants . ID ) ; if ( propertyNode ) { DigitalTwinDiagnosticProvider . validateNode ( version , propertyPair . value , propertyNode , problems ) ; } break ; case DigitalTwinConstants . CONTEXT : if ( classNode . constraint && classNode . constraint . required && classNode . constraint . required . includes ( DigitalTwinConstants . CONTEXT ) ) { if ( IntelliSenseUtility . getDigitalTwinVersion ( propertyPair . value ) !== version ) { DigitalTwinDiagnosticProvider . addProblem ( propertyPair . value , problems , DiagnosticMessage . InvalidContext ) ; } } else { DigitalTwinDiagnosticProvider . addProblemOfUnexpectedProperty ( propertyPair . name , problems ) ; } break ; case DigitalTwinConstants . TYPE : break ; default : propertyNode = expectedProperties . get ( propertyName ) ; if ( ! propertyNode ) { DigitalTwinDiagnosticProvider . addProblemOfUnexpectedProperty ( propertyPair . name , problems ) ; } else { DigitalTwinDiagnosticProvider . validateNode ( version , propertyPair . value , propertyNode , problems ) ; } } } expectedProperties . clear ( ) ; } param version param jsonNode param digitalTwinNode param problems private static validateArrayNode ( version , jsonNode , digitalTwinNode , problems ) { if ( ! digitalTwinNode . isArray ) { DigitalTwinDiagnosticProvider . addProblemOfInvalidType ( jsonNode , digitalTwinNode , problems ) ; return ; } if ( ! jsonNode . children || ! jsonNode . children . length ) { DigitalTwinDiagnosticProvider . addProblem ( jsonNode , problems , DiagnosticMessage . EmptyArray , true ) ; return ; } let message ; if ( digitalTwinNode . constraint ) { if ( digitalTwinNode . constraint . minItems && jsonNode . children . length < digitalTwinNode . constraint . minItems ) { message = `${ DiagnosticMessage . TooFewItems } ${ digitalTwinNode . constraint . minItems }.` ; DigitalTwinDiagnosticProvider . addProblem ( jsonNode , problems , message , true ) ; } else if ( digitalTwinNode . constraint . maxItems && jsonNode . children . length > digitalTwinNode . constraint . maxItems ) { message = `${ DiagnosticMessage . TooManyItems } ${ digitalTwinNode . constraint . maxItems }.` ; DigitalTwinDiagnosticProvider . addProblem ( jsonNode , problems , message , true ) ; } } let propertyPair ; let objectName ; const exist = new Set < string > ( ) ; for ( const child of jsonNode . children ) { propertyPair = DigitalTwinDiagnosticProvider . getNamePropertyPair ( child ) ; if ( propertyPair ) { objectName = propertyPair . value . value as string ; if ( exist . has ( objectName ) ) { message = `${ objectName } ${ DiagnosticMessage . DuplicateItem }` ; DigitalTwinDiagnosticProvider . addProblem ( propertyPair . value , problems , message ) ; } else { exist . add ( objectName ) ; } } DigitalTwinDiagnosticProvider . validateNode ( version , child , digitalTwinNode , problems ) ; } exist . clear ( ) ; } param version param jsonNode param digitalTwinNode param problems private static validateStringNode ( version , jsonNode , digitalTwinNode , problems ) { const classNode = DigitalTwinDiagnosticProvider . findClassNode ( digitalTwinNode , ValueSchema . String ) ; if ( ! classNode ) { DigitalTwinDiagnosticProvider . validateEnumNode ( version , jsonNode , digitalTwinNode , problems ) ; return ; } const value = jsonNode . value as string ; if ( ! value ) { DigitalTwinDiagnosticProvider . addProblem ( jsonNode , problems , DiagnosticMessage . EmptyString ) ; return ; } let message ; if ( digitalTwinNode . constraint ) { if ( digitalTwinNode . constraint . minLength && value . length < digitalTwinNode . constraint . minLength ) { message = `${ DiagnosticMessage . ShorterThanMinLength } ${ digitalTwinNode . constraint . minLength }.` ; DigitalTwinDiagnosticProvider . addProblem ( jsonNode , problems , message ) ; return ; } else if ( digitalTwinNode . constraint . maxLength && value . length > digitalTwinNode . constraint . maxLength ) { message = `${ DiagnosticMessage . LongerThanMaxLength } ${ digitalTwinNode . constraint . maxLength }.` ; DigitalTwinDiagnosticProvider . addProblem ( jsonNode , problems , message ) ; return ; } else if ( digitalTwinNode . constraint . pattern ) { const regex = new RegExp ( digitalTwinNode . constraint . pattern ) ; if ( ! regex . test ( value ) ) { message = `${ DiagnosticMessage . NotMatchPattern } ${ digitalTwinNode . constraint . pattern }.` ; DigitalTwinDiagnosticProvider . addProblem ( jsonNode , problems , message ) ; return ; } } } } param version param jsonNode param digitalTwinNode param problems private static validateEnumNode ( version , jsonNode , digitalTwinNode , problems ) { const enums = IntelliSenseUtility . getEnums ( digitalTwinNode , version ) ; if ( ! enums . length ) { DigitalTwinDiagnosticProvider . addProblemOfInvalidType ( jsonNode , digitalTwinNode , problems ) ; } else if ( ! enums . includes ( jsonNode . value as string ) ) { const message = [ DiagnosticMessage . InvalidEnum , ... enums ] . join ( Constants . LINE_FEED ) ; DigitalTwinDiagnosticProvider . addProblem ( jsonNode , problems , message ) ; } enums . length = 0 ; } param jsonNode param digitalTwinNode param problems private static validateNumberNode ( jsonNode , digitalTwinNode , problems ) { const classNode = DigitalTwinDiagnosticProvider . findClassNode ( digitalTwinNode , ValueSchema . Int ) ; if ( ! classNode || ! Number . isInteger ( jsonNode . value as number ) ) { DigitalTwinDiagnosticProvider . addProblemOfInvalidType ( jsonNode , digitalTwinNode , problems ) ; return ; } } param jsonNode param digitalTwinNode param problems private static validateBooleanNode ( jsonNode , digitalTwinNode , problems ) { const classNode = DigitalTwinDiagnosticProvider . findClassNode ( digitalTwinNode , ValueSchema . Boolean ) ; if ( ! classNode ) { DigitalTwinDiagnosticProvider . addProblemOfInvalidType ( jsonNode , digitalTwinNode , problems ) ; return ; } } param version param jsonNode param digitalTwinNode param problems private static validateLanguageNode ( version , jsonNode , digitalTwinNode , problems ) { if ( ! jsonNode . children ) { return ; } let propertyName ; let propertyPair ; for ( const child of jsonNode . children ) { propertyPair = IntelliSenseUtility . parseProperty ( child ) ; if ( ! propertyPair ) { continue ; } propertyName = propertyPair . name . value as string ; if ( ! LANGUAGE_CODE . has ( propertyName ) ) { DigitalTwinDiagnosticProvider . addProblemOfUnexpectedProperty ( propertyPair . name , problems ) ; } else if ( propertyPair . value . type !== JsonNodeType . String ) { DigitalTwinDiagnosticProvider . addProblem ( propertyPair . value , problems , DiagnosticMessage . ValueNotString ) ; } else { DigitalTwinDiagnosticProvider . validateStringNode ( version , propertyPair . value , digitalTwinNode , problems ) ; } } } param document param collection updateDiagnostics ( document , collection ) { if ( ! IntelliSenseUtility . enabled ( ) ) { return ; } const modelContent = IntelliSenseUtility . parseDigitalTwinModel ( document . getText ( ) ) ; if ( ! modelContent ) { collection . delete ( document . uri ) ; return ; } const diagnostics = this . provideDiagnostics ( document , modelContent ) ; collection . set ( document . uri , diagnostics ) ; } param document param modelContent private provideDiagnostics ( document , modelContent ) { let diagnostics = [ ] ; const digitalTwinNode = IntelliSenseUtility . getEntryNode ( ) ; if ( ! digitalTwinNode ) { return diagnostics ; } const problems = [ ] ; DigitalTwinDiagnosticProvider . validateNode ( modelContent . version , modelContent . jsonNode , digitalTwinNode , problems ) ; diagnostics = problems . map ( p => new vscode . Diagnostic ( new vscode . Range ( document . positionAt ( p . offset ) , document . positionAt ( p . offset + p . length ) ) , p . message , vscode . DiagnosticSeverity . Error ) ) ; problems . length = 0 ; return diagnostics ; } }