UNPKG

inventoresed

Version:

Z-Wave driver written entirely in JavaScript/TypeScript

182 lines (170 loc) 5.27 kB
/*! * This scripts checks if there is anything wrong with the defined CC interviews. * Since v3.0.0, non-application CCs may no longer depend on application CCs because * the interview for application CCs on the root endpoint is deferred */ 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: ts.SourceFile, method: ts.MethodDeclaration, ): CommandClasses[] | undefined { 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; // TODO: Check if that includes the super call const ret = elements .map((e) => expressionToCommandClass(sourceFile, e)) .filter((cc) => cc != undefined) as CommandClasses[]; return ret; } export function lintCCInterview(): Promise<void> { // Create a Program to represent the project, then pull out the // source file to parse its AST. const tsConfig = loadTSConfig("cc"); const program = ts.createProgram(tsConfig.fileNames, tsConfig.options); let hasError = false; // Scan all source files for (const sourceFile of program.getSourceFiles()) { const relativePath = path .relative(projectRoot, sourceFile.fileName) .replace(/\\/g, "/"); // Only look at files in this package if (relativePath.startsWith("..")) continue; // Only look at the lib dir if (!relativePath.includes("/src/lib/")) { continue; } // Ignore test files and the index if ( relativePath.endsWith(".test.ts") || relativePath.endsWith("index.ts") ) { continue; } // Visit each CC class and see if it overwrites determineRequiredCCInterviews ts.forEachChild(sourceFile, (node) => { // Only look at class declarations ending with "CC" that have a commandClass decorator if ( ts.isClassDeclaration(node) && node.name && node.name.text.endsWith("CC") ) { let ccId: CommandClasses | undefined; 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; } // Ensure the filename ends with CC.ts - otherwise CommandClass.from won't find it if (!relativePath.endsWith("CC.ts")) { hasError = true; reportProblem({ severity: "error", filename: relativePath, message: `Files containing CC implementations MUST end with "CC.ts"!`, }); } // Only look at implementations of determineRequiredCCInterviews 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)) { // This is a non-application CC const requiredApplicationCCs = requiredCCs.filter((cc) => applicationCCs.includes(cc), ); if (requiredApplicationCCs.length > 0) { // that depends on an application CC 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: any) { 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));