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*\(|(? { 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), ), ];