import { applicationCCs , CommandClasses } from "@zwave-js/core" ; import { getCommandClassFromClassDeclaration , loadTSConfig , projectRoot , reportProblem , } from "@zwave-js/maintenance" ; import { blue , green } from "ansi-colors" ; import * as path from "path" ; import ts from "typescript" ; const whitelistedCCs = [ CommandClasses . Configuration , CommandClasses [ "Firmware Update Meta Data" ] , ] ; function isCallToPersistValues ( node ) { if ( ts . isExpressionStatement ( node ) && ts . isCallExpression ( node . expression ) && ts . isPropertyAccessExpression ( node . expression . expression ) ) { const expr = node . expression . expression ; return ( expr . expression . kind === ts . SyntaxKind . ThisKeyword && expr . name . text === "persistValues" ) ; } return false ; } function isCheckForDeserializationOptions ( node ) { return ( ts . isIfStatement ( node ) && ts . isCallExpression ( node . expression ) && ts . isIdentifier ( node . expression . expression ) && node . expression . expression . text === "gotDeserializationOptions" && ts . isBlock ( node . thenStatement ) ) ; } function getCommandClassFromClassOrParent ( checker , sourceFile , node ) { while ( node ) { const ccId = getCommandClassFromClassDeclaration ( sourceFile , node ) ; if ( ccId != undefined ) return ccId ; if ( ! node . heritageClauses ) return ; const parentTypeClause = node . heritageClauses . find ( ( c ) => c . token === ts . SyntaxKind . ExtendsKeyword && c . types . length === 1 ) ; if ( ! parentTypeClause ) return ; const symbol = checker . getSymbolAtLocation ( parentTypeClause . types [ 0 ] . expression ) ; if ( ! symbol || ! symbol . valueDeclaration || ! ts . isClassDeclaration ( symbol . valueDeclaration ) ) { return ; } node = symbol . valueDeclaration ; } } export function lintCCConstructors ( ) { 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" ) || relativePath . includes ( "/manufacturerProprietary/" ) ) { continue ; } ts . forEachChild ( sourceFile , ( node ) => { if ( ts . isClassDeclaration ( node ) && node . name && node . name . text . includes ( "CC" ) && node . name . text . endsWith ( "Report" ) ) { const classLocation = ts . getLineAndCharacterOfPosition ( sourceFile , node . getStart ( sourceFile , false ) ) ; const fail = ( reason , severity = "error" ) => { if ( severity === "error" ) hasError = true ; reportProblem ( { severity , filename : relativePath , line : classLocation . line + 1 , message : reason } ) ; } ; const constructor = node . members . find ( ( member ) : member is ts . ConstructorDeclaration => ts . isConstructorDeclaration ( member ) ) ; if ( ! constructor || ! constructor . body ) { if ( node . name . text . endsWith ( "Report" ) ) { fail ( `The CC report class ${ node . name . text } has no constructor!` ) ; } return ; } const ccId = getCommandClassFromClassOrParent ( program . getTypeChecker ( ) , sourceFile , node ) ; if ( ccId != undefined && whitelistedCCs . includes ( ccId ) ) return ; const isApplicationCC = ccId != undefined && applicationCCs . includes ( ccId ) ; const severity = isApplicationCC ? "error" : "warn" ; const hasCallToPersistValuesInConstructorBody = ! ! constructor . body . statements . find ( isCallToPersistValues ) ; const checkForDeserializationOptions = constructor . body . statements . find ( isCheckForDeserializationOptions ) ; const hasCallToPersistValuesInCheck = checkForDeserializationOptions && ! ! checkForDeserializationOptions . thenStatement . statements . find ( isCallToPersistValues ) ; const hasMergePartialCCsImplementation = ! ! node . members . find ( ( member ) => ts . isMethodDeclaration ( member ) && ts . isIdentifier ( member . name ) && member . name . text === "mergePartialCCs" ) ; const hasNoCCValuesComment = node . members . some ( ( member ) => { const sourceText = sourceFile . getText ( ) ; const comments = ts . getLeadingCommentRanges ( sourceText , member . getFullStart ( ) ) ; if ( ! comments ) return false ; const commentTexts = comments . map ( ( c ) => sourceText . slice ( c . pos , c . end ) ) ; return commentTexts . some ( ( c ) => c . trim ( ) . startsWith ( "// @noCCValues" ) ) ; } ) ; if ( hasMergePartialCCsImplementation || hasNoCCValuesComment ) { return ; } if ( hasCallToPersistValuesInConstructorBody ) return ; if ( hasCallToPersistValuesInCheck ) return ; fail ( `The${ isApplicationCC ? " application" : "" } CC report class ${ blue ( node . name . text ) } ${ severity === "error" ? "needs" : "might need" } a call to ${ blue ( "persistValues()" ) } in the constructor or an implementation for ${ blue ( "mergePartialCCs()" ) }
If this is a false-positive, consider suppressing this error with a ${ green ( "// @noCCValues" ) } comment.` , severity ) ; return ; } } ) ; } if ( hasError ) { return Promise . reject ( new Error ( "Linting the CC constructors was not successful! See log output for details." ) ) ; } else { return Promise . resolve ( ) ; } } if ( require . main === module ) lintCCConstructors ( ) . then ( ( ) => process . exit ( 0 ) ) . catch ( ( ) => process . exit ( 1 ) ) ;