node-zwave-js/packages/cc/maintenance/lintCCConstructor.ts

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));