import { applicationCCs , CommandClasses , getCCName } from "@zwave-js/core" ; import { expressionToCommandClass , getCommandClassFromDecorator , loadTSConfig , projectRoot , reportProblem , } from "@zwave-js/maintenance" ; import * as path from "path" ; import ts from "typescript" ; function getRequiredInterviewCCsFromMethod ( sourceFile , method ) { const returnExpression = method . body ?. statements . find ( ( statement ) => ts . isReturnStatement ( statement ) && statement . expression && ts . isArrayLiteralExpression ( statement . expression ) ) as ts . ReturnStatement | undefined ; if ( ! returnExpression ) return ; const elements = ( returnExpression . expression as ts . ArrayLiteralExpression ) . elements ; const ret = elements . map ( ( e ) => expressionToCommandClass ( sourceFile , e ) ) . filter ( ( cc ) => cc != undefined ) as CommandClasses [ ] ; return ret ; } export function lintCCInterview ( ) { const tsConfig = loadTSConfig ( "zwave-js" ) ; const program = ts . createProgram ( tsConfig . fileNames , tsConfig . options ) ; let hasError = false ; for ( const sourceFile of program . getSourceFiles ( ) ) { const relativePath = path . relative ( projectRoot , sourceFile . fileName ) . replace ( /\\/g , "/" ) ; if ( relativePath . startsWith ( ".." ) ) continue ; if ( ! relativePath . includes ( "/commandclass/" ) ) { continue ; } if ( relativePath . endsWith ( ".test.ts" ) || relativePath . endsWith ( "index.ts" ) ) { continue ; } ts . forEachChild ( sourceFile , ( node ) => { if ( ts . isClassDeclaration ( node ) && node . name && node . name . text . endsWith ( "CC" ) ) { let ccId ; if ( node . decorators && node . decorators . length > 0 ) { for ( const decorator of node . decorators ) { ccId = getCommandClassFromDecorator ( sourceFile , decorator ) ; if ( ccId != undefined ) break ; } } if ( ccId == undefined ) { if ( ! relativePath . includes ( "/manufacturerProprietary/" ) ) { const location = ts . getLineAndCharacterOfPosition ( sourceFile , node . getStart ( sourceFile , false ) ) ; reportProblem ( { severity : "warn" , filename : relativePath , line : location . line + 1 , message : `Could not determine defined CC for ${ node . name . text }!` } ) ; } return ; } if ( ! relativePath . endsWith ( "CC.ts" ) ) { hasError = true ; reportProblem ( { severity : "error" , filename : relativePath , message : `Files containing CC implementations MUST end with "CC.ts"!` } ) ; } for ( const member of node . members ) { if ( ts . isMethodDeclaration ( member ) && member . name . getText ( sourceFile ) === "determineRequiredCCInterviews" ) { const location = ts . getLineAndCharacterOfPosition ( sourceFile , member . getStart ( sourceFile , false ) ) ; try { const requiredCCs = getRequiredInterviewCCsFromMethod ( sourceFile , member ) ; if ( ! requiredCCs ) { throw new Error ( `Could not determine required CC interviews for ${ node . name . text }!` ) ; } else if ( ! applicationCCs . includes ( ccId ) ) { const requiredApplicationCCs = requiredCCs . filter ( ( cc ) => applicationCCs . includes ( cc ) ) ; if ( requiredApplicationCCs . length > 0 ) { throw new Error ( `Interview procedure of the non-application CC ${ getCCName ( ccId ) } must not depend on application CCs, but depends on the CC${ requiredApplicationCCs . length > 1 ? "s" : "" } ${ requiredApplicationCCs . map ( ( cc ) => getCCName ( cc ) ) . join ( ", " ) }!` ) ; } } } catch ( e ) { hasError = true ; reportProblem ( { severity : "error" , filename : relativePath , line : location . line + 1 , message : e . message } ) ; } } } } } ) ; } if ( hasError ) { return Promise . reject ( new Error ( "Linting the CC interview was not successful! See log output for details." ) ) ; } else { return Promise . resolve ( ) ; } } if ( require . main === module ) lintCCInterview ( ) . then ( ( ) => process . exit ( 0 ) ) . catch ( ( ) => process . exit ( 1 ) ) ;