Replace regex prop counter with TypeScript compiler API
Uses ts.createProgram to parse real AST instead of regex + brace-depth state machine. Immune to comments, strings, and complex type syntax. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -9,11 +9,14 @@
|
|||||||
* Only scans component files (not hooks, adapters, etc.) and only
|
* Only scans component files (not hooks, adapters, etc.) and only
|
||||||
* counts properties declared directly in *Props interfaces — inherited
|
* counts properties declared directly in *Props interfaces — inherited
|
||||||
* or extended HTML attributes are not counted.
|
* or extended HTML attributes are not counted.
|
||||||
|
*
|
||||||
|
* Uses the TypeScript compiler API for accurate AST-based counting,
|
||||||
|
* immune to comments, strings, and complex type syntax.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { execSync } from "node:child_process";
|
import { execSync } from "node:child_process";
|
||||||
import { readFileSync } from "node:fs";
|
|
||||||
import { relative } from "node:path";
|
import { relative } from "node:path";
|
||||||
|
import ts from "typescript";
|
||||||
|
|
||||||
const MAX_PROPS = 8;
|
const MAX_PROPS = 8;
|
||||||
|
|
||||||
@@ -25,66 +28,38 @@ const files = execSync(
|
|||||||
.split("\n")
|
.split("\n")
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
|
|
||||||
|
const program = ts.createProgram(files, {
|
||||||
|
target: ts.ScriptTarget.ESNext,
|
||||||
|
module: ts.ModuleKind.ESNext,
|
||||||
|
jsx: ts.JsxEmit.ReactJSX,
|
||||||
|
strict: true,
|
||||||
|
noEmit: true,
|
||||||
|
skipLibCheck: true,
|
||||||
|
});
|
||||||
|
|
||||||
let errors = 0;
|
let errors = 0;
|
||||||
|
|
||||||
const propsRegex = /^(?:export\s+)?interface\s+(\w+Props)\s*\{/;
|
|
||||||
|
|
||||||
for (const file of files) {
|
for (const file of files) {
|
||||||
const content = readFileSync(file, "utf-8");
|
const sourceFile = program.getSourceFile(file);
|
||||||
const lines = content.split("\n");
|
if (!sourceFile) continue;
|
||||||
|
|
||||||
let inInterface = false;
|
ts.forEachChild(sourceFile, (node) => {
|
||||||
let interfaceName = "";
|
if (!ts.isInterfaceDeclaration(node)) return;
|
||||||
let braceDepth = 0;
|
if (!node.name.text.endsWith("Props")) return;
|
||||||
let parenDepth = 0;
|
|
||||||
let propCount = 0;
|
|
||||||
let startLine = 0;
|
|
||||||
|
|
||||||
for (let i = 0; i < lines.length; i++) {
|
const propCount = node.members.filter((m) =>
|
||||||
const line = lines[i];
|
ts.isPropertySignature(m),
|
||||||
|
).length;
|
||||||
|
|
||||||
if (!inInterface) {
|
|
||||||
const match = propsRegex.exec(line);
|
|
||||||
if (match) {
|
|
||||||
inInterface = true;
|
|
||||||
interfaceName = match[1];
|
|
||||||
braceDepth = 0;
|
|
||||||
parenDepth = 0;
|
|
||||||
propCount = 0;
|
|
||||||
startLine = i + 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (inInterface) {
|
|
||||||
for (const ch of line) {
|
|
||||||
if (ch === "{") braceDepth++;
|
|
||||||
if (ch === "}") braceDepth--;
|
|
||||||
if (ch === "(") parenDepth++;
|
|
||||||
if (ch === ")") parenDepth--;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Count prop lines at brace depth 1 and not inside function params:
|
|
||||||
// Matches " propName?: type" and " readonly propName: type"
|
|
||||||
if (
|
|
||||||
braceDepth === 1 &&
|
|
||||||
parenDepth === 0 &&
|
|
||||||
/^\s+(?:readonly\s+)?\w+\??\s*:/.test(line)
|
|
||||||
) {
|
|
||||||
propCount++;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (braceDepth === 0) {
|
|
||||||
if (propCount > MAX_PROPS) {
|
if (propCount > MAX_PROPS) {
|
||||||
const rel = relative(process.cwd(), file);
|
const rel = relative(process.cwd(), file);
|
||||||
|
const { line } = sourceFile.getLineAndCharacterOfPosition(node.name.pos);
|
||||||
console.error(
|
console.error(
|
||||||
`${rel}:${startLine}: ${interfaceName} has ${propCount} props (max ${MAX_PROPS})`,
|
`${rel}:${line + 1}: ${node.name.text} has ${propCount} props (max ${MAX_PROPS})`,
|
||||||
);
|
);
|
||||||
errors++;
|
errors++;
|
||||||
}
|
}
|
||||||
inInterface = false;
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errors > 0) {
|
if (errors > 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user