fix(scripts/check-test-imports): filter TS keywords + local declarations
The check-test-imports drift guard was emitting too many false positives
to be safely integrated into npm run lint (per CLAUDE.md: "NOT integrated
into npm run lint by default — too broad"). Two big classes of FP:
1) TypeScript keywords + utility types treated as undeclared (any, type,
ReturnType, Partial, Record, never, unknown, etc.) — added to the
JS_KEYWORDS set since the script doesn't otherwise distinguish JS
from TS.
2) Identifiers declared locally in the file (function declarations,
const/let/var declarations, destructured patterns, function/arrow
parameters, catch params, class names, type/interface/enum names) —
added a new collectLocalDeclarations() pass that regex-scans these
patterns and feeds the results into the filter chain.
After this patch the script no longer flags makeMockTUI / loader / tui
(local lets), `ReturnType<...>` (TS utility), or `any` (TS keyword) on
the canonical TUI test files. It still flags type-only imports
(`import type { Foo }` lines) and object-literal property names
(`{ recursive: true }`) — those remain as known FP classes documented
in the file's header for a future TS-parser-based pass.
Self-test 5/5 passes. Not yet integrating into npm run lint pending
further FP reduction; see filed self-feedback for the broader
integration plan.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
56e8ec6c53
commit
e8bbb477e6
1 changed files with 150 additions and 1 deletions
|
|
@ -185,6 +185,55 @@ const JS_KEYWORDS = new Set([
|
|||
"target",
|
||||
"from",
|
||||
"as",
|
||||
// TypeScript keywords / modifiers
|
||||
"any",
|
||||
"unknown",
|
||||
"never",
|
||||
"undefined",
|
||||
"null",
|
||||
"true",
|
||||
"false",
|
||||
"type",
|
||||
"namespace",
|
||||
"module",
|
||||
"declare",
|
||||
"abstract",
|
||||
"readonly",
|
||||
"is",
|
||||
"keyof",
|
||||
"infer",
|
||||
"satisfies",
|
||||
"out",
|
||||
"override",
|
||||
// TypeScript built-in utility types
|
||||
"Partial",
|
||||
"Required",
|
||||
"Readonly",
|
||||
"Pick",
|
||||
"Omit",
|
||||
"Record",
|
||||
"Exclude",
|
||||
"Extract",
|
||||
"NonNullable",
|
||||
"Parameters",
|
||||
"ConstructorParameters",
|
||||
"ReturnType",
|
||||
"InstanceType",
|
||||
"ThisParameterType",
|
||||
"OmitThisParameter",
|
||||
"ThisType",
|
||||
"Uppercase",
|
||||
"Lowercase",
|
||||
"Capitalize",
|
||||
"Uncapitalize",
|
||||
"Awaited",
|
||||
// Primitive type names used as TS annotations
|
||||
"string",
|
||||
"number",
|
||||
"boolean",
|
||||
"bigint",
|
||||
"symbol",
|
||||
"object",
|
||||
]);
|
||||
|
||||
// Common test framework globals that are always available in vitest/jest
|
||||
|
|
@ -289,6 +338,104 @@ function parseImports(source) {
|
|||
return { named, namespace };
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect identifiers declared LOCALLY in the file (functions, var/let/const,
|
||||
* destructured patterns, function/arrow parameters). These should not be
|
||||
* flagged as "undeclared" — they are declared, just not via an import. Used
|
||||
* to suppress false positives where the file defines its own helpers/locals
|
||||
* inside the test body.
|
||||
*/
|
||||
function collectLocalDeclarations(source) {
|
||||
const decls = new Set();
|
||||
// Strip strings and comments first so we don't pick up identifiers inside
|
||||
// quoted text (e.g. assertion strings, template literals).
|
||||
const stripped = source
|
||||
.replace(/'[^']*'/g, "'_'")
|
||||
.replace(/"[^"]*"/g, '"_"')
|
||||
.replace(/`[^`]*`/g, "``_`")
|
||||
.replace(/\/\/[^\n]*/g, "")
|
||||
.replace(/\/\*[\s\S]*?\*\//g, "");
|
||||
|
||||
// `function NAME(` — function declarations (named + async + generator)
|
||||
for (const m of stripped.matchAll(
|
||||
/\b(?:async\s+)?function\s*\*?\s*([a-zA-Z_$][\w$]*)\s*\(/g,
|
||||
)) {
|
||||
decls.add(m[1]);
|
||||
}
|
||||
// `class NAME` — class declarations
|
||||
for (const m of stripped.matchAll(/\bclass\s+([a-zA-Z_$][\w$]*)\b/g)) {
|
||||
decls.add(m[1]);
|
||||
}
|
||||
// `const/let/var NAME = ...` — simple single-name declarations
|
||||
for (const m of stripped.matchAll(
|
||||
/\b(?:const|let|var)\s+([a-zA-Z_$][\w$]*)\b/g,
|
||||
)) {
|
||||
decls.add(m[1]);
|
||||
}
|
||||
// `const/let/var { a, b: c, ...d } = ...` — object destructuring
|
||||
for (const m of stripped.matchAll(
|
||||
/\b(?:const|let|var)\s*\{\s*([^}]+)\s*\}\s*=/g,
|
||||
)) {
|
||||
const inner = m[1];
|
||||
for (const part of inner.split(",")) {
|
||||
// Patterns: `name`, `name: alias`, `name = default`, `...rest`
|
||||
const cleaned = part
|
||||
.trim()
|
||||
.replace(/^\.\.\./, "")
|
||||
.split(":")
|
||||
.pop()
|
||||
.split("=")[0]
|
||||
.trim();
|
||||
const idMatch = cleaned.match(/^([a-zA-Z_$][\w$]*)$/);
|
||||
if (idMatch) decls.add(idMatch[1]);
|
||||
}
|
||||
}
|
||||
// `const/let/var [a, b, ...c] = ...` — array destructuring
|
||||
for (const m of stripped.matchAll(
|
||||
/\b(?:const|let|var)\s*\[\s*([^\]]+)\s*\]\s*=/g,
|
||||
)) {
|
||||
const inner = m[1];
|
||||
for (const part of inner.split(",")) {
|
||||
const cleaned = part
|
||||
.trim()
|
||||
.replace(/^\.\.\./, "")
|
||||
.split("=")[0]
|
||||
.trim();
|
||||
const idMatch = cleaned.match(/^([a-zA-Z_$][\w$]*)$/);
|
||||
if (idMatch) decls.add(idMatch[1]);
|
||||
}
|
||||
}
|
||||
// `function name(a, b: T, { c, d } = {}, ...rest)` — function params.
|
||||
// Also handles arrow functions: `(a, b) => ...` and method shorthand.
|
||||
for (const m of stripped.matchAll(
|
||||
/(?:function\s*\*?\s*[a-zA-Z_$]?[\w$]*\s*|=>|\bcatch\s*)\(\s*([^)]*?)\s*\)/g,
|
||||
)) {
|
||||
const inner = m[1];
|
||||
if (!inner) continue;
|
||||
for (const part of inner.split(",")) {
|
||||
const cleaned = part
|
||||
.trim()
|
||||
.replace(/^\.\.\./, "")
|
||||
.split(":")[0]
|
||||
.split("=")[0]
|
||||
.trim();
|
||||
const idMatch = cleaned.match(/^([a-zA-Z_$][\w$]*)$/);
|
||||
if (idMatch) decls.add(idMatch[1]);
|
||||
}
|
||||
}
|
||||
// `enum NAME` — TS enum declarations
|
||||
for (const m of stripped.matchAll(/\benum\s+([a-zA-Z_$][\w$]*)\b/g)) {
|
||||
decls.add(m[1]);
|
||||
}
|
||||
// `interface NAME` / `type NAME =` — TS type declarations
|
||||
for (const m of stripped.matchAll(
|
||||
/\b(?:interface|type)\s+([a-zA-Z_$][\w$]*)\b/g,
|
||||
)) {
|
||||
decls.add(m[1]);
|
||||
}
|
||||
return decls;
|
||||
}
|
||||
|
||||
/**
|
||||
* Collect all identifier references in the source, excluding:
|
||||
* - strings, comments, template literals
|
||||
|
|
@ -338,14 +485,16 @@ function collectReferences(source) {
|
|||
function checkFile(filePath, source) {
|
||||
const imports = parseImports(source);
|
||||
const refs = collectReferences(source);
|
||||
const localDecls = collectLocalDeclarations(source);
|
||||
|
||||
const itemizedCount = imports.named.size;
|
||||
|
||||
// Collect candidate undeclared identifiers (same filtering as before)
|
||||
// Collect candidate undeclared identifiers (filtering chain)
|
||||
const undeclared = new Set();
|
||||
for (const id of refs) {
|
||||
if (imports.named.has(id)) continue;
|
||||
if (imports.namespace.has(id)) continue;
|
||||
if (localDecls.has(id)) continue;
|
||||
if (NODE_BUILTINS.has(id)) continue;
|
||||
if (NODE_MODULE_GLOBALS.has(id)) continue;
|
||||
if (JS_KEYWORDS.has(id)) continue;
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue