249 lines
7.7 KiB
TypeScript
249 lines
7.7 KiB
TypeScript
// /*!
|
|
// * This scripts checks the constructors of Command Classes for common errors.
|
|
// * Since v3.7.0, all report-type application CCs must call persistValues in their constructor,
|
|
// * so values can be mapped from the root endpoint to endpoint 1.
|
|
// */
|
|
|
|
// 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";
|
|
|
|
// // Configure which CCs are excluded from this check
|
|
// const whitelistedCCs: CommandClasses[] = [
|
|
// // Configuration CC has a different way of storing its values
|
|
// CommandClasses.Configuration,
|
|
// // Firmware Update CC is handled entirely on a request base
|
|
// CommandClasses["Firmware Update Meta Data"],
|
|
// ];
|
|
|
|
// function isCallToPersistValues(node: ts.Node): node is ts.CallExpression {
|
|
// 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: ts.Node,
|
|
// ): node is ts.IfStatement & { thenStatement: ts.Block } {
|
|
// 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: ts.TypeChecker,
|
|
// sourceFile: ts.SourceFile,
|
|
// node: ts.ClassDeclaration,
|
|
// ): CommandClasses | undefined {
|
|
// 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(): Promise<void> {
|
|
// // Create a Program to represent the project, then pull out the
|
|
// // source file to parse its AST.
|
|
|
|
// const tsConfig = loadTSConfig("zwave-js");
|
|
// 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 commandclass dir
|
|
// if (!relativePath.includes("/commandclass/")) {
|
|
// continue;
|
|
// }
|
|
// // Ignore test files, compiled files and the index
|
|
// if (
|
|
// relativePath.endsWith(".test.ts") ||
|
|
// relativePath.endsWith("index.ts") ||
|
|
// // and the manufacturer proprietary implementations
|
|
// relativePath.includes("/manufacturerProprietary/")
|
|
// ) {
|
|
// continue;
|
|
// }
|
|
|
|
// // Visit each CC class and see if it overwrites determineRequiredCCInterviews
|
|
// ts.forEachChild(sourceFile, (node) => {
|
|
// // Only look at CC class declarations ending with "Report"
|
|
// 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: string,
|
|
// severity: "error" | "warn" = "error",
|
|
// ) => {
|
|
// if (severity === "error") hasError = true;
|
|
// reportProblem({
|
|
// severity,
|
|
// filename: relativePath,
|
|
// line: classLocation.line + 1,
|
|
// message: reason,
|
|
// });
|
|
// };
|
|
|
|
// // Only look at the constructor
|
|
// 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,
|
|
// );
|
|
// // Ignore whitelisted CCs
|
|
// if (ccId != undefined && whitelistedCCs.includes(ccId)) return;
|
|
// // Error only for Application CCs
|
|
// const isApplicationCC =
|
|
// ccId != undefined && applicationCCs.includes(ccId);
|
|
// const severity = isApplicationCC ? "error" : "warn";
|
|
|
|
// // persistValues must be called:
|
|
// // a) in CCs with name *Report (mandatory):
|
|
// // a1) either in the constructor body
|
|
// // a2) or inside an if statement with argument gotDeserializationOptions
|
|
// // b) except if:
|
|
// // b1) there is a method definition for mergePartialCCs
|
|
// // b2) there is a @noCCValues comment
|
|
|
|
// 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"),
|
|
// );
|
|
// });
|
|
|
|
// // b)
|
|
// if (hasMergePartialCCsImplementation || hasNoCCValuesComment) {
|
|
// return;
|
|
// }
|
|
|
|
// // a)
|
|
// 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 (esMain(import.meta))
|
|
// lintCCConstructors()
|
|
// .then(() => process.exit(0))
|
|
// .catch(() => process.exit(1));
|