11import { promises as fs } from 'node:fs'
22import path from 'node:path'
33
4- import { transformAsync } from '@babel/core'
4+ import { parseSync , transformAsync } from '@babel/core'
5+ import * as BabelTypes from '@babel/types'
56import { createFictPlugin , type CompilerWarning , type FictCompilerOptions } from '@fictjs/compiler'
67import type * as TypeScriptApi from 'typescript'
78
@@ -18,6 +19,9 @@ interface DiagnosticsInput {
1819 config : PlaygroundConfig
1920}
2021
22+ type CompilerErrorContext = 'loop-or-conditional' | 'nested-function'
23+ type CompilerMacroName = '$effect' | '$state'
24+
2125export async function collectSessionDiagnostics (
2226 input : DiagnosticsInput ,
2327) : Promise < PlaygroundDiagnosticsResult > {
@@ -97,7 +101,7 @@ async function collectCompilerDiagnostics(
97101 } )
98102 }
99103 } catch ( error ) {
100- diagnostics . push ( fromCompilerError ( input . rootDir , absolutePath , error ) )
104+ diagnostics . push ( fromCompilerError ( input . rootDir , absolutePath , sourceCode , error ) )
101105 }
102106 }
103107
@@ -231,22 +235,182 @@ function extractLocationFromCompilerMessage(
231235) : { line : number ; column : number } | null {
232236 const lineMatch = / ^ > \s + ( \d + ) \s + \| / m. exec ( message )
233237 const columnMatch = / ^ \s * \| \s + ( \^ + ) / m. exec ( message )
234- if ( ! lineMatch || ! columnMatch ) return null
238+ if ( lineMatch && columnMatch ) {
239+ const line = Number . parseInt ( lineMatch [ 1 ] ?? '' , 10 )
240+ const carets = columnMatch [ 1 ]
241+ if ( ! Number . isFinite ( line ) || ! carets ) return null
242+
243+ const markerIndex = columnMatch . index + columnMatch [ 0 ] . indexOf ( carets )
244+ const lineStart = message . lastIndexOf ( '\n' , columnMatch . index ) + 1
245+ const column = markerIndex - lineStart
246+
247+ return { line, column : column + 1 }
248+ }
235249
236- const line = Number . parseInt ( lineMatch [ 1 ] ?? '' , 10 )
237- const carets = columnMatch [ 1 ]
238- if ( ! Number . isFinite ( line ) || ! carets ) return null
250+ const summaryMatch = / \( ( \d + ) : ( \d + ) \) (?: \s | $ ) / . exec ( message )
251+ if ( ! summaryMatch ) return null
239252
240- const markerIndex = columnMatch . index + columnMatch [ 0 ] . indexOf ( carets )
241- const lineStart = message . lastIndexOf ( '\n ', columnMatch . index ) + 1
242- const column = markerIndex - lineStart
253+ const line = Number . parseInt ( summaryMatch [ 1 ] ?? '' , 10 )
254+ const column = Number . parseInt ( summaryMatch [ 2 ] ?? ' ', 10 )
255+ if ( ! Number . isFinite ( line ) || ! Number . isFinite ( column ) ) return null
243256
244257 return { line, column : column + 1 }
245258}
246259
260+ function classifyCompilerPlacementError (
261+ message : string ,
262+ ) : { context : CompilerErrorContext ; macroName : CompilerMacroName } | null {
263+ if ( / \$ s t a t e \( \) c a n n o t b e d e c l a r e d i n s i d e l o o p s o r c o n d i t i o n a l s \. / . test ( message ) ) {
264+ return {
265+ context : 'loop-or-conditional' ,
266+ macroName : '$state' ,
267+ }
268+ }
269+
270+ if ( / \$ s t a t e \( \) c a n n o t b e d e c l a r e d i n s i d e n e s t e d f u n c t i o n s \. / . test ( message ) ) {
271+ return {
272+ context : 'nested-function' ,
273+ macroName : '$state' ,
274+ }
275+ }
276+
277+ if ( / \$ e f f e c t \( \) c a n n o t b e c a l l e d i n s i d e l o o p s o r c o n d i t i o n a l s \. / . test ( message ) ) {
278+ return {
279+ context : 'loop-or-conditional' ,
280+ macroName : '$effect' ,
281+ }
282+ }
283+
284+ if ( / \$ e f f e c t \( \) c a n n o t b e c a l l e d i n s i d e n e s t e d f u n c t i o n s \. / . test ( message ) ) {
285+ return {
286+ context : 'nested-function' ,
287+ macroName : '$effect' ,
288+ }
289+ }
290+
291+ return null
292+ }
293+
294+ function parseSourceAstSafely ( sourceCode : string , filePath : string ) : BabelTypes . File | null {
295+ try {
296+ const ast = parseSync ( sourceCode , {
297+ filename : filePath ,
298+ sourceType : 'module' ,
299+ parserOpts : {
300+ sourceType : 'module' ,
301+ plugins : [ 'typescript' , 'jsx' ] ,
302+ allowReturnOutsideFunction : true ,
303+ } ,
304+ } )
305+
306+ return ast && ast . type === 'File' ? ast : null
307+ } catch {
308+ return null
309+ }
310+ }
311+
312+ function isLoopNode ( node : BabelTypes . Node ) : boolean {
313+ return (
314+ BabelTypes . isForStatement ( node ) ||
315+ BabelTypes . isForInStatement ( node ) ||
316+ BabelTypes . isForOfStatement ( node ) ||
317+ BabelTypes . isWhileStatement ( node ) ||
318+ BabelTypes . isDoWhileStatement ( node )
319+ )
320+ }
321+
322+ function isConditionalNode ( node : BabelTypes . Node ) : boolean {
323+ return (
324+ BabelTypes . isIfStatement ( node ) ||
325+ BabelTypes . isSwitchStatement ( node ) ||
326+ BabelTypes . isSwitchCase ( node ) ||
327+ BabelTypes . isConditionalExpression ( node ) ||
328+ BabelTypes . isLogicalExpression ( node )
329+ )
330+ }
331+
332+ function findFirstMacroCallInContext (
333+ node : BabelTypes . Node ,
334+ macroName : CompilerMacroName ,
335+ context : CompilerErrorContext ,
336+ ancestors : BabelTypes . Node [ ] = [ ] ,
337+ ) : { line : number ; column : number } | null {
338+ const nextAncestors = [ ...ancestors , node ]
339+
340+ if (
341+ BabelTypes . isCallExpression ( node ) &&
342+ BabelTypes . isIdentifier ( node . callee ) &&
343+ node . callee . name === macroName &&
344+ node . loc
345+ ) {
346+ const hasLoop = nextAncestors . some ( isLoopNode )
347+ const hasConditional = nextAncestors . some ( isConditionalNode )
348+ const functionDepth = nextAncestors . filter ( ancestor => BabelTypes . isFunction ( ancestor ) ) . length
349+
350+ if ( context === 'loop-or-conditional' && ( hasLoop || hasConditional ) ) {
351+ return {
352+ line : node . loc . start . line ,
353+ column : node . loc . start . column + 1 ,
354+ }
355+ }
356+
357+ if ( context === 'nested-function' && functionDepth > 1 ) {
358+ return {
359+ line : node . loc . start . line ,
360+ column : node . loc . start . column + 1 ,
361+ }
362+ }
363+ }
364+
365+ const visitorKeys = BabelTypes . VISITOR_KEYS [ node . type ] ?? [ ]
366+ for ( const key of visitorKeys ) {
367+ const value = ( node as unknown as Record < string , unknown > ) [ key ]
368+
369+ if ( Array . isArray ( value ) ) {
370+ for ( const child of value ) {
371+ if ( ! child || typeof child !== 'object' || ! ( 'type' in child ) ) continue
372+ const found = findFirstMacroCallInContext (
373+ child as BabelTypes . Node ,
374+ macroName ,
375+ context ,
376+ nextAncestors ,
377+ )
378+ if ( found ) return found
379+ }
380+ continue
381+ }
382+
383+ if ( ! value || typeof value !== 'object' || ! ( 'type' in value ) ) continue
384+ const found = findFirstMacroCallInContext (
385+ value as BabelTypes . Node ,
386+ macroName ,
387+ context ,
388+ nextAncestors ,
389+ )
390+ if ( found ) return found
391+ }
392+
393+ return null
394+ }
395+
396+ export function inferCompilerLocationFromSource (
397+ sourceCode : string ,
398+ filePath : string ,
399+ message : string ,
400+ ) : { line : number ; column : number } | null {
401+ const classification = classifyCompilerPlacementError ( message )
402+ if ( ! classification ) return null
403+
404+ const ast = parseSourceAstSafely ( sourceCode , filePath )
405+ if ( ! ast ) return null
406+
407+ return findFirstMacroCallInContext ( ast , classification . macroName , classification . context )
408+ }
409+
247410function fromCompilerError (
248411 rootDir : string ,
249412 fileName : string ,
413+ sourceCode : string ,
250414 error : unknown ,
251415) : PlaygroundDiagnostic {
252416 let message = 'Unknown compiler transform failure'
@@ -256,6 +420,7 @@ function fromCompilerError(
256420 if ( error instanceof Error ) {
257421 message = error . message
258422 const derivedLocation = extractLocationFromCompilerMessage ( message )
423+ const inferredLocation = inferCompilerLocationFromSource ( sourceCode , fileName , message )
259424 const errorWithLocation = error as Error & {
260425 loc ?: {
261426 line : number
@@ -268,6 +433,9 @@ function fromCompilerError(
268433 } else if ( derivedLocation ) {
269434 line = derivedLocation . line
270435 column = derivedLocation . column
436+ } else if ( inferredLocation ) {
437+ line = inferredLocation . line
438+ column = inferredLocation . column
271439 }
272440 }
273441
0 commit comments