44 * When the generate-starters.ts script creates skeleton code from reference
55 * implementations, it blanks function bodies but can miss "implementation
66 * 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.
7+ * (e.g. `const filtered = items.filter(f => ...)`), ternary logic, Math
8+ * operations, chained string methods, Object.keys/values/entries chains,
9+ * unblanked function bodies, and inline callbacks with implementation.
10+ * These leaked expressions cause behavioral tests to pass before the user
11+ * writes any code.
912 *
1013 * This test iterates every UI pattern across all 4 frameworks and flags
1114 * starters that contain implementation logic which should have been blanked.
@@ -37,6 +40,21 @@ const IMPL_METHOD_CHAIN = [
3740 / \. \s * f o r E a c h \s * \( \s * (?: \w + | \( [ ^ ) ] * \) ) \s * = > / ,
3841] ;
3942
43+ /** Ternary expressions with comparison logic (not simple fallbacks). */
44+ const IMPL_TERNARY =
45+ / \? [ ^ : ] * (?: [ < > = ! ] = | \. t e s t \( | \. i n c l u d e s \( | \. m a t c h \( | \. l e n g t h \s * [ < > = ! ] | M a t h \. | & & | \| \| ) .* : / ;
46+
47+ /** Math.* operations (excluding Math.random alone). */
48+ const IMPL_MATH = / M a t h \. ( f l o o r | c e i l | r o u n d | m a x | m i n | a b s | p o w | s q r t | s i g n | t r u n c ) \s * \( / ;
49+
50+ /** Chained string/array methods (2+ calls). */
51+ const IMPL_STRING_CHAIN =
52+ / \. \s * (?: r e p l a c e | s p l i t | s l i c e | s u b s t r i n g | c h a r A t | t r i m | p a d S t a r t | p a d E n d ) \s * \( [ ^ ) ] * \) \s * \. \s * (?: r e p l a c e | s p l i t | j o i n | s l i c e | s u b s t r i n g | c h a r A t | t r i m | p a d S t a r t | p a d E n d | t o U p p e r C a s e | t o L o w e r C a s e | m a p | f i l t e r ) \s * \( / ;
53+
54+ /** Object.keys/values/entries with callbacks. */
55+ const IMPL_OBJECT_CHAIN =
56+ / O b j e c t \. \s * (?: k e y s | v a l u e s | e n t r i e s ) \s * \( [ ^ ) ] * \) \s * \. \s * (?: m a p | f i l t e r | r e d u c e | f o r E a c h | s o m e | e v e r y | f i n d ) \s * \( / ;
57+
4058/** Check whether a starter string looks like an actual skeleton. */
4159function isSkeleton ( code : string ) : boolean {
4260 return / \/ \/ \s * ( T O D O | S t e p \s + \d | Y o u r \s + c o d e | Y o u r \s + a n s w e r | I m p l e m e n t ) / i. test ( code ) ;
@@ -45,18 +63,31 @@ function isSkeleton(code: string): boolean {
4563/**
4664 * Detect implementation expressions leaked into starter code.
4765 *
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.)
66+ * Checks for:
67+ * 1. Variable assignments with method chains + arrow callbacks
68+ * 2. Ternary expressions with comparison logic
69+ * 3. Math.* operations combined with arithmetic
70+ * 4. Chained string/array methods (2+ calls)
71+ * 5. Object.keys/values/entries with callbacks
72+ * 6. Unblanked function/arrow bodies (no TODO comment)
73+ * 7. Single-line and multi-line callbacks with implementation
74+ *
75+ * Skips:
76+ * - Lines inside JSX return blocks
77+ * - Hook/reactive declarations
78+ * - Destructuring
79+ * - Lines containing TODO comments
80+ * - Simple fallbacks (x || 'default', x ?? fallback)
81+ * - React component functions (uppercase names) and depth-0 utilities (which contain JSX, not implementation)
5482 *
55- * Returns array of { line, lineNumber, content } for each leak found.
83+ * Returns array of { lineNumber, content, category } for each leak found.
5684 */
57- function findImplementationLeaks ( starterCode : string ) : { lineNumber : number ; content : string } [ ] {
85+ function findImplementationLeaks (
86+ starterCode : string ,
87+ frameworkName ?: string ,
88+ ) : { lineNumber : number ; content : string ; category : string } [ ] {
5889 const lines = starterCode . split ( '\n' ) ;
59- const leaks : { lineNumber : number ; content : string } [ ] = [ ] ;
90+ const leaks : { lineNumber : number ; content : string ; category : string } [ ] = [ ] ;
6091
6192 // Track brace depth to skip JSX/template regions
6293 let insideReturn = false ;
@@ -65,10 +96,19 @@ function findImplementationLeaks(starterCode: string): { lineNumber: number; con
6596 // When a skipped line opens a block, skip all lines until the block closes.
6697 let skipBraceDepth = 0 ;
6798
99+ // Track overall brace depth to detect if we're inside the main component/function
100+ let overallBraceDepth = 0 ;
101+
68102 for ( let i = 0 ; i < lines . length ; i ++ ) {
69103 const line = lines [ i ] ;
70104 const trimmed = line . trim ( ) ;
71105
106+ // Track overall brace depth
107+ for ( const ch of line ) {
108+ if ( ch === '{' ) overallBraceDepth ++ ;
109+ if ( ch === '}' ) overallBraceDepth -- ;
110+ }
111+
72112 // If inside a skipped block, track braces and skip until balanced
73113 if ( skipBraceDepth > 0 ) {
74114 for ( const ch of line ) {
@@ -86,57 +126,228 @@ function findImplementationLeaks(starterCode: string): { lineNumber: number; con
86126 // Skip lines inside JSX return block
87127 if ( insideReturn ) continue ;
88128
89- // Only check const/let/var assignments
90- if ( ! / ^ ( c o n s t | l e t | v a r ) \s + \w + \s * = / . test ( trimmed ) ) continue ;
129+ // Skip TODO/blanked lines (applies to all checks)
130+ if ( / \/ \/ \s * T O D O / i. test ( trimmed ) ) continue ;
131+
132+ // ── Variable assignment checks ──
133+ const isVarAssignment = / ^ ( c o n s t | l e t | v a r ) \s + \w + \s * = / . test ( trimmed ) ;
91134
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 ( / ^ ( c o n s t | l e t | v a r ) \s + \w + \s * = \s * (?: \( [ ^ ) ] * \) | \w + ) \s * = > \s * \{ / . test ( trimmed ) ) continue ;
95- // Function expressions: `const fn = function(`
96- if ( / ^ ( c o n s t | l e t | v a r ) \s + \w + \s * = \s * f u n c t i o n \s * \( / . test ( trimmed ) ) continue ;
135+ if ( isVarAssignment ) {
136+ // Skip lines that are function declarations — handled by unblanked-function-body check below
137+ const isFuncDecl =
138+ / ^ ( c o n s t | l e t | v a r ) \s + \w + \s * = \s * (?: \( [ ^ ) ] * \) | \w + ) \s * = > \s * \{ / . test ( trimmed ) ||
139+ / ^ ( c o n s t | l e t | v a r ) \s + \w + \s * = \s * f u n c t i o n \s * \( / . test ( trimmed ) ;
97140
98- // Skip hook/state calls — also skip their block bodies if they open one
99- if ( / = \s * u s e \w + \s * \( / . test ( trimmed ) || / = \s * R e a c t \. u s e \w + \s * \( / . test ( trimmed ) ) {
100- for ( const ch of line ) {
101- if ( ch === '{' ) skipBraceDepth ++ ;
102- if ( ch === '}' ) skipBraceDepth -- ;
141+ // Skip hook/state calls — also skip their block bodies if they open one
142+ if ( / = \s * u s e \w + \s * \( / . test ( trimmed ) || / = \s * R e a c t \. u s e \w + \s * \( / . test ( trimmed ) ) {
143+ for ( const ch of line ) {
144+ if ( ch === '{' ) skipBraceDepth ++ ;
145+ if ( ch === '}' ) skipBraceDepth -- ;
146+ }
147+ continue ;
103148 }
104- continue ;
105- }
106- if ( / = \s * (?: r e f | r e a c t i v e | c o m p u t e d | w a t c h ) \s * \( / . test ( trimmed ) ) {
107- for ( const ch of line ) {
108- if ( ch === '{' ) skipBraceDepth ++ ;
109- if ( ch === '}' ) skipBraceDepth -- ;
149+ if ( / = \s * (?: r e f | r e a c t i v e | c o m p u t e d | w a t c h ) \s * \( / . test ( trimmed ) ) {
150+ for ( const ch of line ) {
151+ if ( ch === '{' ) skipBraceDepth ++ ;
152+ if ( ch === '}' ) skipBraceDepth -- ;
153+ }
154+ continue ;
155+ }
156+
157+ // Skip destructuring from modules: const { ... } = React or const [...] = useState(...)
158+ if ( / ^ ( c o n s t | l e t | v a r ) \s * [ [ { ] / . test ( trimmed ) ) continue ;
159+
160+ // ── Check for unblanked function/arrow bodies ──
161+ if ( isFuncDecl ) {
162+ // Skip depth-0 functions (utilities before the main component) — these should have bodies
163+ // For React, also skip component functions (uppercase names) — they contain JSX, not implementation
164+ const fnNameMatch = trimmed . match ( / ^ (?: c o n s t | l e t | v a r ) \s + ( \w + ) / ) ;
165+ const fnName = fnNameMatch ?. [ 1 ] ;
166+ const isReactComponent = frameworkName === 'React' && fnName && / ^ [ A - Z ] / . test ( fnName ) ;
167+ const isDepth0 = overallBraceDepth === 1 ; // Opening brace already counted above, so depth 1 = top-level
168+
169+ if ( isDepth0 || isReactComponent ) {
170+ continue ; // Depth-0 utility or React component — skip this check
171+ }
172+
173+ // Find matching close brace
174+ let depth = 0 ;
175+ let bodyEnd = i ;
176+ for ( let j = i ; j < lines . length ; j ++ ) {
177+ for ( const ch of lines [ j ] ) {
178+ if ( ch === '{' ) depth ++ ;
179+ if ( ch === '}' ) {
180+ depth -- ;
181+ if ( depth === 0 ) {
182+ bodyEnd = j ;
183+ break ;
184+ }
185+ }
186+ }
187+ if ( depth === 0 ) break ;
188+ }
189+
190+ // Check body for TODO
191+ const bodyLines = lines . slice ( i + 1 , bodyEnd ) ;
192+ const bodyText = bodyLines . join ( '\n' ) ;
193+ const hasTodo = / \/ \/ \s * T O D O / i. test ( bodyText ) ;
194+ const nonEmptyBodyLines = bodyLines . filter ( ( l ) => l . trim ( ) . length > 0 ) ;
195+
196+ if ( ! hasTodo && nonEmptyBodyLines . length > 1 ) {
197+ leaks . push ( { lineNumber : i + 1 , content : trimmed , category : 'unblanked-function-body' } ) ;
198+ }
199+ continue ;
200+ }
201+
202+ // ── Method chain with arrow callback (existing check) ──
203+ const hasImplMethodChain = IMPL_METHOD_CHAIN . some ( ( rx ) => rx . test ( trimmed ) ) ;
204+
205+ if ( hasImplMethodChain ) {
206+ leaks . push ( { lineNumber : i + 1 , content : trimmed , category : 'method-chain' } ) ;
207+ continue ;
208+ }
209+
210+ // ── Ternary expressions with comparison logic ──
211+ // Skip simple fallbacks like `x || 'default'` and `x ?? fallback`
212+ const afterEquals = trimmed . replace ( / ^ ( c o n s t | l e t | v a r ) \s + \w + \s * = \s * / , '' ) ;
213+ const isSimpleFallback =
214+ / ^ \w + (?: \. \w + ) * \s * (?: \| \| | & & | \? \? ) \s * [ ' " ` \w ] / . test ( afterEquals ) &&
215+ ! / \? [ ^ : ] * : / . test ( afterEquals ) ;
216+ if ( ! isSimpleFallback && IMPL_TERNARY . test ( trimmed ) ) {
217+ leaks . push ( { lineNumber : i + 1 , content : trimmed , category : 'ternary-logic' } ) ;
218+ continue ;
219+ }
220+
221+ // ── Math.* operations combined with arithmetic ──
222+ if ( IMPL_MATH . test ( trimmed ) ) {
223+ // Only flag if combined with arithmetic operators (/, *, -, +) or nested expressions
224+ const hasMathWithArithmetic =
225+ / M a t h \. \w + \s * \( [ ^ ) ] * [ / * \- + ] [ ^ ) ] * \) / . test ( trimmed ) ||
226+ / [ / * \- + ] \s * M a t h \. / . test ( trimmed ) ||
227+ / M a t h \. \w + \s * \( [ ^ ) ] * \) \s * [ / * \- + ] / . test ( trimmed ) ;
228+ if ( hasMathWithArithmetic ) {
229+ leaks . push ( { lineNumber : i + 1 , content : trimmed , category : 'math-operation' } ) ;
230+ continue ;
231+ }
232+ }
233+
234+ // ── Chained string methods (2+ calls) ──
235+ if ( IMPL_STRING_CHAIN . test ( trimmed ) ) {
236+ leaks . push ( { lineNumber : i + 1 , content : trimmed , category : 'string-chain' } ) ;
237+ continue ;
238+ }
239+
240+ // ── Object.keys/values/entries with callbacks ──
241+ if ( IMPL_OBJECT_CHAIN . test ( trimmed ) ) {
242+ leaks . push ( { lineNumber : i + 1 , content : trimmed , category : 'object-chain' } ) ;
243+ continue ;
244+ }
245+
246+ // ── Multi-line continuation check for method chains (existing) ──
247+ if ( ! trimmed . endsWith ( ';' ) ) {
248+ const contLines = [ trimmed ] ;
249+ for ( let k = i + 1 ; k < Math . min ( i + 5 , lines . length ) ; k ++ ) {
250+ contLines . push ( lines [ k ] . trim ( ) ) ;
251+ if ( lines [ k ] . trim ( ) . endsWith ( ';' ) ) break ;
252+ }
253+ const joined = contLines . join ( ' ' ) ;
254+ if ( IMPL_METHOD_CHAIN . some ( ( rx ) => rx . test ( joined ) ) ) {
255+ leaks . push ( { lineNumber : i + 1 , content : trimmed , category : 'method-chain' } ) ;
256+ continue ;
257+ }
110258 }
259+
111260 continue ;
112261 }
113262
114- // Skip destructuring from modules: const { ... } = React or const [...] = useState(...)
115- if ( / ^ ( c o n s t | l e t | v a r ) \s * [ [ { ] / . test ( trimmed ) ) continue ;
263+ // ── Non-variable-assignment checks ──
116264
117- // Skip TODO/blanked lines
118- if ( / \/ \/ \s * T O D O / i. test ( trimmed ) ) continue ;
265+ // Skip function/class declarations (handled separately)
266+ if ( / ^ ( f u n c t i o n | c l a s s ) \s / . test ( trimmed ) ) {
267+ // Check for unblanked function body (standalone function declarations)
268+ const funcMatch = trimmed . match ( / ^ f u n c t i o n \s + ( \w + ) \s * \( [ ^ ) ] * \) \s * \{ / ) ;
269+ if ( funcMatch ) {
270+ // Skip depth-0 functions (utilities before the main component) — these should have bodies
271+ // For React, also skip component functions (uppercase names) — they contain JSX, not implementation
272+ const fnName = funcMatch [ 1 ] ;
273+ const isReactComponent = frameworkName === 'React' && / ^ [ A - Z ] / . test ( fnName ) ;
274+ const isDepth0 = overallBraceDepth === 1 ; // Opening brace already counted above, so depth 1 = top-level
119275
120- // Check for implementation method chains with callbacks
121- const hasImplMethodChain = IMPL_METHOD_CHAIN . some ( ( rx ) => rx . test ( trimmed ) ) ;
276+ if ( isDepth0 || isReactComponent ) {
277+ continue ; // Depth-0 utility or React component — skip this check
278+ }
122279
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 ;
280+ let depth = 0 ;
281+ let bodyEnd = i ;
282+ for ( let j = i ; j < lines . length ; j ++ ) {
283+ for ( const ch of lines [ j ] ) {
284+ if ( ch === '{' ) depth ++ ;
285+ if ( ch === '}' ) {
286+ depth -- ;
287+ if ( depth === 0 ) {
288+ bodyEnd = j ;
289+ break ;
290+ }
291+ }
292+ }
293+ if ( depth === 0 ) break ;
294+ }
295+
296+ const bodyLines = lines . slice ( i + 1 , bodyEnd ) ;
297+ const bodyText = bodyLines . join ( '\n' ) ;
298+ const hasTodo = / \/ \/ \s * T O D O / i. test ( bodyText ) ;
299+ const nonEmptyBodyLines = bodyLines . filter ( ( l ) => l . trim ( ) . length > 0 ) ;
300+
301+ if ( ! hasTodo && nonEmptyBodyLines . length > 1 ) {
302+ leaks . push ( { lineNumber : i + 1 , content : trimmed , category : 'unblanked-function-body' } ) ;
303+ }
130304 }
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 ;
305+ continue ;
306+ }
307+
308+ // ── Single-line callback detection ──
309+ // something.addEventListener('event', () => { logic; });
310+ // items.forEach(item => { logic; });
311+ const hasSingleLineCallback = / = > \s * \{ [ ^ } ] + \} / . test ( trimmed ) ;
312+ const isCallbackStatement =
313+ / \. a d d E v e n t L i s t e n e r \s * \( / . test ( trimmed ) ||
314+ / \. \s * (?: f o r E a c h | m a p | r e d u c e | f i l t e r ) \s * \( / . test ( trimmed ) ||
315+ / n e w \s + (?: I n t e r s e c t i o n O b s e r v e r | M u t a t i o n O b s e r v e r | R e s i z e O b s e r v e r ) \s * \( / . test ( trimmed ) ;
316+
317+ if ( hasSingleLineCallback && isCallbackStatement ) {
318+ // Extract the callback body and check if it has implementation
319+ const bodyMatch = trimmed . match ( / = > \s * \{ ( [ ^ } ] + ) \} / ) ;
320+ if ( bodyMatch ) {
321+ const callbackBody = bodyMatch [ 1 ] . trim ( ) ;
322+ // Skip if it's just a TODO
323+ if ( ! / \/ \/ \s * T O D O / i. test ( callbackBody ) && callbackBody . length > 5 ) {
324+ leaks . push ( { lineNumber : i + 1 , content : trimmed , category : 'single-line-callback' } ) ;
325+ continue ;
326+ }
135327 }
136328 }
137329
138- if ( hasImplMethodChain ) {
139- leaks . push ( { lineNumber : i + 1 , content : trimmed } ) ;
330+ // ── Multi-line callback without TODO (not on variable assignment) ──
331+ if ( / = > \s * \{ $ / . test ( trimmed ) && isCallbackStatement ) {
332+ let depth = 0 ;
333+ let bodyEnd = i ;
334+ for ( let j = i ; j < lines . length ; j ++ ) {
335+ for ( const ch of lines [ j ] ) {
336+ if ( ch === '{' ) depth ++ ;
337+ if ( ch === '}' ) {
338+ depth -- ;
339+ if ( depth === 0 ) {
340+ bodyEnd = j ;
341+ break ;
342+ }
343+ }
344+ }
345+ if ( depth === 0 ) break ;
346+ }
347+ const bodyText = lines . slice ( i + 1 , bodyEnd ) . join ( '\n' ) ;
348+ if ( ! / \/ \/ \s * T O D O / i. test ( bodyText ) && bodyText . trim ( ) . length > 5 ) {
349+ leaks . push ( { lineNumber : i + 1 , content : trimmed , category : 'multi-line-callback' } ) ;
350+ }
140351 }
141352 }
142353
@@ -187,10 +398,12 @@ describe('UI Pattern starter code — false positive detection', () => {
187398 if ( ! isSkeleton ( starter ) ) continue ;
188399
189400 it ( `${ patternId } — starter should not contain implementation expressions` , ( ) => {
190- const leaks = findImplementationLeaks ( starter ) ;
401+ const leaks = findImplementationLeaks ( starter , fw . name ) ;
191402
192403 if ( leaks . length > 0 ) {
193- const details = leaks . map ( ( l ) => ` line ${ l . lineNumber } : ${ l . content } ` ) . join ( '\n' ) ;
404+ const details = leaks
405+ . map ( ( l ) => ` [${ l . category } ] line ${ l . lineNumber } : ${ l . content } ` )
406+ . join ( '\n' ) ;
194407 expect . soft ( leaks . length , `Implementation leaked in ${ patternId } :\n${ details } ` ) . toBe ( 0 ) ;
195408 }
196409 } ) ;
0 commit comments