@@ -32,6 +32,8 @@ export type CallInfo = {
3232 argsCount : number ;
3333 range : Range ;
3434 refersToId ?: number ;
35+ calleeChain ?: string [ ] ;
36+ baseIdentifier ?: string ;
3537} ;
3638
3739export type RefInfo = {
@@ -41,11 +43,22 @@ export type RefInfo = {
4143 refersToId ?: number ;
4244} ;
4345
46+ export type CallArg = { label ?: string ; text : string ; range : Range ; nodeId : number } ;
47+
4448export type Analysis = {
4549 symbols : Map < number , SymbolInfo > ;
4650 calls : CallInfo [ ] ;
4751 refs : RefInfo [ ] ;
4852 byName : Map < string , number [ ] > ;
53+ getNode : ( id : number ) => AstNode | undefined ;
54+ getChildren : ( id : number ) => number [ ] ;
55+ getParent : ( id : number ) => number | undefined ;
56+ getCalleeText : ( callId : number ) => string | undefined ;
57+ getStringLiteralValue : ( nodeId : number ) => string | undefined ;
58+ getCallArgs : ( callId : number ) => CallArg [ ] ;
59+ extractDictionary : ( nodeId : number ) => any ;
60+ findEnclosing : ( nodeId : number , kinds ?: string [ ] ) => SymbolInfo | undefined ;
61+ collectConstStrings : ( ) => Map < string , string > ;
4962 findCallsByName : ( name : string ) => CallInfo [ ] ;
5063 resolveNameAt : ( name : string , offset : number ) => SymbolInfo | undefined ;
5164} ;
@@ -109,6 +122,83 @@ export function analyzeAst(ast: Ast, source: string): Analysis {
109122 const byName = new Map < string , number [ ] > ( ) ;
110123 const calls : CallInfo [ ] = [ ] ;
111124 const refs : RefInfo [ ] = [ ] ;
125+ const constStrings = new Map < string , string > ( ) ;
126+
127+ const nodeAt = ( id : number ) => ast . nodes [ id ] ;
128+ const kidsOf = ( id : number ) => children . get ( id ) ?? [ ] ;
129+ const parentOf = ( id : number ) => parent . get ( id ) ;
130+
131+ function findDirectChildToken ( id : number , token : string ) : number | undefined {
132+ for ( const k of kidsOf ( id ) ) {
133+ const n = nodeAt ( k ) ;
134+ if ( n && n . kind === 'TokenSyntax' && n . tokenText === token ) return k ;
135+ }
136+ return undefined ;
137+ }
138+
139+ function calleeTextForCall ( id : number ) : string | undefined {
140+ const node = nodeAt ( id ) ;
141+ if ( ! node ) return undefined ;
142+ const lparenId = findDirectChildToken ( id , '(' ) ;
143+ if ( ! lparenId ) return undefined ;
144+ const lp = nodeAt ( lparenId ) ! ;
145+ const start = node . range . start . offset ;
146+ const end = lp . range . start . offset ; // up to the '(' that begins this call's arg list
147+ return source . slice ( start , end ) . trim ( ) ;
148+ }
149+
150+ function calleeChainParts ( text : string ) : string [ ] {
151+ if ( ! text ) return [ ] ;
152+ // Split on member separators, handling optional/force chaining
153+ return text . split ( / (?: \? \. | ! \. | \. ) / ) . map ( s => s . trim ( ) ) . filter ( Boolean ) ;
154+ }
155+
156+ function baseIdentifierFromChain ( chain : string [ ] ) : string | undefined {
157+ if ( ! chain . length ) return undefined ;
158+ const base = chain [ 0 ] ;
159+ return base . replace ( / [ ! ? ] + $ / g, '' ) . replace ( / \( \) $ / , '' ) ;
160+ }
161+
162+ function splitTopLevel ( input : string , sep : string ) : string [ ] {
163+ const out : string [ ] = [ ] ;
164+ let depthPar = 0 , depthBr = 0 , depthBr2 = 0 ;
165+ let inStr = false , esc = false ;
166+ let acc = '' ;
167+ for ( let i = 0 ; i < input . length ; i ++ ) {
168+ const ch = input [ i ] ;
169+ if ( inStr ) {
170+ acc += ch ;
171+ if ( esc ) { esc = false ; continue ; }
172+ if ( ch === '\\' ) { esc = true ; continue ; }
173+ if ( ch === '"' ) { inStr = false ; }
174+ continue ;
175+ }
176+ if ( ch === '"' ) { inStr = true ; acc += ch ; continue ; }
177+ if ( ch === '(' ) depthPar ++ ;
178+ else if ( ch === ')' ) depthPar = Math . max ( 0 , depthPar - 1 ) ;
179+ else if ( ch === '[' ) depthBr ++ ;
180+ else if ( ch === ']' ) depthBr = Math . max ( 0 , depthBr - 1 ) ;
181+ else if ( ch === '{' ) depthBr2 ++ ;
182+ else if ( ch === '}' ) depthBr2 = Math . max ( 0 , depthBr2 - 1 ) ;
183+
184+ if ( ch === sep && depthPar === 0 && depthBr === 0 && depthBr2 === 0 ) {
185+ out . push ( acc ) ;
186+ acc = '' ;
187+ } else {
188+ acc += ch ;
189+ }
190+ }
191+ if ( acc . trim ( ) !== '' ) out . push ( acc ) ;
192+ return out . map ( s => s . trim ( ) ) ;
193+ }
194+
195+ function unquote ( s : string ) : string {
196+ if ( s . startsWith ( '"' ) && s . endsWith ( '"' ) ) {
197+ const body = s . slice ( 1 , - 1 ) ;
198+ return body . replace ( / \\ " / g, '"' ) . replace ( / \\ n / g, '\n' ) . replace ( / \\ r / g, '\r' ) . replace ( / \\ t / g, '\t' ) . replace ( / \\ \\ / g, '\\' ) ;
199+ }
200+ return s ;
201+ }
112202
113203 // DFS with scope stack of maps: name -> symbolId
114204 const scopeStack : Array < Map < string , number > > = [ new Map ( ) ] ;
@@ -158,11 +248,14 @@ export function analyzeAst(ast: Ast, source: string): Analysis {
158248
159249 // Calls and references
160250 if ( kind === 'FunctionCallExprSyntax' ) {
161- const text = slice ( source , node . range ) ;
162- const c = extractCallFromText ( text ) ;
251+ const fullText = slice ( source , node . range ) ;
252+ const c = extractCallFromText ( fullText ) ;
253+ let calleeText = calleeTextForCall ( id ) ;
254+ const chain = calleeText ? calleeChainParts ( calleeText ) : undefined ;
255+ const baseId = chain ? baseIdentifierFromChain ( chain ) : undefined ;
163256 if ( c ) {
164257 const refersToId = resolveName ( c . name ) ;
165- calls . push ( { id, name : c . name , receiver : c . receiver , argsCount : c . argsCount , range : node . range , refersToId } ) ;
258+ calls . push ( { id, name : c . name , receiver : c . receiver , argsCount : c . argsCount , range : node . range , refersToId, calleeChain : chain , baseIdentifier : baseId } ) ;
166259 }
167260 }
168261 if ( kind === 'DeclReferenceExprSyntax' ) {
@@ -173,18 +266,132 @@ export function analyzeAst(ast: Ast, source: string): Analysis {
173266 refs . push ( { id, name, range : node . range , refersToId } ) ;
174267 }
175268
269+ // Best-effort const strings: let NAME = "..."
270+ if ( kind === 'VariableDeclSyntax' ) {
271+ const text = slice ( source , node . range ) ;
272+ const m = text . match ( / \b l e t \s + ( [ A - Z a - z _ ] [ A - Z a - z 0 - 9 _ ] * ) \s * (?: : [ ^ = ] + ) ? = \s * ( " (?: [ ^ " \\ ] | \\ .) * " ) / s) ;
273+ if ( m ) {
274+ constStrings . set ( m [ 1 ] , unquote ( m [ 2 ] ) ) ;
275+ }
276+ }
277+
176278 for ( const k of kids ) walk ( k ) ;
177279
178280 if ( isScope ( kind ) ) scopeStack . pop ( ) ;
179281 }
180282
181283 walk ( ast . root ) ;
182284
285+ function getCallArgs ( callId : number ) : CallArg [ ] {
286+ const call = nodeAt ( callId ) ;
287+ if ( ! call || call . kind !== 'FunctionCallExprSyntax' ) return [ ] ;
288+ const out : CallArg [ ] = [ ] ;
289+ // Search shallow descendants up to depth 2 for labeled exprs
290+ const level1 = kidsOf ( callId ) ;
291+ const level2 = level1 . flatMap ( k => kidsOf ( k ) ) ;
292+ const candidateIds = [ ...level1 , ...level2 ] ;
293+ for ( const id of candidateIds ) {
294+ const n = nodeAt ( id ) ;
295+ if ( ! n ) continue ;
296+ if ( n . kind === 'LabeledExprSyntax' || n . kind === 'TupleExprElementSyntax' ) {
297+ const txt = slice ( source , n . range ) ;
298+ const m = txt . match ( / ^ \s * ( [ A - Z a - z _ ] [ A - Z a - z 0 - 9 _ ] * ) \s * : \s * / ) ;
299+ let label : string | undefined ;
300+ let valueStart = n . range . start . offset ;
301+ if ( m ) { label = m [ 1 ] ; valueStart += m [ 0 ] . length ; }
302+ const valueRange : Range = { start : { ...n . range . start , offset : valueStart } , end : n . range . end } ;
303+ const valueText = source . slice ( valueStart , n . range . end . offset ) . trim ( ) ;
304+ out . push ( { label, text : valueText , range : valueRange , nodeId : id } ) ;
305+ }
306+ }
307+ return out ;
308+ }
309+
310+ function parseValue ( text : string ) : any {
311+ const t = text . trim ( ) ;
312+ if ( t . startsWith ( '"' ) && t . endsWith ( '"' ) ) return unquote ( t ) ;
313+ if ( t === 'true' ) return true ;
314+ if ( t === 'false' ) return false ;
315+ if ( t === 'nil' || t === 'null' ) return null ;
316+ if ( / ^ - ? \d + (?: \. \d + ) ? $ / . test ( t ) ) return Number ( t ) ;
317+ if ( t . startsWith ( "[" ) && t . endsWith ( "]" ) ) {
318+ const inner = t . slice ( 1 , - 1 ) ;
319+ const parts = splitTopLevel ( inner , ',' ) ;
320+ return parts . map ( p => parseValue ( p ) ) ;
321+ }
322+ if ( t . startsWith ( "[" ) && t . includes ( ":" ) ) {
323+ // dictionary fallback when we can't rely on kind
324+ return parseDict ( text ) ;
325+ }
326+ return t ; // fallback raw
327+ }
328+
329+ function parseDict ( text : string ) : any {
330+ const body = text . trim ( ) . replace ( / ^ \[ / , '' ) . replace ( / \] $ / , '' ) ;
331+ const parts = splitTopLevel ( body , ',' ) ;
332+ const obj : any = { } ;
333+ for ( const part of parts ) {
334+ if ( ! part ) continue ;
335+ const m = part . match ( / ^ \s * ( " (?: [ ^ " \\ ] | \\ .) * " ) \s * : \s * ( [ \s \S ] * ) $ / ) ;
336+ if ( ! m ) continue ;
337+ const key = unquote ( m [ 1 ] ) ;
338+ const val = parseValue ( m [ 2 ] ) ;
339+ obj [ key ] = val ;
340+ }
341+ return obj ;
342+ }
343+
344+ function extractDictionary ( nodeId : number ) : any {
345+ const n = nodeAt ( nodeId ) ;
346+ if ( ! n ) return undefined ;
347+ const text = slice ( source , n . range ) ;
348+ return parseDict ( text ) ;
349+ }
350+
351+ function findEnclosing ( nodeId : number , kinds ?: string [ ] ) : SymbolInfo | undefined {
352+ const set = new Set ( kinds && kinds . length ? kinds : [ 'FunctionDeclSyntax' , 'ClassDeclSyntax' , 'StructDeclSyntax' ] ) ;
353+ let cur = parentOf ( nodeId ) ;
354+ while ( cur !== undefined ) {
355+ const n = nodeAt ( cur ) ;
356+ if ( n && set . has ( n . kind ) ) {
357+ const ids = byName . get ( n . name ?? '' ) ?? [ ] ;
358+ for ( const id of ids ) {
359+ const s = symbols . get ( id ) ;
360+ if ( s && s . id === cur ) return s ;
361+ }
362+ // fallback if symbol map missed it
363+ const k = classifyDeclKind ( n . kind ) ;
364+ if ( k && n . name ) {
365+ return { id : cur , kind : k , name : n . name , range : n . range , parentId : parentOf ( cur ) } ;
366+ }
367+ }
368+ cur = parentOf ( cur ! ) ;
369+ }
370+ return undefined ;
371+ }
372+
373+ function getStringLiteralValue ( nodeId : number ) : string | undefined {
374+ const n = nodeAt ( nodeId ) ;
375+ if ( ! n ) return undefined ;
376+ const t = slice ( source , n . range ) . trim ( ) ;
377+ if ( t . startsWith ( '"' ) && t . endsWith ( '"' ) ) return unquote ( t ) ;
378+ return undefined ;
379+ }
380+
183381 return {
184382 symbols,
185383 calls,
186384 refs,
187385 byName,
386+ getNode : ( id : number ) => nodeAt ( id ) ,
387+ getChildren : ( id : number ) => kidsOf ( id ) ,
388+ getParent : ( id : number ) => parentOf ( id ) ,
389+ getCalleeText : ( callId : number ) => calleeTextForCall ( callId ) ,
390+ getStringLiteralValue,
391+ getCallArgs,
392+ extractDictionary,
393+ findEnclosing,
394+ collectConstStrings : ( ) => constStrings ,
188395 findCallsByName : ( name : string ) => calls . filter ( c => c . name === name ) ,
189396 resolveNameAt : ( name : string , _offset : number ) => {
190397 const ids = byName . get ( name ) ;
0 commit comments