@@ -40,12 +40,14 @@ export class Resolver {
4040 private readonly evaluations = new WeakMap < t . Node , Evidence > ( )
4141 private unknowns = new WeakMap < t . Node , Map < string , Evidence > > ( )
4242 private readonly opaqueValues = new Map < string , Evidence > ( )
43+ private readonly resolvingExports = new Set < string > ( )
4344 private readonly resolvingUnknown = new Set < t . Node > ( )
4445 private steps = 0
4546 private parsedModules = 0
4647 private readonly active = new Set < t . Node > ( )
4748 private dependencies = new Set < string > ( )
4849 private unresolved = new Set < string > ( )
50+ private readonly eventProps = new WeakMap < t . Node , Set < string > > ( )
4951
5052 constructor (
5153 readonly tree : SourceTree ,
@@ -295,7 +297,7 @@ export class Resolver {
295297 'Configured capability adapter: declared environment fields and provider factories are traced without execution'
296298 )
297299 return {
298- $environmentAdapter : this . opaqueExports ( adapter . module , adapter . export ) ,
300+ $environmentAdapter : fingerprint ( this . module ( adapter . module ) . ast . program ) ,
299301 implementation,
300302 environment : selected ,
301303 arguments : children ( path , 'arguments' ) . map ( ( argument ) =>
@@ -331,6 +333,44 @@ export class Resolver {
331333 return { value : this . value ( input , file , depth + 1 ) , conditions }
332334 }
333335
336+ /** Resolve props whose only uses select event handlers, without assuming custom prop names. */
337+ eventOnlyProp ( name : NodePath , property : string , file : string ) : boolean {
338+ if ( ! name . isJSXIdentifier ( ) || ! / ^ [ A - Z ] / . test ( name . node . name ) ) return false
339+ const key = name . node
340+ let properties = this . eventProps . get ( key )
341+ if ( ! properties ) {
342+ properties = new Set < string > ( )
343+ this . eventProps . set ( key , properties )
344+ const target = this . callable ( name , file )
345+ if ( ! target ) return false
346+ const parameter = children ( target . path , 'params' ) [ 0 ]
347+ if ( ! parameter ?. isObjectPattern ( ) ) return false
348+ for ( const prop of children ( parameter , 'properties' ) ) {
349+ if ( ! prop . isObjectProperty ( ) || prop . node . computed ) continue
350+ const value = child ( prop , 'value' )
351+ const local = value . isAssignmentPattern ( ) ? child ( value , 'left' ) : value
352+ if ( ! local . isIdentifier ( ) ) continue
353+ const references = local . scope
354+ . getBinding ( local . node . name )
355+ ?. referencePaths . filter (
356+ ( reference ) => ! reference . findParent ( ( parent ) => parent . isTSType ( ) )
357+ )
358+ if ( ! references ?. length ) continue
359+ if (
360+ references . every ( ( reference ) => {
361+ for ( let parent = reference . parentPath ; parent ; parent = parent . parentPath ) {
362+ if ( parent . isFunction ( ) || parent . isCallExpression ( ) ) return false
363+ if ( parent . isJSXAttribute ( ) ) return / ^ o n [ A - Z ] / . test ( propertyName ( parent . node . name ) )
364+ }
365+ return false
366+ } )
367+ )
368+ properties . add ( propertyName ( prop . node . key ) )
369+ }
370+ }
371+ return properties . has ( property )
372+ }
373+
334374 private currentFile = ''
335375
336376 /** Select a property before expanding siblings, including createEnv's schema convention. */
@@ -492,7 +532,7 @@ export class Resolver {
492532 if ( path . isFunction ( ) ) return { path, file }
493533 if ( path . isTSAsExpression ( ) || path . isTSSatisfiesExpression ( ) || path . isTSNonNullExpression ( ) )
494534 return this . callable ( child ( path , 'expression' ) , file , seen )
495- if ( ! path . isIdentifier ( ) ) return undefined
535+ if ( ! path . isIdentifier ( ) && ! path . isJSXIdentifier ( ) ) return undefined
496536 const binding = path . scope . getBinding ( path . node . name )
497537 if ( ! binding ?. constant ) return undefined
498538 if ( binding . path . isFunctionDeclaration ( ) ) return { path : binding . path , file }
@@ -691,24 +731,42 @@ export class Resolver {
691731 } )
692732 return renders
693733 ? { $renderFunction : { file, symbol : symbolName ( path ) } }
694- : fingerprint ( path . node )
734+ : this . unknown ( path , 'Opaque helper input is traced without execution' , file )
695735 }
696736
697737 /** Opaque export summaries are independent of expression budgets and parser cache eviction. */
698738 private opaqueExports ( file : string , name : string ) : Data {
699739 const key = `${ file } :${ name } `
740+ if ( this . affected && ! this . affected . has ( file ) ) {
741+ this . dependencies . add ( file )
742+ return { $unchangedInput : { file, export : name } }
743+ }
700744 const cached = this . opaqueValues . get ( key )
701745 if ( cached ) {
702746 for ( const file of cached . dependencies ) this . dependencies . add ( file )
703747 return cached . value
704748 }
749+ this . dependencies . add ( file )
750+ if ( this . resolvingExports . has ( key ) ) {
751+ this . unresolved . add ( 'Opaque export dependency cycle' )
752+ return { $cycle : key }
753+ }
754+ if ( this . resolvingExports . size >= 64 ) {
755+ this . unresolved . add ( 'Opaque export dependency depth limit' )
756+ return { $unresolvedExport : key , blob : this . tree . entries . get ( file ) ?. oid ?? null }
757+ }
758+ this . resolvingExports . add ( key )
705759 const dependencies = this . dependencies
706760 this . dependencies = new Set ( [ file ] )
707- const value = this . opaqueExportInner ( file , name )
708- this . opaqueValues . set ( key , { value, dependencies : [ ...this . dependencies ] , unresolved : [ ] } )
709- for ( const file of this . dependencies ) dependencies . add ( file )
710- this . dependencies = dependencies
711- return value
761+ try {
762+ const value = this . opaqueExportInner ( file , name )
763+ this . opaqueValues . set ( key , { value, dependencies : [ ...this . dependencies ] , unresolved : [ ] } )
764+ return value
765+ } finally {
766+ for ( const file of this . dependencies ) dependencies . add ( file )
767+ this . dependencies = dependencies
768+ this . resolvingExports . delete ( key )
769+ }
712770 }
713771
714772 /** Follow export declarations only when normal evaluation is bounded or ambiguous. */
@@ -770,11 +828,50 @@ export class Resolver {
770828 const dependencies = this . dependencies
771829 this . dependencies = new Set ( [ file ] )
772830 const inputs = new Map < string , Data > ( )
831+ const inspected = new Set < t . Node > ( )
773832 const inspect = ( reference : NodePath ) => {
774833 if ( ! reference . isReferencedIdentifier ( ) ) return
834+ if ( reference . findParent ( ( parent ) => parent . isTSType ( ) ) ) return
775835 const binding = reference . scope . getBinding ( reference . node . name )
776836 if ( ! binding ) return
777837 const bound = binding . path
838+ if ( bound === path || bound . findParent ( ( parent ) => parent === path ) ) return
839+ const member = reference . parentPath
840+ if ( member ?. isMemberExpression ( ) && member . node . object === reference . node ) {
841+ const selection = member . node . computed
842+ ? finiteKeys ( child ( member , 'property' ) , file , this )
843+ : { keys : [ propertyName ( member . node . property ) ] , dependencies : [ ] }
844+ if ( selection ?. keys ) {
845+ for ( const dependency of selection . dependencies ) this . dependencies . add ( dependency )
846+ const values = selection . keys . map ( ( key ) => this . selected ( reference , String ( key ) , file , 0 ) )
847+ if ( values . every ( ( value ) => value !== undefined ) ) {
848+ inputs . set ( `selected:${ reference . node . name } .[${ selection . keys . join ( ',' ) } ]` , {
849+ $finiteSelection : selection . keys . map ( ( key , index ) => [ key , values [ index ] ! ] as Data ) ,
850+ } )
851+ return
852+ }
853+ }
854+ }
855+ if (
856+ ! bound . isImportSpecifier ( ) &&
857+ ! bound . isImportDefaultSpecifier ( ) &&
858+ ! bound . isImportNamespaceSpecifier ( )
859+ ) {
860+ if ( inspected . has ( bound . node ) ) return
861+ inspected . add ( bound . node )
862+ const parameter = this . parameter ( bound , reference . node . name , file , 0 )
863+ if ( parameter !== undefined ) {
864+ inputs . set ( `parameter:${ reference . node . name } ` , parameter )
865+ return
866+ }
867+ const value = bound . isVariableDeclarator ( ) ? child ( bound , 'init' ) : bound
868+ if ( value . node )
869+ inputs . set (
870+ `local:${ reference . node . name } ` ,
871+ this . unknown ( value , 'Captured input feeds unresolved rendering' , file )
872+ )
873+ return
874+ }
778875 if (
779876 bound . isImportSpecifier ( ) ||
780877 bound . isImportDefaultSpecifier ( ) ||
@@ -790,25 +887,11 @@ export class Resolver {
790887 ? 'default'
791888 : '*'
792889 this . dependencies . add ( target )
793- const member = reference . parentPath
794- if ( member ?. isMemberExpression ( ) && member . node . object === reference . node ) {
795- const selection = member . node . computed
796- ? finiteKeys ( child ( member , 'property' ) , file , this )
797- : { keys : [ propertyName ( member . node . property ) ] , dependencies : [ ] }
798- if ( selection ?. keys ) {
799- for ( const file of selection . dependencies ) this . dependencies . add ( file )
800- const values = selection . keys . map ( ( key ) =>
801- this . selected ( reference , String ( key ) , file , 0 )
802- )
803- if ( values . every ( ( value ) => value !== undefined ) ) {
804- inputs . set ( `${ target } :${ name } .[${ selection . keys . join ( ',' ) } ]` , {
805- $finiteSelection : selection . keys . map ( ( key , index ) => [ key , values [ index ] ! ] as Data ) ,
806- } )
807- return
808- }
809- }
810- }
811890 const origin = this . tree . graph ?. resolvedExport ( target , name ) ?. origin
891+ if ( this . affected && origin && ! this . affected . has ( origin . file ) ) {
892+ inputs . set ( `${ target } :${ name } ` , this . opaqueExports ( origin . file , origin . exported ) )
893+ return
894+ }
812895 inputs . set (
813896 `${ target } :${ name } ` ,
814897 origin
@@ -858,7 +941,7 @@ export class Resolver {
858941 const key = `${ file } :${ name } `
859942 if ( depth > this . tree . config . limits . resolutionDepth || visited . has ( key ) ) {
860943 this . unresolved . add ( visited . has ( key ) ? 'Dependency cycle' : 'Export resolution depth limit' )
861- return { $unresolved : key }
944+ return this . opaqueExports ( file , name )
862945 }
863946 visited . add ( key )
864947 this . dependencies . add ( file )
@@ -960,6 +1043,7 @@ export class Resolver {
9601043 if ( attribute . isJSXAttribute ( ) ) {
9611044 const name = propertyName ( attribute . node . name )
9621045 if ( / ^ (?: k e y | r e f | o n [ A - Z ] .* ) $ / . test ( name ) ) continue
1046+ if ( this . eventOnlyProp ( child ( opening , 'name' ) , name , file ) ) continue
9631047 attributes . push ( [
9641048 name ,
9651049 attribute . node . value ? this . value ( child ( attribute , 'value' ) , file , depth + 1 ) : true ,
0 commit comments