Skip to content

Commit b2b0b39

Browse files
davidagustinclaude
andcommitted
fix(ui-patterns): blank implementation expressions in generated starters
The blankFunctionBodies() generator only handled 6 named function patterns, missing implementation expressions (variable assignments with method chains like .filter/.map/.reduce), reactive wrapper bodies (computed/useMemo), and anonymous callbacks (useEffect, addEventListener). These leaked expressions caused behavioral tests to pass before users wrote any code. Added 3 new blanking categories to generate-starters.ts: - Implementation expressions → replaced with safe defaults ([], null, -1) - Reactive wrappers (computed/useMemo) → body blanked with TODO - Anonymous callbacks (useEffect, addEventListener) → body blanked Added false-positive detection test that iterates all 616+ UI patterns across 4 frameworks and flags any starters containing leaked logic. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent d958062 commit b2b0b39

6 files changed

Lines changed: 746 additions & 1875 deletions

File tree

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
/**
2+
* False positive detection for UI pattern starter code.
3+
*
4+
* When the generate-starters.ts script creates skeleton code from reference
5+
* implementations, it blanks function bodies but can miss "implementation
6+
* expressions" — variable assignments containing method chains with callbacks
7+
* (e.g. `const filtered = items.filter(f => ...)`). These leaked expressions
8+
* cause behavioral tests to pass before the user writes any code.
9+
*
10+
* This test iterates every UI pattern across all 4 frameworks and flags
11+
* starters that contain implementation logic which should have been blanked.
12+
*/
13+
import { describe, expect, it } from 'vitest';
14+
import { angularStarters } from '../frontend-drills/ui-patterns/starters/angular';
15+
import { nativeJsStarters } from '../frontend-drills/ui-patterns/starters/native-js';
16+
import { reactStarters } from '../frontend-drills/ui-patterns/starters/react';
17+
import { vueStarters } from '../frontend-drills/ui-patterns/starters/vue';
18+
import { angularTests } from '../frontend-drills/ui-patterns/tests/angular';
19+
import type { PatternTestCase } from '../frontend-drills/ui-patterns/tests/index';
20+
import { nativeJsTests } from '../frontend-drills/ui-patterns/tests/native-js';
21+
import { reactTests } from '../frontend-drills/ui-patterns/tests/react';
22+
import { vueTests } from '../frontend-drills/ui-patterns/tests/vue';
23+
24+
// ─── Implementation patterns that should be blanked in starters ───
25+
26+
/** Regexes matching array/collection method chains with arrow callbacks. */
27+
const IMPL_METHOD_CHAIN = [
28+
/\.\s*filter\s*\(\s*(?:\w+|\([^)]*\))\s*=>/,
29+
/\.\s*map\s*\(\s*(?:\w+|\([^)]*\))\s*=>/,
30+
/\.\s*reduce\s*\(\s*(?:\w+|\([^)]*\))\s*=>/,
31+
/\.\s*find\s*\(\s*(?:\w+|\([^)]*\))\s*=>/,
32+
/\.\s*findIndex\s*\(\s*(?:\w+|\([^)]*\))\s*=>/,
33+
/\.\s*some\s*\(\s*(?:\w+|\([^)]*\))\s*=>/,
34+
/\.\s*every\s*\(\s*(?:\w+|\([^)]*\))\s*=>/,
35+
/\.\s*sort\s*\(\s*(?:\w+|\([^)]*\))\s*=>/,
36+
/\.\s*flatMap\s*\(\s*(?:\w+|\([^)]*\))\s*=>/,
37+
/\.\s*forEach\s*\(\s*(?:\w+|\([^)]*\))\s*=>/,
38+
];
39+
40+
/** Check whether a starter string looks like an actual skeleton. */
41+
function isSkeleton(code: string): boolean {
42+
return /\/\/\s*(TODO|Step\s+\d|Your\s+code|Your\s+answer|Implement)/i.test(code);
43+
}
44+
45+
/**
46+
* Detect implementation expressions leaked into starter code.
47+
*
48+
* Only checks lines that are:
49+
* 1. Variable assignments (`const/let/var NAME = ...`)
50+
* 2. NOT function declarations (those are handled by blankFunctionBodies)
51+
* 3. NOT hook/state calls (useState, useRef, computed, ref, etc.)
52+
* 4. NOT simple destructuring from modules
53+
* 5. Contain method chains with arrow callbacks (.filter(f =>, .map(x =>, etc.)
54+
*
55+
* Returns array of { line, lineNumber, content } for each leak found.
56+
*/
57+
function findImplementationLeaks(starterCode: string): { lineNumber: number; content: string }[] {
58+
const lines = starterCode.split('\n');
59+
const leaks: { lineNumber: number; content: string }[] = [];
60+
61+
// Track brace depth to skip JSX/template regions
62+
let insideReturn = false;
63+
64+
// Track brace depth for skipped blocks (computed bodies, event handlers, etc.)
65+
// When a skipped line opens a block, skip all lines until the block closes.
66+
let skipBraceDepth = 0;
67+
68+
for (let i = 0; i < lines.length; i++) {
69+
const line = lines[i];
70+
const trimmed = line.trim();
71+
72+
// If inside a skipped block, track braces and skip until balanced
73+
if (skipBraceDepth > 0) {
74+
for (const ch of line) {
75+
if (ch === '{') skipBraceDepth++;
76+
if (ch === '}') skipBraceDepth--;
77+
}
78+
continue;
79+
}
80+
81+
// Detect return statement (JSX starts here — skip)
82+
if (/^\s*return\s*[({]/.test(line) || /^\s*return\s*$/.test(line)) {
83+
insideReturn = true;
84+
}
85+
86+
// Skip lines inside JSX return block
87+
if (insideReturn) continue;
88+
89+
// Only check const/let/var assignments
90+
if (!/^(const|let|var)\s+\w+\s*=/.test(trimmed)) continue;
91+
92+
// Skip lines that are function declarations (already blanked or intentionally kept)
93+
// Arrow function declarations: `const fn = (params) => {` or `const fn = param => {`
94+
if (/^(const|let|var)\s+\w+\s*=\s*(?:\([^)]*\)|\w+)\s*=>\s*\{/.test(trimmed)) continue;
95+
// Function expressions: `const fn = function(`
96+
if (/^(const|let|var)\s+\w+\s*=\s*function\s*\(/.test(trimmed)) continue;
97+
98+
// Skip hook/state calls — also skip their block bodies if they open one
99+
if (/=\s*use\w+\s*\(/.test(trimmed) || /=\s*React\.use\w+\s*\(/.test(trimmed)) {
100+
for (const ch of line) {
101+
if (ch === '{') skipBraceDepth++;
102+
if (ch === '}') skipBraceDepth--;
103+
}
104+
continue;
105+
}
106+
if (/=\s*(?:ref|reactive|computed|watch)\s*\(/.test(trimmed)) {
107+
for (const ch of line) {
108+
if (ch === '{') skipBraceDepth++;
109+
if (ch === '}') skipBraceDepth--;
110+
}
111+
continue;
112+
}
113+
114+
// Skip destructuring from modules: const { ... } = React or const [...] = useState(...)
115+
if (/^(const|let|var)\s*[[{]/.test(trimmed)) continue;
116+
117+
// Skip TODO/blanked lines
118+
if (/\/\/\s*TODO/i.test(trimmed)) continue;
119+
120+
// Check for implementation method chains with callbacks
121+
const hasImplMethodChain = IMPL_METHOD_CHAIN.some((rx) => rx.test(trimmed));
122+
123+
// Also check multi-line: if this line doesn't end with ; and continues
124+
// to the next line(s), join them and check — but stop at statement boundaries
125+
if (!hasImplMethodChain && !trimmed.endsWith(';')) {
126+
const contLines = [trimmed];
127+
for (let k = i + 1; k < Math.min(i + 5, lines.length); k++) {
128+
contLines.push(lines[k].trim());
129+
if (lines[k].trim().endsWith(';')) break;
130+
}
131+
const joined = contLines.join(' ');
132+
if (IMPL_METHOD_CHAIN.some((rx) => rx.test(joined))) {
133+
leaks.push({ lineNumber: i + 1, content: trimmed });
134+
continue;
135+
}
136+
}
137+
138+
if (hasImplMethodChain) {
139+
leaks.push({ lineNumber: i + 1, content: trimmed });
140+
}
141+
}
142+
143+
return leaks;
144+
}
145+
146+
// ─── Test data ───
147+
148+
interface FrameworkTestData {
149+
name: string;
150+
starters: Record<string, string>;
151+
tests: Record<string, PatternTestCase[]>;
152+
}
153+
154+
const frameworks: FrameworkTestData[] = [
155+
{ name: 'React', starters: reactStarters, tests: reactTests },
156+
{ name: 'Vue', starters: vueStarters, tests: vueTests },
157+
{ name: 'Angular', starters: angularStarters, tests: angularTests },
158+
{ name: 'Native JS', starters: nativeJsStarters, tests: nativeJsTests },
159+
];
160+
161+
// ─── Tests ───
162+
163+
describe('UI Pattern starter code — false positive detection', () => {
164+
for (const fw of frameworks) {
165+
describe(fw.name, () => {
166+
const patternIds = Object.keys(fw.starters);
167+
168+
it(`should have starters for all patterns with tests`, () => {
169+
const testPatternIds = Object.keys(fw.tests);
170+
const missingStarters = testPatternIds.filter((id) => !fw.starters[id]);
171+
// Not a hard failure — some patterns may not have starters yet
172+
if (missingStarters.length > 0) {
173+
console.warn(
174+
`${fw.name}: ${missingStarters.length} patterns with tests but no starter: ${missingStarters.slice(0, 5).join(', ')}...`,
175+
);
176+
}
177+
});
178+
179+
for (const patternId of patternIds) {
180+
const starter = fw.starters[patternId];
181+
const tests = fw.tests[patternId];
182+
183+
// Only check patterns that have both a starter AND behavioral tests
184+
if (!starter || !tests || tests.length === 0) continue;
185+
186+
// Only check starters that pass the isSkeleton gate (others are ignored by the page)
187+
if (!isSkeleton(starter)) continue;
188+
189+
it(`${patternId} — starter should not contain implementation expressions`, () => {
190+
const leaks = findImplementationLeaks(starter);
191+
192+
if (leaks.length > 0) {
193+
const details = leaks.map((l) => ` line ${l.lineNumber}: ${l.content}`).join('\n');
194+
expect.soft(leaks.length, `Implementation leaked in ${patternId}:\n${details}`).toBe(0);
195+
}
196+
});
197+
}
198+
});
199+
}
200+
});

0 commit comments

Comments
 (0)