scripts/test-quality-audit-model.ts
Rama main · réplica de lectura del repositorio original.
import { lineColumnAt } from "./line-column.ts";
import { blankSpans, skipCommentOrString } from "./typescript-lex.ts";
export type TestQualityFinding = {
column: number;
line: number;
message: string;
path: string;
};
// Count bare `expect(`/`assert*(` as well as project assertion helpers that
// wrap them, otherwise tests that only assert through helpers look empty.
const EXPECT_PATTERN = /\bexpect\w*\s*\(|\bassert\w*\s*\(/;
const WEAK_ASSERTION_PATTERNS: { message: string; pattern: RegExp }[] = [
{
message:
"presence-only assertion; prefer checking the value, shape, or invariant",
pattern: /expect\s*\([^;]+?\)\s*\.\s*toBe(?:Defined|Undefined)\s*\(/g,
},
{
message:
"truthiness assertion; prefer an exact value or contract-specific matcher",
pattern: /expect\s*\([^;]+?\)\s*\.\s*toBe(?:Truthy|Falsy)\s*\(/g,
},
{
message:
"compound boolean assertion; split into contract-specific assertions",
pattern:
/expect\s*\([^)]*(?:&&|\|\||===|!==|>=|<=|>|<)[^)]*\)\s*\.\s*toBe\s*\(\s*(?:true|false)\s*\)/g,
},
];
const testBlockEnd = (content: string, start: number): number | null => {
let depth = 0;
let seenOpen = false;
for (let index = start; index < content.length; index += 1) {
const skipped = skipCommentOrString(content, index);
if (skipped !== index) {
index = skipped - 1;
continue;
}
const char = content[index];
if (char === "(") {
depth += 1;
seenOpen = true;
}
if (char === ")") depth -= 1;
if (seenOpen && depth === 0) return index + 1;
}
return null;
};
const testBlockRanges = (content: string): { end: number; start: number }[] => {
const ranges: { end: number; start: number }[] = [];
// Match test declarations, but not predicate calls such as regex.test(value).
const startPattern = /\bDeno\.test\s*\(|(?<![.\w$])(?:test|it)\s*\(/g;
const code = blankSpans(content, true);
let match = startPattern.exec(code);
while (match !== null) {
const start = match.index;
const end = testBlockEnd(content, start);
if (end !== null) ranges.push({ end, start });
match = startPattern.exec(code);
}
return ranges;
};
const findAssertionlessTests = (
path: string,
content: string,
): TestQualityFinding[] => {
const code = blankSpans(content, true);
return testBlockRanges(content)
.filter(({ end, start }) => !EXPECT_PATTERN.test(code.slice(start, end)))
.map(({ start }) => ({
...lineColumnAt(content, start),
message: "test has no visible assertion",
path,
}));
};
/** Every `pattern` match in one file's comment-and-string-blanked code, with
* the line and column each match sits on. One scanner behind the
* weak-assertion audit and the presence-assertion gate, so the two cannot
* drift apart. */
export const findWeakPattern = (
path: string,
content: string,
{ message, pattern }: { message: string; pattern: RegExp },
): TestQualityFinding[] => {
const findings: TestQualityFinding[] = [];
const code = blankSpans(content, true);
let match = pattern.exec(code);
while (match !== null) {
findings.push({
...lineColumnAt(content, match.index),
message,
path,
});
match = pattern.exec(code);
}
return findings;
};
export const auditTestContent = (
path: string,
content: string,
): TestQualityFinding[] => [
...findAssertionlessTests(path, content),
...WEAK_ASSERTION_PATTERNS.flatMap((weak) =>
findWeakPattern(path, content, weak),
),
];