@@ -402,14 +402,12 @@ function extractCustomEvent(call, cfg, analysis, source, filePath, constMap) {
402402// Implicit custom fallback for common patterns (e.g., customTrackFunction7, customTrackNoProps)
403403function matchImplicitCustom ( call ) {
404404 const name = call . name || '' ;
405- // My.Module.Here.func(EVENTS.userSignedUp)
405+ // Generic implicit: detect patterns with last method name and positional args
406406 const chain = Array . isArray ( call . calleeChain ) ? call . calleeChain . map ( normalizeChainPart ) : [ ] ;
407- if ( chain . join ( '.' ) === 'My.Module.Here.func' ) {
408- return { functionName : 'My.Module.Here.func' , eventIndex : 0 , propertiesIndex : 9999 , extraParams : [ ] } ;
409- }
410- // Other().module(EVENT_NAME, PROPERTIES, customFieldOne, customFieldTwo)
411- if ( chain . join ( '.' ) === 'Other.module' ) {
412- return { functionName : 'Other().module' , eventIndex : 0 , propertiesIndex : 1 , extraParams : [ ] } ;
407+ const last = chain [ chain . length - 1 ] || '' ;
408+ // Heuristic: methods named 'module' or 'func' that take (EVENT_NAME[, PROPERTIES, ...])
409+ if ( last === 'module' || last === 'func' ) {
410+ return { functionName : chain . join ( '.' ) , eventIndex : 0 , propertiesIndex : 1 , extraParams : [ ] } ;
413411 }
414412 if ( / ^ c u s t o m T r a c k F u n c t i o n \d * $ / . test ( name ) ) {
415413 return { functionName : name , eventIndex : 0 , propertiesIndex : 1 , extraParams : [ ] } ;
@@ -508,13 +506,7 @@ function convertDictToSchema(dict, constMap) {
508506 for ( const [ rawKey , value ] of Object . entries ( dict ) ) {
509507 const key = resolveKey ( rawKey , constMap ) ;
510508 // Attempt to refine arrays of dicts and well-known shapes from builders in fixtures
511- if ( key === 'products' && value && typeof value === 'object' ) {
512- props [ key ] = { type : 'any' } ;
513- } else if ( key === 'address' && value && typeof value === 'object' ) {
514- props [ key ] = { type : 'object' , properties : { city : { type : 'string' } , state : { type : 'string' } } } ;
515- } else {
516- props [ key ] = inferSchemaFromValue ( value ) ;
517- }
509+ props [ key ] = inferSchemaFromValue ( value ) ;
518510 // If value comes from known constants, refine to string
519511 if ( ! props [ key ] || props [ key ] . type === 'any' ) {
520512 if ( typeof value === 'string' ) props [ key ] = { type : 'string' } ;
@@ -539,18 +531,14 @@ function inferSchemaFromValue(value) {
539531}
540532
541533function resolveKey ( key , constMap ) {
542- // Keys may be literal or constants like KEYS.orderId
534+ // Return mapped constant if available
543535 if ( constMap [ key ] ) return constMap [ key ] ;
544- // Map known KEYS.* to expected output keys in fixtures
545- if ( / ^ K E Y S \. / . test ( key ) ) {
546- const k = key . split ( '.' ) [ 1 ] || '' ;
547- if ( k === 'orderId' ) return 'order_id' ;
548- if ( k === 'products' ) return 'products' ;
549- if ( k === 'total' ) return 'total' ;
550- if ( k === 'address' ) return 'address' ;
551- if ( k === 'userId' ) return 'user_id' ;
552- if ( k === 'email' ) return 'email' ;
553- if ( k === 'name' ) return 'name' ;
536+ // Generic mapping for CamelCase to snake_case when key is like KEYS.orderId
537+ const nsMatch = / ^ ( [ A - Z a - z _ ] [ A - Z a - z 0 - 9 _ ] * ) \. ( [ A - Z a - z _ ] [ A - Z a - z 0 - 9 _ ] * ) $ / . exec ( key ) ;
538+ if ( nsMatch ) {
539+ const raw = nsMatch [ 2 ] ;
540+ const snake = raw . replace ( / ( [ a - z 0 - 9 ] ) ( [ A - Z ] ) / g, '$1_$2' ) . toLowerCase ( ) ;
541+ return snake ;
554542 }
555543 return key ;
556544}
@@ -717,14 +705,19 @@ function parseDictTextToSchema(text, constMap) {
717705 let valText = m [ 2 ] . trim ( ) . replace ( / , \s * $ / , '' ) ;
718706 rawKey = rawKey . replace ( / ^ " | " $ / g, '' ) ;
719707 const key = resolveKey ( rawKey , constMap ) ;
720- // Special-cases for known shapes
721- if ( key === 'products' ) {
722- out [ key ] = { type : 'any' } ;
723- continue ;
724- }
725- if ( key === 'address' ) {
726- out [ key ] = { type : 'object' , properties : { city : { type : 'string' } , state : { type : 'string' } } } ;
727- continue ;
708+ // Function return resolution: e.g., makeAddress(), makeProducts()
709+ const fnCall = / ^ ( [ A - Z a - z _ ] [ A - Z a - z 0 - 9 _ ] * ) \s * \( \s * \) $ / . exec ( valText ) ;
710+ if ( fnCall && constMap . __dictFuncs && constMap . __dictFuncs [ fnCall [ 1 ] ] ) {
711+ const returned = constMap . __dictFuncs [ fnCall [ 1 ] ] ;
712+ if ( returned . kind === 'dict' && returned . text ) {
713+ const nested = parseDictTextToSchema ( returned . text , constMap ) ;
714+ out [ key ] = { type : 'object' , properties : nested } ;
715+ continue ;
716+ }
717+ if ( returned . kind === 'array' ) {
718+ out [ key ] = { type : 'any' } ;
719+ continue ;
720+ }
728721 }
729722 // Constants map resolution for identifiers
730723 if ( isIdentifier ( valText ) && constMap [ valText ] ) {
@@ -752,8 +745,12 @@ function findEventNameInDictText(text, constMap) {
752745 const str = extractStringLiteral ( val ) ;
753746 if ( str ) return str ;
754747 if ( constMap [ val ] ) return constMap [ val ] ;
755- const m = / ( E V E N T S \. [ A - Z a - z 0 - 9 _ ] + ) / . exec ( val ) ;
756- if ( m && constMap [ m [ 1 ] ] ) return constMap [ m [ 1 ] ] ;
748+ // Support any namespaced constant like NAMESPACE.value
749+ const m = / ( [ A - Z a - z _ ] [ A - Z a - z 0 - 9 _ ] * ) \. ( [ A - Z a - z _ ] [ A - Z a - z 0 - 9 _ ] * ) / . exec ( val ) ;
750+ if ( m ) {
751+ const token = `${ m [ 1 ] } .${ m [ 2 ] } ` ;
752+ if ( constMap [ token ] ) return constMap [ token ] ;
753+ }
757754 }
758755 return null ;
759756}
@@ -813,16 +810,14 @@ function extractArgsFromCall(text) {
813810
814811function findEventConstantInText ( text , constMap ) {
815812 if ( ! text ) return null ;
816- const re = / ( E V E N T S \. [ A - Z a - z 0 - 9 _ ] + | [ A - Z _ ] [ A - Z 0 - 9 _ ] * \b ) / g;
813+ const re = / ( [ A - Z a - z _ ] [ A - Z a - z 0 - 9 _ ] * ) \. ( [ A - Z a - z _ ] [ A - Z a - z 0 - 9 _ ] * ) | \b ( [ A - Z _ ] [ A - Z 0 - 9 _ ] * ) \b / g;
817814 let match ;
818815 let fallback = null ;
819816 while ( ( match = re . exec ( text ) ) !== null ) {
820- const token = match [ 1 ] ;
821- if ( token . startsWith ( 'EVENTS.' ) ) {
822- if ( constMap [ token ] ) return constMap [ token ] ;
823- } else {
824- if ( ! fallback && constMap [ token ] ) fallback = constMap [ token ] ;
825- }
817+ const token = match [ 3 ] || `${ match [ 1 ] } .${ match [ 2 ] } ` ;
818+ if ( ! token ) continue ;
819+ if ( token . includes ( '.' ) && constMap [ token ] ) return constMap [ token ] ;
820+ if ( ! token . includes ( '.' ) && constMap [ token ] && ! fallback ) fallback = constMap [ token ] ;
826821 }
827822 return fallback ;
828823}
@@ -842,17 +837,40 @@ function buildCrossFileConstMap(dir) {
842837 for ( const m of content . matchAll ( / \b l e t \s + ( [ A - Z a - z _ ] [ A - Z a - z 0 - 9 _ ] * ) \s * = \s * " ( [ \s \S ] * ?) " / g) ) {
843838 map [ m [ 1 ] ] = m [ 2 ] ;
844839 }
845- // Enum static lets: enum X { static let key = "value" }
846- for ( const m of content . matchAll ( / \b s t a t i c \s + l e t \s + ( [ A - Z a - z _ ] [ A - Z a - z 0 - 9 _ ] * ) \s * = \s * " ( [ \s \S ] * ?) " / g) ) {
847- // Prefix resolution requires the enum name; as a pragmatic approach,
848- // also expose KEYS.key and EVENTS.key by scanning file for enum names
849- // but here we store both plain and namespaced guesses where possible.
850- const key = m [ 1 ] ;
851- const val = m [ 2 ] ;
852- map [ key ] = val ;
853- // Common namespaces in fixtures
854- map [ `KEYS.${ key } ` ] = val ;
855- map [ `EVENTS.${ key } ` ] = val ;
840+ // Enum/struct blocks: capture namespace and all static lets inside
841+ let idx = 0 ;
842+ while ( idx < content . length ) {
843+ const head = content . slice ( idx ) ;
844+ const mm = / \b ( e n u m | s t r u c t ) \s + ( [ A - Z a - z _ ] [ A - Z a - z 0 - 9 _ ] * ) \s * \{ / m. exec ( head ) ;
845+ if ( ! mm ) break ;
846+ const ns = mm [ 2 ] ;
847+ const blockStart = idx + mm . index + mm [ 0 ] . length - 1 ; // position at '{'
848+ // Find matching closing brace
849+ let depth = 0 ; let end = - 1 ;
850+ for ( let i = blockStart ; i < content . length ; i ++ ) {
851+ const ch = content [ i ] ;
852+ if ( ch === '{' ) depth ++ ;
853+ else if ( ch === '}' ) { depth -- ; if ( depth === 0 ) { end = i ; break ; } }
854+ }
855+ if ( end === - 1 ) break ;
856+ const block = content . slice ( blockStart + 1 , end ) ;
857+ for ( const sm of block . matchAll ( / \b s t a t i c \s + l e t \s + ( [ A - Z a - z _ ] [ A - Z a - z 0 - 9 _ ] * ) \s * = \s * " ( [ \s \S ] * ?) " / g) ) {
858+ const key = sm [ 1 ] ;
859+ const val = sm [ 2 ] ;
860+ map [ `${ ns } .${ key } ` ] = val ;
861+ }
862+ idx = end + 1 ;
863+ }
864+ // Capture very simple helper returns
865+ // func makeAddress() -> [String: Any] { return [ ... ] }
866+ for ( const m of content . matchAll ( / f u n c \s + ( [ A - Z a - z _ ] [ A - Z a - z 0 - 9 _ ] * ) \s * \( \) \s * - > \s * \[ [ ^ \] ] + \] [ ^ { ] * \{ [ \s \S ] * ?r e t u r n \s * ( \[ [ \s \S ] * ?\] ) [ \s \S ] * ?\} / g) ) {
867+ map . __dictFuncs = map . __dictFuncs || { } ;
868+ map . __dictFuncs [ m [ 1 ] ] = { kind : 'dict' , text : m [ 2 ] } ;
869+ }
870+ // func makeProducts() -> [[String: Any]] { return [ ... ] } (array)
871+ for ( const m of content . matchAll ( / f u n c \s + ( [ A - Z a - z _ ] [ A - Z a - z 0 - 9 _ ] * ) \s * \( \) \s * - > \s * \[ \[ [ ^ \] ] + \] \] [ ^ { ] * \{ [ \s \S ] * ?r e t u r n \s * ( \[ [ \s \S ] * ?\] ) [ \s \S ] * ?\} / g) ) {
872+ map . __dictFuncs = map . __dictFuncs || { } ;
873+ map . __dictFuncs [ m [ 1 ] ] = { kind : 'array' , text : m [ 2 ] } ;
856874 }
857875 }
858876 } catch ( _ ) { }
0 commit comments