inventoresed
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
182 lines (170 loc) • 5.27 kB
text/typescript
/*!
* 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));