inventoresed
Version:
Z-Wave driver written entirely in JavaScript/TypeScript
174 lines (157 loc) • 4.62 kB
text/typescript
/*!
* This scripts checks that all CC API classes have a @noValidateArgs decorator on their methods which need one
*/
import { getCCName } from "@zwave-js/core";
import {
getCommandClassFromDecorator,
hasComment,
loadTSConfig,
projectRoot,
reportProblem,
} from "@zwave-js/maintenance";
import { blue, green } from "ansi-colors";
import * as path from "path";
import ts from "typescript";
function hasNoValidateArgsComment(
node: ts.Node,
sourceFile: ts.SourceFile,
): boolean {
return hasComment(sourceFile, node, (text) =>
text.includes("@noValidateArgs"),
);
}
function hasInternalJsDoc(node: ts.Node, sourceFile: ts.SourceFile): boolean {
return hasComment(
sourceFile,
node,
(text, kind) =>
text.includes("@internal") &&
kind === ts.SyntaxKind.MultiLineCommentTrivia,
);
}
export function lintCCValidateArgs(): 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 *CC.ts files in the lib dir
if (
!relativePath.includes("/src/lib/") ||
!relativePath.endsWith("CC.ts")
) {
continue;
}
ts.forEachChild(sourceFile, (node) => {
// Only look at class decorations that are annotated with @API and don't have a // @noValidateArgs comment
if (!ts.isClassDeclaration(node)) return;
if (!node.decorators) return;
if (hasNoValidateArgsComment(node, sourceFile)) return;
const cc = node.decorators
.filter(
(d) =>
ts.isCallExpression(d.expression) &&
ts.isIdentifier(d.expression.expression) &&
d.expression.expression.text === "API",
)
.map((d) => getCommandClassFromDecorator(sourceFile, d))
.find((cc) => cc != undefined);
if (!cc) return;
// Check all public method declarations with arguments that are not called supportsCommand
const methods = node.members
.filter(
(m): m is ts.MethodDeclaration =>
ts.isMethodDeclaration(m) &&
// Ignore overload declarations
!!m.body &&
m.parameters.length > 0,
)
.filter(
(m) =>
ts.isIdentifier(m.name) &&
m.name.text !== "supportsCommand" &&
m.name.text !== "isSetValueOptimistic",
)
.filter((m) =>
m.modifiers?.some(
(mod) => mod.kind === ts.SyntaxKind.PublicKeyword,
),
)
// Ignore methods marked with @internal
.filter((m) => !hasInternalJsDoc(m, sourceFile));
if (methods.length === 0) {
// ignore empty classes
return;
} else {
for (const method of methods) {
const methodLocation = ts.getLineAndCharacterOfPosition(
sourceFile,
method.getStart(sourceFile, false),
);
const fail = (
reason: string,
severity: "error" | "warn" = "error",
) => {
if (severity === "error") hasError = true;
reportProblem({
severity,
filename: relativePath,
line: methodLocation.line + 1,
message: reason,
});
};
if (hasNoValidateArgsComment(method, sourceFile)) {
// ignored
return;
} else {
const hasValidateArgsDecorator =
!!method.decorators?.some(
(d) =>
ts.isCallExpression(d.expression) &&
ts.isIdentifier(d.expression.expression) &&
d.expression.expression.text ===
"validateArgs",
);
if (!hasValidateArgsDecorator) {
fail(
`The API class for the ${blue(
getCCName(cc),
)} CC is missing the ${blue(
"@validateArgs()",
)} decorator on the ${blue(
(method.name as ts.Identifier).text,
)} method.
Public CC API methods should have argument validation to catch user errors.
If this is a false-positive, consider suppressing this error with a ${green(
"// @noValidateArgs",
)} comment before the method implementation.`,
"error",
);
}
}
}
}
});
}
if (hasError) {
return Promise.reject(
new Error(
"Linting the CC API method validations was not successful! See log output for details.",
),
);
} else {
return Promise.resolve();
}
}
if (require.main === module) {
lintCCValidateArgs()
.then(() => process.exit(0))
.catch(() => process.exit(1));
}