name-matcher.ts 154 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411
  1. /**
  2. * Name Matcher
  3. *
  4. * Handles symbol name matching for reference resolution.
  5. */
  6. import * as path from 'path';
  7. import { Language, Node } from '../types';
  8. import { UnresolvedRef, ResolvedRef, ResolutionContext, SUPERTYPE_TARGET_KINDS, isInheritanceRef, isImportableKind } from './types';
  9. import { blankStringContents, stripCommentsForRegex } from './strip-comments';
  10. import { JS_BUILT_INS } from './js-builtins';
  11. import { resolveViaImport } from './import-resolver';
  12. /**
  13. * Ceiling on how many same-named definitions a FUZZY name-match strategy will
  14. * score. A name defined more times than this is "ubiquitous" — a method/symbol
  15. * re-declared across a vendored theme or SDK (e.g. `init`/`update`/`render` on
  16. * every widget of a committed Metronic theme — #999). No directory-proximity or
  17. * receiver-word-overlap score can reliably pick THE one true target among
  18. * thousands, so the fuzzy strategies (matchByExactName's findBestMatch, and
  19. * matchMethodCall Strategy 3) decline above the ceiling instead of emitting a
  20. * low-confidence, almost-certainly-wrong edge. This also caps their per-ref cost
  21. * at O(ceiling): without it, K same-named refs each scored K candidates — the
  22. * O(K²) blow-up that pinned a core for 15-28 min at "Resolving refs … 94%" on a
  23. * repo vendoring a large JS/TS theme (#999). The PRECISE strategies are
  24. * unaffected: qualified-name, import-based, and class-name (Strategy 1/2)
  25. * resolution all still run and resolve a ubiquitous name when the context names
  26. * its exact target. Real repos top out near ~40 same-named methods, so a normal
  27. * codebase never reaches this; only bulk-vendored code does. Tune via
  28. * `CODEGRAPH_AMBIGUOUS_NAME_CEILING`.
  29. */
  30. const DEFAULT_AMBIGUOUS_NAME_CEILING = 500;
  31. function resolveAmbiguousNameCeiling(): number {
  32. const raw = process.env.CODEGRAPH_AMBIGUOUS_NAME_CEILING;
  33. if (!raw) return DEFAULT_AMBIGUOUS_NAME_CEILING;
  34. const parsed = Number.parseInt(raw, 10);
  35. return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_AMBIGUOUS_NAME_CEILING;
  36. }
  37. const AMBIGUOUS_NAME_CEILING = resolveAmbiguousNameCeiling();
  38. /**
  39. * Try to resolve a path-like reference (e.g., "snippets/drawer-menu.liquid")
  40. * by matching the filename against file nodes.
  41. */
  42. export function matchByFilePath(
  43. ref: UnresolvedRef,
  44. context: ResolutionContext
  45. ): ResolvedRef | null {
  46. // Path-like (`a/b.liquid`) OR a bare filename ending in a short extension
  47. // (`Foo.h` — an Objective-C `#import "Foo.h"`, resolved to the header by
  48. // basename). A bare ref WITHOUT an extension is a symbol name, not a file, so
  49. // leave it to the symbol-matching strategies.
  50. if (!ref.referenceName.includes('/') && !/\.[A-Za-z][A-Za-z0-9]{0,3}$/.test(ref.referenceName)) {
  51. return null;
  52. }
  53. // Extract the filename from the path
  54. const fileName = ref.referenceName.split('/').pop();
  55. if (!fileName) return null;
  56. // Search for file nodes with this name
  57. const candidates = context.getNodesByName(fileName);
  58. const fileNodes = candidates.filter(n => n.kind === 'file');
  59. if (fileNodes.length === 0) return null;
  60. // Prefer exact path match on qualified_name
  61. const exactMatch = fileNodes.find(n => n.qualifiedName === ref.referenceName || n.filePath === ref.referenceName);
  62. if (exactMatch) {
  63. return {
  64. original: ref,
  65. targetNodeId: exactMatch.id,
  66. confidence: 0.95,
  67. resolvedBy: 'file-path',
  68. };
  69. }
  70. // Fall back to suffix match (e.g., ref="snippets/foo.liquid" matches
  71. // "src/snippets/foo.liquid"). When several files share the basename — a
  72. // `#include "RNCAsyncStorage.h"` with a same-named header on another platform
  73. // (windows/code/ vs apple/) — prefer the one in the includer's own directory,
  74. // then by directory proximity / same language family. A C/C++ include (and any
  75. // bare-filename import) resolves relative to the including file, not to an
  76. // arbitrary same-named header elsewhere in the tree.
  77. const suffixMatches = fileNodes.filter(
  78. n => n.qualifiedName.endsWith(ref.referenceName) || n.filePath.endsWith(ref.referenceName)
  79. );
  80. if (suffixMatches.length > 0) {
  81. return {
  82. original: ref,
  83. targetNodeId: pickClosestFileNode(suffixMatches, ref).id,
  84. confidence: 0.85,
  85. resolvedBy: 'file-path',
  86. };
  87. }
  88. // If only one file node with this name, use it with lower confidence
  89. if (fileNodes.length === 1) {
  90. return {
  91. original: ref,
  92. targetNodeId: fileNodes[0]!.id,
  93. confidence: 0.7,
  94. resolvedBy: 'file-path',
  95. };
  96. }
  97. return null;
  98. }
  99. /**
  100. * Among several file nodes that all match a bare include/import by basename,
  101. * pick the one closest to the referencing file: same directory first, then by
  102. * directory-tree proximity, with the same language family as a tiebreak. A
  103. * C/C++ `#include "X.h"` (and any bare-filename import) resolves relative to the
  104. * including file — not to an arbitrary same-named header on another platform.
  105. */
  106. function pickClosestFileNode(candidates: Node[], ref: UnresolvedRef): Node {
  107. const dirOf = (p: string): string => {
  108. const i = p.lastIndexOf('/');
  109. return i >= 0 ? p.slice(0, i) : '';
  110. };
  111. const refDir = dirOf(ref.filePath);
  112. const sameDir = candidates.filter((c) => dirOf(c.filePath) === refDir);
  113. const pool = sameDir.length > 0 ? sameDir : candidates;
  114. let best = pool[0]!;
  115. let bestScore = -Infinity;
  116. for (const c of pool) {
  117. const score =
  118. computePathProximity(ref.filePath, c.filePath) +
  119. (sameLanguageFamily(c.language, ref.language) ? 5 : 0);
  120. if (score > bestScore) {
  121. bestScore = score;
  122. best = c;
  123. }
  124. }
  125. return best;
  126. }
  127. /**
  128. * Language families that share a type system / runtime, so a same-language-only
  129. * reference may still resolve across them (a Kotlin `Foo.BAR` can name a Java
  130. * `Foo`). Anything not listed forms its own singleton family.
  131. */
  132. const LANGUAGE_FAMILY: Record<string, string> = {
  133. java: 'jvm', kotlin: 'jvm', scala: 'jvm',
  134. swift: 'apple', objc: 'apple',
  135. // ArkTS is a TS superset — every HarmonyOS project mixes `.ets` UI with
  136. // `.ts` logic modules, so refs must cross freely between them.
  137. typescript: 'web', tsx: 'web', javascript: 'web', jsx: 'web', arkts: 'web',
  138. c: 'c', cpp: 'c',
  139. // Razor/Blazor markup names C# types — same family so `@model Foo` /
  140. // `<MyComponent/>` resolve to their `.cs` class through the cross-family gate.
  141. csharp: 'dotnet', razor: 'dotnet',
  142. };
  143. export function sameLanguageFamily(a: string, b: string): boolean {
  144. if (a === b) return true;
  145. const fa = LANGUAGE_FAMILY[a];
  146. return fa !== undefined && fa === LANGUAGE_FAMILY[b];
  147. }
  148. /**
  149. * True when `lang` belongs to a known multi-language family (jvm/apple/web/c).
  150. * Languages not listed (php, python, go, ruby, rust, dart, …) and config
  151. * formats (yaml/xml/blade) form their own singleton families and return
  152. * `false` — used to leave config↔code framework bridges (whose config side is
  153. * never a known programming-language family) out of the cross-family gate.
  154. */
  155. export function isKnownLanguageFamily(lang: string): boolean {
  156. return LANGUAGE_FAMILY[lang] !== undefined;
  157. }
  158. /**
  159. * True when `a` and `b` are two DIFFERENT *known* language families — the
  160. * signature of a coincidental cross-language name collision (a TS `import
  161. * React` matching a Swift `import React`, a C++ `#include "X.h"` matching a
  162. * same-named ObjC header on another platform). The both-*known* test is
  163. * deliberately weaker than {@link sameLanguageFamily}'s negation: a
  164. * single-file-component language that carries its own tag (`vue`/`svelte`)
  165. * importing a `.ts` module, or any singleton-family language (php/go/ruby/…),
  166. * returns `false` here and is left alone.
  167. */
  168. export function crossesKnownFamily(a: string, b: string): boolean {
  169. return isKnownLanguageFamily(a) && isKnownLanguageFamily(b) && !sameLanguageFamily(a, b);
  170. }
  171. /**
  172. * Drop cross-language candidates from a name lookup. Two regimes:
  173. * - `references` (type-usage): a type named in language X resolves to a
  174. * SAME-family type, never a coincidentally same-named symbol in another
  175. * language (the Android `BatteryManager` system class vs a JS one). Strict
  176. * same-family filter — cross-language communication is `calls`, not refs.
  177. * - `imports` (import binding): an `import`/`#include` never crosses two
  178. * KNOWN families (TS `import React` ↮ Swift `import React`). Weaker
  179. * both-known filter so `.vue`/`.svelte` (own tag) importing `.ts` survives.
  180. */
  181. function applyLanguageGate(candidates: Node[], ref: UnresolvedRef): Node[] {
  182. if (ref.referenceKind === 'references' || ref.referenceKind === 'function_ref') {
  183. return candidates.filter((c) => sameLanguageFamily(c.language, ref.language));
  184. }
  185. if (ref.referenceKind === 'imports') {
  186. return candidates.filter((c) => !crossesKnownFamily(c.language, ref.language));
  187. }
  188. return candidates;
  189. }
  190. /**
  191. * Resolve a function-as-value reference (#756) — a function name used as a
  192. * callback/function-pointer value (`register(handler)`, `o->cb = handler`,
  193. * `{ .cb = handler }`, `signal(SIGINT, handler)`). The ONLY strategy allowed
  194. * for `function_ref` refs: exact name, function/method targets only, same
  195. * language family, same-file first, and cross-file only when the match is
  196. * UNIQUE. No fuzzy fallback, no qualified-name walking — a wrong callback
  197. * edge is worse than none.
  198. */
  199. export function matchFunctionRef(
  200. ref: UnresolvedRef,
  201. context: ResolutionContext
  202. ): ResolvedRef | null {
  203. // `this.<member>` refs are resolved ONLY by the class-scoped resolver in
  204. // resolveOne (resolveThisMemberFnRef) — never by name matching here.
  205. if (ref.referenceName.startsWith('this.')) return null;
  206. // In JS/TS/Python a bare identifier can never be a method value (methods
  207. // are only reachable through a receiver — `this.m` / `self.m` /
  208. // `Cls.m`), so bare fn-refs match FUNCTIONS only. This also sidesteps the
  209. // pre-existing TS quirk of class fields extracting as method-kind nodes,
  210. // which otherwise soaked up local names passed as arguments (excalidraw
  211. // A/B finding; same pattern in vendored docopt.py). Python's `self.m`
  212. // form keeps method targets via its own capture shape. C++ likewise: a
  213. // bare identifier can only be a FREE function (member values need
  214. // `&Cls::method`). PHP string callables name global FUNCTIONS (methods
  215. // need the `[$obj, 'm']` array form, which carries its own shape). Other
  216. // languages keep method targets: C# method groups, Swift/Dart
  217. // implicit-self, Java/Kotlin method references.
  218. const bareFnOnly =
  219. ref.language === 'typescript' || ref.language === 'tsx' ||
  220. ref.language === 'javascript' || ref.language === 'jsx' ||
  221. ref.language === 'arkts' ||
  222. ref.language === 'cpp' || ref.language === 'python' ||
  223. ref.language === 'php';
  224. // Python additionally accepts CLASS targets for bare identifiers (#1478):
  225. // class-as-value is a core Python idiom (`return SomeSerializer`,
  226. // `Meta.model = Org`, registry dicts, `admin.site.register(Model, Admin)`)
  227. // and, unlike TS, Python has no type-annotation recovery path. The
  228. // false-positive mechanism behind the function-only rule was lowercase
  229. // locals colliding with same-named METHODS (docopt.py) — a candidate must
  230. // be an exact-name CLASS node here, and the extraction gate (same-file
  231. // class ∪ imports) plus unique-or-drop still apply. Methods stay excluded.
  232. const bareClassOk = ref.language === 'python';
  233. // Qualified member-pointer (`&Widget::on_click` → "Widget::on_click"):
  234. // resolve the member ON THAT SCOPE — exempt from bareFnOnly (the `&Cls::m`
  235. // shape is an explicit member reference). Unique-or-drop like everything else.
  236. if (ref.referenceName.includes('::')) {
  237. const memberName = ref.referenceName.slice(ref.referenceName.lastIndexOf('::') + 2);
  238. const scoped = context
  239. .getNodesByName(memberName)
  240. .filter(
  241. (n) =>
  242. (n.kind === 'function' || n.kind === 'method') &&
  243. sameLanguageFamily(n.language, ref.language) &&
  244. n.id !== ref.fromNodeId &&
  245. (n.qualifiedName === ref.referenceName ||
  246. n.qualifiedName.endsWith(`::${ref.referenceName}`))
  247. );
  248. if (scoped.length === 0) return null;
  249. const sameFileScoped = scoped.filter((n) => n.filePath === ref.filePath);
  250. const pool = sameFileScoped.length > 0 ? sameFileScoped : scoped;
  251. if (sameFileScoped.length === 0 && scoped.length > 1) return null;
  252. const target = pool.reduce((a, b) => (a.startLine <= b.startLine ? a : b));
  253. return {
  254. original: ref,
  255. targetNodeId: target.id,
  256. confidence: 0.9,
  257. resolvedBy: 'function-ref',
  258. };
  259. }
  260. let candidates = context
  261. .getNodesByName(ref.referenceName)
  262. .filter(
  263. (n) =>
  264. (n.kind === 'function' ||
  265. (!bareFnOnly && n.kind === 'method') ||
  266. (bareClassOk && n.kind === 'class')) &&
  267. sameLanguageFamily(n.language, ref.language) &&
  268. n.id !== ref.fromNodeId // a function registering itself is not a dependency edge
  269. );
  270. if (candidates.length === 0) return null;
  271. // Swift implicit-self: a bare identifier can name a METHOD only of the
  272. // ENCLOSING type (`Button(action: handleTap)` written inside that type) —
  273. // a same-named method on any OTHER class is a parameter collision
  274. // (Alamofire: a `request` parameter resolving to EventMonitor::request).
  275. // Scope method candidates to the from-symbol's type; top-level code has no
  276. // implicit self, so method targets are excluded there entirely. Free
  277. // functions are unaffected.
  278. if (ref.language === 'swift' && candidates.some((n) => n.kind === 'method')) {
  279. const fromNode = context.getNodeById?.(ref.fromNodeId);
  280. const sep = fromNode ? fromNode.qualifiedName.lastIndexOf('::') : -1;
  281. const classPrefix = fromNode && sep > 0 ? fromNode.qualifiedName.slice(0, sep) : null;
  282. candidates = candidates.filter((n) => {
  283. if (n.kind !== 'method') return true;
  284. if (!classPrefix) return false;
  285. const mSep = n.qualifiedName.lastIndexOf('::');
  286. if (mSep <= 0) return false;
  287. const methodPrefix = n.qualifiedName.slice(0, mSep);
  288. // Accept exact-scope matches plus suffix relationships either way, so
  289. // extension-declared members (`Holder::m`) still match a nested
  290. // from-scope (`Module::Holder::wire`) and vice versa.
  291. return (
  292. methodPrefix === classPrefix ||
  293. methodPrefix.endsWith(`::${classPrefix}`) ||
  294. classPrefix.endsWith(`::${methodPrefix}`)
  295. );
  296. });
  297. if (candidates.length === 0) return null;
  298. }
  299. // Same-file definition wins — the extraction gate guarantees most survivors
  300. // have one, and it's the dominant C pattern (static callback registered in
  301. // a same-file ops struct).
  302. const sameFile = candidates.filter((n) => n.filePath === ref.filePath);
  303. if (sameFile.length > 0) {
  304. // Swift: several same-named METHODS in one file is an API overload family
  305. // (`Session.request(...)` × N), and a bare identifier hitting it is almost
  306. // always a same-named parameter, not a method value (Alamofire A/B
  307. // finding) — refuse rather than guess. A single method (SwiftUI's
  308. // `action: handleTap`) still resolves.
  309. if (
  310. ref.language === 'swift' &&
  311. sameFile.length > 1 &&
  312. sameFile.every((n) => n.kind === 'method')
  313. ) {
  314. return null;
  315. }
  316. // Same-name overloads in one file are the same conceptual symbol; pick
  317. // the first by position for determinism.
  318. const target = sameFile.reduce((a, b) => (a.startLine <= b.startLine ? a : b));
  319. return {
  320. original: ref,
  321. targetNodeId: target.id,
  322. confidence: sameFile.length === 1 ? 0.95 : 0.9,
  323. resolvedBy: 'function-ref',
  324. };
  325. }
  326. // Cross-file (imported names the import resolver didn't already claim):
  327. // only an unambiguous match resolves.
  328. if (candidates.length === 1) {
  329. return {
  330. original: ref,
  331. targetNodeId: candidates[0]!.id,
  332. confidence: 0.8,
  333. resolvedBy: 'function-ref',
  334. };
  335. }
  336. return null;
  337. }
  338. /** Languages with no nested named functions: nesting in the graph is never a scope. */
  339. const NO_NESTED_FUNCTIONS = new Set<string>(['c', 'cpp']);
  340. /**
  341. * A function nested inside another FUNCTION is only callable from within its
  342. * container — Python, JS/TS, and every closure language scope it lexically.
  343. * Resolving a bare name from elsewhere to a nested local fabricates an edge
  344. * scope already rules out: `join(...)` in one function must never bind to a
  345. * `join` defined inside a DIFFERENT function (#1230). A candidate whose
  346. * qualifiedName parent is a same-file function/method is kept only when the
  347. * ref originates inside that parent's line range. Class members are
  348. * unaffected (their parent resolves to a class-like node), as are top-level
  349. * symbols and C++ namespace-prefixed names (the prefix has no node).
  350. */
  351. function isLexicallyReachable(
  352. candidate: Node,
  353. ref: UnresolvedRef,
  354. context: ResolutionContext
  355. ): boolean {
  356. if (candidate.kind !== 'function') return true;
  357. // C and C++ have no nested named functions, so a function the graph shows
  358. // inside another is an extraction artifact, not a scope: tree-sitter-c
  359. // cannot parse a macro call whose arguments are designated initializers
  360. // (betaflight's `RESET_CONFIG(pidProfile_t, pidProfile, .pid = {…})`), and
  361. // its error recovery runs the enclosing function_definition to the end of
  362. // the file, nesting every function after it. Trusting that nesting rejected
  363. // 117 real calls into pid.c on that tree; the functions are reachable.
  364. if (NO_NESTED_FUNCTIONS.has(candidate.language)) return true;
  365. const qn = candidate.qualifiedName;
  366. if (!qn || !qn.includes('::')) return true;
  367. const parentQn = qn.slice(0, qn.lastIndexOf('::'));
  368. const containers = context
  369. .getNodesByQualifiedName(parentQn)
  370. .filter(
  371. (p) =>
  372. p.filePath === candidate.filePath &&
  373. (p.kind === 'function' || p.kind === 'method') &&
  374. p.startLine <= candidate.startLine &&
  375. p.endLine >= candidate.endLine
  376. );
  377. if (containers.length === 0) return true;
  378. return (
  379. ref.filePath === candidate.filePath &&
  380. containers.some((p) => ref.line >= p.startLine && ref.line <= p.endLine)
  381. );
  382. }
  383. /** Languages whose module boundary is `import`/`export` (or CommonJS). */
  384. const ESM_FAMILY = new Set<string>(['typescript', 'tsx', 'javascript', 'jsx', 'arkts']);
  385. /**
  386. * A line-initial `import` statement — the marker that a JS/TS file is a MODULE
  387. * rather than a classic script. Line-anchored and followed by a name, brace,
  388. * star or quote, so a dynamic `import(` and the word inside a comment or string
  389. * do not match.
  390. */
  391. const HAS_IMPORT_STATEMENT = /^[ \t]*import[\s{*'"]/m;
  392. /**
  393. * Anything the file could offer another file, in every form the extractor's own
  394. * `isExported` flag misses. `^export` covers the declaration and later forms
  395. * (`export const`, `export { x }`, `export default x`, `export *`); the
  396. * CommonJS shapes cover files that never use ESM syntax at all, in both the dot
  397. * and the bracket form; and `declare global` contributes names to every file
  398. * whether or not the module exports anything of its own. Kept as a source test
  399. * rather than a node scan precisely because `isExported` is set only from an
  400. * `export_statement` ancestor, so `const x = …; export { x }` and
  401. * `module.exports = { x }` both read as unexported on the node.
  402. */
  403. const HAS_ESM_EXPORT = /^[ \t]*export[\s{*]|^[ \t]*declare\s+global\b/m;
  404. const HAS_CJS_EXPORT = /\bmodule\.exports\b|\bexports\s*[.[]/;
  405. /**
  406. * Per-context memo of "this file is a module that exports nothing", asked once
  407. * per candidate FILE rather than once per reference. Derived from file source,
  408. * so it drops with the context's file caches — clearNameMatcherMemos deletes it
  409. * alongside INFER_SCAN_STATES.
  410. */
  411. const SEALED_MODULES = new WeakMap<ResolutionContext, Map<string, boolean>>();
  412. /**
  413. * Whether `filePath` is a JS/TS module that exports NOTHING — an import
  414. * statement present, no export of any form. No reference from another file can
  415. * reach any binding in such a file, so every one of its symbols is a false
  416. * candidate for a cross-file name match.
  417. *
  418. * This is the general case behind a package name capturing a same-named local:
  419. * on `vitejs/vite`, 157 cross-file `imports` refs — every `import { defineConfig
  420. * } from 'vite'` in the playground and the create-vite templates — resolved onto
  421. * `playground/ssr-html/test-stacktrace.js::vite`, which is `const vite = await
  422. * createServer(…)` at module scope in a file with zero exports. The existing
  423. * guards cannot see it: `isLexicallyReachable` returns early for any candidate
  424. * that is not a `function`, and the bare-import guard correctly declines because
  425. * `vite` IS a workspace member, so the specifier really is project-local. What
  426. * is wrong is only which node the name lands on.
  427. *
  428. * Deliberately narrow on three axes, because each is a class this would
  429. * otherwise resolve wrongly in the opposite direction:
  430. *
  431. * - **A classic script is exempt.** Requiring an `import` statement means a
  432. * non-module `.js` file — concatenated globals, a browser `<script>` — keeps
  433. * its cross-file matches, where a top-level binding genuinely is reachable.
  434. * - **CommonJS is exempt.** `module.exports` and `exports.x` are matched as
  435. * exports, so a CJS file is never sealed.
  436. * - **Other languages are exempt.** Go, Python, Java and the rest have no
  437. * equivalent boundary, and several extractors hardcode `isExported`.
  438. */
  439. function isSealedModule(filePath: string, context: ResolutionContext): boolean {
  440. let memo = SEALED_MODULES.get(context);
  441. if (!memo) {
  442. memo = new Map();
  443. SEALED_MODULES.set(context, memo);
  444. }
  445. const hit = memo.get(filePath);
  446. if (hit !== undefined) return hit;
  447. const source = context.readFile(filePath);
  448. const code = source === null ? '' : blankStringContents(stripCommentsForRegex(source, 'typescript'));
  449. // CommonJS assignments can execute inside template interpolations, which the
  450. // masker blanks. Keep the conservative raw-source exemption for those forms.
  451. const sealed =
  452. source !== null && HAS_IMPORT_STATEMENT.test(code) &&
  453. !context.getNodesInFile(filePath).some((n) => n.isExported) &&
  454. !HAS_ESM_EXPORT.test(code) && !HAS_CJS_EXPORT.test(source);
  455. memo.set(filePath, sealed);
  456. return sealed;
  457. }
  458. /**
  459. * Whether `candidate` can be named by a reference in `ref`'s file at all.
  460. * Both name-based strategies validate their chosen candidate. Removing an
  461. * unreachable candidate before ranking can promote an unrelated runner-up;
  462. * rejecting the chosen target must leave the reference unresolved instead.
  463. */
  464. function isCrossFileReachable(
  465. candidate: Node,
  466. ref: UnresolvedRef,
  467. context: ResolutionContext
  468. ): boolean {
  469. if ((ref.language as string) !== 'markdown' && (candidate.language as string) === 'markdown') return false;
  470. if (ref.referenceKind === 'calls' && ESM_FAMILY.has(candidate.language) &&
  471. (candidate.kind === 'constant' || candidate.kind === 'variable') &&
  472. /^=\s*require\s*\(\s*(['"])[^'"]+\.json\1\s*\)\s*;?\s*$/.test(candidate.signature ?? '')) return false;
  473. return (
  474. candidate.filePath === ref.filePath ||
  475. !ESM_FAMILY.has(candidate.language) ||
  476. !isSealedModule(candidate.filePath, context)
  477. );
  478. }
  479. /**
  480. * Languages in which `visibility: 'private'` on a definition means no other
  481. * FILE can name it: a Kotlin `private fun` is file- or class-local, and the
  482. * same holds for Java, C#, Swift, Scala, Dart and PHP members.
  483. */
  484. const PRIVATE_IS_FILE_LOCAL = new Set<string>(['kotlin', 'java', 'csharp', 'swift', 'scala', 'dart', 'php']);
  485. /** Per-context memo: node id → "this C/C++ function is declared `static`". */
  486. const C_STATIC_MEMO = new WeakMap<ResolutionContext, Map<string, boolean>>();
  487. /**
  488. * A C/C++ file that IS a translation unit. A `static` defined here is local
  489. * to it. A `static` (typically `static inline`) in a header is a different
  490. * thing: the header is textually included, so the function exists in every
  491. * unit that includes it and is callable from each — MAVLink's generated
  492. * `mavlink_msg_*.h` are nothing but such functions, 4,306 real calls on one
  493. * betaflight tree.
  494. */
  495. const C_SOURCE_EXT = /\.(c|cc|cpp|cxx|c\+\+|m|mm)$/i;
  496. /**
  497. * Whether a C/C++ function definition carries the `static` storage class —
  498. * read from its first source line(s), since the extractor records no storage
  499. * class and the kernel arm would need the same field. `static` on the line
  500. * above the name (`static void\nfoo(void)`) is the common alternative layout.
  501. */
  502. function isStaticCFunction(candidate: Node, context: ResolutionContext): boolean {
  503. let memo = C_STATIC_MEMO.get(context);
  504. if (!memo) {
  505. memo = new Map();
  506. C_STATIC_MEMO.set(context, memo);
  507. }
  508. const hit = memo.get(candidate.id);
  509. if (hit !== undefined) return hit;
  510. const lines = context.getFileLines?.(candidate.filePath) ?? context.readFile(candidate.filePath)?.split('\n') ?? [];
  511. const head = [lines[candidate.startLine - 2] ?? '', lines[candidate.startLine - 1] ?? ''].join('\n');
  512. const isStatic = /(^|[\s;}])static\s/.test(head);
  513. memo.set(candidate.id, isStatic);
  514. return isStatic;
  515. }
  516. /** Per-context memo: node id → "this Rust method implements a trait". */
  517. const RUST_TRAIT_IMPL_MEMO = new WeakMap<ResolutionContext, Map<string, boolean>>();
  518. /**
  519. * Whether a Rust method sits in an `impl Trait for Type` block. Such a method
  520. * carries no `pub` — the trait decides its visibility — so the extractor
  521. * records it as private; it is reachable wherever the trait is. Read from the
  522. * nearest enclosing `impl` header above the method, memoised per node.
  523. */
  524. function isRustTraitImplMethod(candidate: Node, context: ResolutionContext): boolean {
  525. if (candidate.kind !== 'method') return false;
  526. let memo = RUST_TRAIT_IMPL_MEMO.get(context);
  527. if (!memo) {
  528. memo = new Map();
  529. RUST_TRAIT_IMPL_MEMO.set(context, memo);
  530. }
  531. const hit = memo.get(candidate.id);
  532. if (hit !== undefined) return hit;
  533. const lines = context.getFileLines?.(candidate.filePath) ?? context.readFile(candidate.filePath)?.split('\n') ?? [];
  534. let isTrait = false;
  535. for (let i = candidate.startLine - 2; i >= 0; i--) {
  536. const line = lines[i] ?? '';
  537. if (/^\s*(pub(\([^)]*\))?\s+)?(unsafe\s+)?impl\b/.test(line)) {
  538. isTrait = /\sfor\s/.test(line.replace(/\/\/.*$/, ''));
  539. break;
  540. }
  541. // A top-level item above the method means it was not inside an impl.
  542. if (/^(pub(\([^)]*\))?\s+)?(fn|struct|enum|mod|trait|const|static|type)\b/.test(line)) break;
  543. }
  544. memo.set(candidate.id, isTrait);
  545. return isTrait;
  546. }
  547. /**
  548. * The directory a Rust file's private items are visible from: the file's own
  549. * module subtree. `src/net.rs` and `src/net/mod.rs` own `src/net/`; a crate
  550. * root (`lib.rs` / `main.rs`) owns its directory. A child module reaches its
  551. * ancestors' private items (`super::`), a sibling or another crate never does.
  552. */
  553. function rustModuleDir(filePath: string): string {
  554. const base = path.posix.basename(filePath);
  555. const dir = path.posix.dirname(filePath);
  556. if (base === 'mod.rs' || base === 'lib.rs' || base === 'main.rs') return dir;
  557. return path.posix.join(dir, base.replace(/\.rs$/, ''));
  558. }
  559. /**
  560. * Whether `candidate` can be NAMED from a reference in `ref`'s file at all,
  561. * given what its language says about the definition's visibility. A
  562. * definition the language makes file-local is not a candidate for a
  563. * cross-file name match, however well the names agree:
  564. *
  565. * - **C / C++**: a `static` function defined in a SOURCE file is local to
  566. * that translation unit; one in a header is part of every unit that
  567. * includes it and stays visible. On a 2,109-file betaflight tree 145
  568. * cross-file calls resolved onto a `static` in another `.c` (#1730) —
  569. * `usbd_get_descriptor` onto the `static get_device_descriptor` of
  570. * whichever USB class file ranked first.
  571. * - **Kotlin, Java, C#, Swift, Scala, Dart, PHP**: `private` is class- or
  572. * file-local. An Android `editor.apply()` resolved onto an unrelated class's
  573. * `private fun apply`.
  574. * - **Go**: an unexported (lowercase) identifier is package-local, and a
  575. * package is a directory. Judged by the name's case: the extractor's
  576. * `isExported` is unset for every Go method.
  577. * - **Rust**: a non-`pub` item is visible to its module and that module's
  578. * descendants, never to a sibling module or another crate — `.count()` on
  579. * an iterator resolved onto a `fn count` in a different crate. A method in
  580. * an `impl Trait for Type` block has the trait's visibility, not `private`.
  581. * - **JS / TS / ArkTS**: a binding in a module that exports nothing (an
  582. * `import` present, no `export` / CommonJS / `declare global`) is sealed —
  583. * the vite playground's `const vite = await createServer(…)` took 157
  584. * `import { defineConfig } from 'vite'` edges (#1719). Classic scripts,
  585. * CommonJS, later `export { … }`, and ambient globals stay visible.
  586. *
  587. * Same-file candidates are always visible. Applied by ReferenceResolver to
  588. * the target the whole name-matching pipeline settled on, so a rejection ends
  589. * the reference unresolved: declining inside matchByExactName instead let the
  590. * ref fall through to matchFuzzy, which then committed to a same-language
  591. * namesake the ranking had passed over — eight such edges on one tree, all
  592. * onto a local `const fail = …` arrow the graph does not hold. matchFuzzy
  593. * checks its own survivor as well, since nothing runs after it.
  594. */
  595. export function isVisibleAcrossFiles(candidate: Node, ref: UnresolvedRef, context: ResolutionContext): boolean {
  596. if (candidate.filePath === ref.filePath) return true;
  597. const lang = candidate.language as string;
  598. if (lang === 'c' || lang === 'cpp') {
  599. return (
  600. candidate.kind !== 'function' ||
  601. !C_SOURCE_EXT.test(candidate.filePath) ||
  602. !isStaticCFunction(candidate, context)
  603. );
  604. }
  605. if (lang === 'go') {
  606. // By the name's first letter, not the extractor's flag: the flag is unset
  607. // for every Go method, exported or not.
  608. return /^[A-Z]/.test(candidate.name) || path.posix.dirname(candidate.filePath) === path.posix.dirname(ref.filePath);
  609. }
  610. if (lang === 'rust') {
  611. if (candidate.visibility !== 'private') return true;
  612. if (isRustTraitImplMethod(candidate, context)) return true;
  613. const owner = rustModuleDir(candidate.filePath);
  614. return ref.filePath.startsWith(owner + '/');
  615. }
  616. if (PRIVATE_IS_FILE_LOCAL.has(lang)) return candidate.visibility !== 'private';
  617. // JS/TS/ArkTS sealed modules + markdown/JSON call-target guards (#1719).
  618. // Same predicate matchByExactName / matchFuzzy apply to their survivors so a
  619. // rejection here cannot fall through to a promoted runner-up.
  620. return isCrossFileReachable(candidate, ref, context);
  621. }
  622. const JS_FAMILY = new Set<string>(['typescript', 'tsx', 'javascript', 'jsx']);
  623. /**
  624. * Whether a JS/TS `calls` ref is a RECEIVER-LESS call — `serialize(x)`, not
  625. * `this.serialize(x)` / `obj.serialize(x)`. The extractor emits `this.m()`
  626. * and `super.m()` under the bare method name, so the receiver is read back
  627. * from the call site's own line: the text at the ref's column is the call
  628. * expression, and it starts with the name itself only when nothing precedes
  629. * it. In JS/TS a bare call can never bind to a class method (methods need a
  630. * receiver), so a `method` node is not a candidate for it (#1714) — the
  631. * enclosing method itself least of all, which the same-file proximity term
  632. * used to pick over the module-scope function the call actually means.
  633. */
  634. function isBareJsCall(ref: UnresolvedRef, context: ResolutionContext): boolean {
  635. if (ref.referenceKind !== 'calls' || !JS_FAMILY.has(ref.language)) return false;
  636. if (ref.referenceName.includes('.')) return false;
  637. const line = context.getFileLines?.(ref.filePath)?.[ref.line - 1]
  638. ?? context.readFile(ref.filePath)?.split('\n')[ref.line - 1];
  639. if (line === undefined) return false;
  640. const at = line.slice(ref.column);
  641. const nameEsc = ref.referenceName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  642. if (!new RegExp('^' + nameEsc + '\\s*[(<]').test(at)) return false;
  643. // Nothing but whitespace, an operator or an opener may precede a bare call.
  644. return !/[.\w$\]\)]\s*$/.test(line.slice(0, ref.column)) || /\b(?:return|await|yield|typeof|void|new|else|case|throw|in|of|instanceof)\s*$/.test(line.slice(0, ref.column));
  645. }
  646. /** Per-context memo: `file\0name` → "the file binds this name locally". */
  647. const LOCAL_BINDING_MEMO = new WeakMap<ResolutionContext, Map<string, boolean>>();
  648. /**
  649. * Whether a JS/TS file binds `name` itself — as a `const`/`let`/`var`/
  650. * `function`/`class` declaration (destructuring included) or as a parameter
  651. * of a function or arrow. Such a binding shadows every same-named symbol in
  652. * other files, so a bare call to it has no cross-file candidate: the
  653. * `resolve` of `new Promise((resolve, reject) => …)`, a spec's
  654. * `const transform = await makeTransform()`, a factory's `const now =
  655. * options.now || (() => new Date())`. None of these is a node the graph
  656. * holds (a parameter, a const bound to a call result), so without this the
  657. * matcher hands the call to whichever other file defines the name — and
  658. * once methods stop being candidates for a bare call (#1714), the function
  659. * that was out-ranked steps in. Read from source, memoised per file+name.
  660. */
  661. function isLocallyBoundJsName(name: string, filePath: string, context: ResolutionContext): boolean {
  662. let memo = LOCAL_BINDING_MEMO.get(context);
  663. if (!memo) {
  664. memo = new Map();
  665. LOCAL_BINDING_MEMO.set(context, memo);
  666. }
  667. const key = filePath + '\0' + name;
  668. const hit = memo.get(key);
  669. if (hit !== undefined) return hit;
  670. const source = context.readFile(filePath) ?? '';
  671. const n = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  672. // `const { name } = require('./m')` / `= await import('./m')` binds an IMPORT,
  673. // not a shadow: the symbol lives in the other file and the call means it.
  674. const declRe = new RegExp(
  675. '\\b(?:const|let|var)\\s+(?:' + n + '\\b|[{\\[][^;=]*?\\b' + n + '\\b[^;=]*?[}\\]])\\s*(?:=\\s*([^;\\n]*))?',
  676. 'g'
  677. );
  678. let bound = false;
  679. for (const m of source.matchAll(declRe)) {
  680. if (!/^\s*(?:await\s+)?(?:require|import)\s*\(/.test(m[1] ?? '')) { bound = true; break; }
  681. }
  682. if (!bound) {
  683. bound =
  684. new RegExp('\\b(?:function|class)\\s+' + n + '\\b').test(source) ||
  685. // a parameter: every token before the name in the list is itself a
  686. // parameter (identifier, optional type, optional default) — so a string
  687. // argument containing the word cannot match.
  688. new RegExp(
  689. '\\(\\s*(?:(?:\\.\\.\\.)?[\\w$]+(?:\\s*\\??\\s*:\\s*[^,()]+)?(?:\\s*=\\s*[^,()]+)?\\s*,\\s*)*' +
  690. n + '\\b(?:\\s*\\??\\s*:[^,()]*)?(?:\\s*=[^,()]*)?(?:\\s*,\\s*[^()]*)?\\)\\s*(?::[^=;{]*)?(?:=>|\\{)'
  691. ).test(source) ||
  692. new RegExp('(?:^|[^\\w$.])' + n + '\\s*=>').test(source);
  693. }
  694. memo.set(key, bound);
  695. return bound;
  696. }
  697. /**
  698. * Try to resolve a reference by exact name match
  699. */
  700. export function matchByExactName(
  701. ref: UnresolvedRef,
  702. context: ResolutionContext
  703. ): ResolvedRef | null {
  704. // `import`-kind nodes are import STATEMENTS, not definitions, so a reference
  705. // resolving to a sibling file's `import` is a meaningless edge — the real
  706. // import→definition resolution is the import resolver's job (resolveViaImport),
  707. // never name-matching here. Excluding them also removes a quadratic blow-up:
  708. // a ubiquitous package (`react`, `@superset-ui/core`, Python `logging`/`typing`)
  709. // is re-declared as an `import` node in every file that imports it, so K
  710. // unresolved import refs each scored K same-named import candidates through
  711. // findBestMatch — O(K²) per package, the dominant cost of "Resolving refs" on
  712. // large import-heavy (front-end + back-end) repos (#915).
  713. const bareJs = isBareJsCall(ref, context);
  714. if (bareJs) {
  715. const storeAction = matchJsStoreBindingCall(ref, context);
  716. if (storeAction) return storeAction;
  717. }
  718. const candidates = applyLanguageGate(context.getNodesByName(ref.referenceName), ref)
  719. .filter((n) => n.kind !== 'import')
  720. // Nested locals are only reachable from inside their container (#1230).
  721. .filter((n) => isLexicallyReachable(n, ref, context))
  722. // Preserve import ranking; calls reject the winner without promoting another.
  723. .filter((n) => ref.referenceKind !== 'imports' || n.filePath === ref.filePath ||
  724. !ESM_FAMILY.has(n.language) || !isSealedModule(n.filePath, context))
  725. // A receiver-less JS/TS call cannot reach a method (#1714).
  726. .filter((n) => !(bareJs && n.kind === 'method'))
  727. // A name the file binds itself (a parameter, a const) shadows every other
  728. // file's symbol of that name, so a bare call has no cross-file candidate.
  729. .filter((n) => !(bareJs && n.filePath !== ref.filePath && isLocallyBoundJsName(ref.referenceName, ref.filePath, context)))
  730. // An `extends`/`implements` ref names a supertype, so anything that can't
  731. // BE one is not a candidate at all. This is eligibility, not
  732. // ranking: kind is only a scoring bonus below (and none is awarded for
  733. // inheritance refs), so without this a same-named `enum_member` outranked
  734. // the real `trait`, and as the sole candidate was adopted outright by the
  735. // single-match shortcut. Restricting the pool BEFORE ranking lets the
  736. // legitimate supertype win instead of merely dropping the false edge.
  737. .filter((n) => !isInheritanceRef(ref) || SUPERTYPE_TARGET_KINDS.has(n.kind))
  738. // Likewise for `imports`: a member that only exists inside a type is not
  739. // importable, so it is not a candidate. Without this a `path`/`id`/`url`
  740. // import resolved to some interface's same-named property.
  741. .filter((n) => ref.referenceKind !== 'imports' || isImportableKind(n.kind));
  742. if (candidates.length === 0) {
  743. return null;
  744. }
  745. // If only one match, use it — but penalize cross-language matches
  746. if (candidates.length === 1) {
  747. if (!isCrossFileReachable(candidates[0]!, ref, context)) return null;
  748. const isCrossLanguage = candidates[0]!.language !== ref.language;
  749. return {
  750. original: ref,
  751. targetNodeId: candidates[0]!.id,
  752. confidence: isCrossLanguage ? 0.5 : 0.9,
  753. resolvedBy: 'exact-match',
  754. };
  755. }
  756. // Ubiquitous-name ceiling (#999): above it, picking one target among K
  757. // same-named defs by directory proximity is unreliable AND O(K) per ref — the
  758. // quadratic behind the "Resolving refs" wedge on theme/SDK-vendoring repos.
  759. // Decline; the precise strategies (qualified-name, import, class-name) already
  760. // ran. Falls through to fuzzy, which itself only resolves a UNIQUE candidate.
  761. if (candidates.length > AMBIGUOUS_NAME_CEILING) {
  762. return null;
  763. }
  764. // Multiple matches - try to narrow down
  765. const bestMatch = findBestMatch(ref, candidates, context);
  766. if (bestMatch && isCrossFileReachable(bestMatch, ref, context)) {
  767. // Lower confidence when the match is from a distant/unrelated module
  768. const proximity = computePathProximity(ref.filePath, bestMatch.filePath);
  769. const confidence = proximity >= 30 ? 0.7 : 0.4;
  770. return {
  771. original: ref,
  772. targetNodeId: bestMatch.id,
  773. confidence,
  774. resolvedBy: 'exact-match',
  775. };
  776. }
  777. return null;
  778. }
  779. /**
  780. * Try to resolve by qualified name
  781. */
  782. export function matchByQualifiedName(
  783. ref: UnresolvedRef,
  784. context: ResolutionContext
  785. ): ResolvedRef | null {
  786. // Check if the reference name looks qualified (contains :: or .)
  787. if (!ref.referenceName.includes('::') && !ref.referenceName.includes('.')) {
  788. return null;
  789. }
  790. // A method call `receiver.method()` can share an exact qualified name with a
  791. // config-file key: `service.process()` (a `calls` ref named `service.process`)
  792. // vs the yaml key `service.process`. Config keys are bound to their code refs
  793. // upstream by the framework resolvers (`@Value` → `references`); a `calls` ref
  794. // must never resolve to a yaml/properties config node — that's a wrong edge
  795. // AND it hides the real callee. Drop those from both the exact and the partial
  796. // candidate sets so resolution falls through to method resolution below (#1180).
  797. const keepForRef = (nodes: Node[]): Node[] =>
  798. ref.referenceKind === 'calls'
  799. ? nodes.filter(
  800. (n) => !(n.kind === 'constant' && (n.language === 'yaml' || n.language === 'properties')),
  801. )
  802. : nodes;
  803. const candidates = keepForRef(context.getNodesByQualifiedName(ref.referenceName));
  804. if (candidates.length === 1) {
  805. return {
  806. original: ref,
  807. targetNodeId: candidates[0]!.id,
  808. confidence: 0.95,
  809. resolvedBy: 'qualified-name',
  810. };
  811. }
  812. // Several symbols share this exact qualified name (e.g. `Logger::log` declared
  813. // in two files — an ODR clash or separate translation units): prefer the one
  814. // in the call site's own file before the partial-match fallback below, else
  815. // the first-indexed def wins and a call in `b/svc` targets `a/svc` (#1079).
  816. if (candidates.length > 1) {
  817. const ordered = preferCallSiteFile(candidates, ref.filePath);
  818. if (ordered[0]!.filePath === ref.filePath) {
  819. return {
  820. original: ref,
  821. targetNodeId: ordered[0]!.id,
  822. confidence: 0.95,
  823. resolvedBy: 'qualified-name',
  824. };
  825. }
  826. }
  827. // Erlang qualified refs (#1610): every erlang function's qualifiedName
  828. // carries its arity (`mod::f/2`), and refs carry the call-site arity when it
  829. // is statically known.
  830. if (ref.language === 'erlang' && ref.referenceName.includes('::')) {
  831. // A ref WITH arity that missed the exact lookup names an arity that isn't
  832. // defined (or a module out of repo). Never fall through to the partial
  833. // match — its "last segment" would be the arity digits — and never settle
  834. // for a sibling arity: silent beats wrong.
  835. if (/\/\d{1,3}$/.test(ref.referenceName)) return null;
  836. // An arity-LESS qualified ref (dynamic MFA whose args list wasn't a
  837. // static literal): resolve only when the module defines exactly ONE arity
  838. // of that function; several arities with no signal is a guess.
  839. const base = ref.referenceName.slice(ref.referenceName.lastIndexOf('::') + 2);
  840. const prefix = `${ref.referenceName}/`;
  841. const arityCands = keepForRef(context.getNodesByName(base)).filter(
  842. (n) =>
  843. n.qualifiedName.startsWith(prefix) && /^\d{1,3}$/.test(n.qualifiedName.slice(prefix.length)),
  844. );
  845. if (arityCands.length === 1) {
  846. return {
  847. original: ref,
  848. targetNodeId: arityCands[0]!.id,
  849. confidence: 0.85,
  850. resolvedBy: 'qualified-name',
  851. };
  852. }
  853. return null;
  854. }
  855. // Try partial qualified name match — again preferring the call site's own
  856. // file when more than one symbol's qualifiedName ends with the reference.
  857. const parts = ref.referenceName.split(/[:.]/);
  858. const lastName = parts[parts.length - 1];
  859. if (lastName) {
  860. const partialCandidates = keepForRef(context.getNodesByName(lastName))
  861. .filter((candidate) => candidate.qualifiedName.endsWith(ref.referenceName));
  862. const chosen = preferCallSiteFile(partialCandidates, ref.filePath)[0];
  863. if (chosen) {
  864. return {
  865. original: ref,
  866. targetNodeId: chosen.id,
  867. confidence: 0.85,
  868. resolvedBy: 'qualified-name',
  869. };
  870. }
  871. }
  872. return null;
  873. }
  874. /**
  875. * When a symbol name is ambiguous across files, prefer the candidate(s) declared
  876. * in the call site's own file, keeping the rest in their original order (#1079).
  877. * A same-file definition is the strongest language-agnostic signal for which of
  878. * several same-named symbols a call means; without it, resolution collapses onto
  879. * whichever was indexed first, so a call in `b/svc` wrongly targets `a/svc`.
  880. * No-op when there are <2 candidates or none share the call site's file.
  881. */
  882. export function preferCallSiteFile(nodes: Node[], callSiteFile: string): Node[] {
  883. if (nodes.length < 2) return nodes;
  884. const same: Node[] = [];
  885. const other: Node[] = [];
  886. for (const n of nodes) {
  887. if (n.filePath === callSiteFile) same.push(n);
  888. else other.push(n);
  889. }
  890. return same.length ? [...same, ...other] : nodes;
  891. }
  892. /**
  893. * Languages whose object literals declare callable members — `export const
  894. * api = { call() {…}, get: () => {…} }` used as a namespace (#1573).
  895. */
  896. const OBJECT_LITERAL_LANGUAGES = new Set<string>(['typescript', 'tsx', 'javascript', 'jsx', 'arkts']);
  897. /** True when `inner`'s source range lies within `outer`'s (lines, then columns on a shared line). */
  898. function rangeWithin(inner: Node, outer: Node): boolean {
  899. const innerEnd = inner.endLine ?? inner.startLine;
  900. const outerEnd = outer.endLine ?? outer.startLine;
  901. if (inner.startLine < outer.startLine || innerEnd > outerEnd) return false;
  902. if (inner.startLine === outer.startLine && inner.startColumn < outer.startColumn) return false;
  903. if (innerEnd === outerEnd && inner.endColumn > outer.endColumn) return false;
  904. return true;
  905. }
  906. function sameRange(a: Node, b: Node): boolean {
  907. return (
  908. a.startLine === b.startLine &&
  909. a.startColumn === b.startColumn &&
  910. (a.endLine ?? a.startLine) === (b.endLine ?? b.startLine) &&
  911. a.endColumn === b.endColumn
  912. );
  913. }
  914. /**
  915. * Resolve `container.member` where `container` is a VALUE holding an object
  916. * literal — `export const api = { call() {…}, get: () => {…} }` used as the
  917. * module's namespace (#1573). The members are extracted as plain functions
  918. * with BARE qualified names inside the constant's source extent (there is no
  919. * `api::call`), so neither the `Container::member` lookup the class-shaped
  920. * kinds use (#825) nor the declared-type inference for singleton instances
  921. * (#1292) can reach them, and every such call resolved to nothing — or, via
  922. * an import, to the constant itself. This looks the member up by CONTAINMENT:
  923. * a node named `member` whose range lies inside the container's, in the
  924. * container's own file. A helper declared inside a member's body is not a
  925. * member and is skipped; nothing else in the file can donate a match. Calls
  926. * take callable kinds only; other references accept value members too.
  927. */
  928. export function resolveObjectLiteralMember(
  929. container: Node,
  930. member: string,
  931. ref: UnresolvedRef,
  932. context: ResolutionContext,
  933. confidence: number,
  934. resolvedBy: ResolvedRef['resolvedBy'],
  935. ): ResolvedRef | null {
  936. if (container.kind !== 'constant' && container.kind !== 'variable') return null;
  937. if (!OBJECT_LITERAL_LANGUAGES.has(container.language)) return null;
  938. if (!sameLanguageFamily(container.language, ref.language)) return null;
  939. const inFile = context.getNodesInFile(container.filePath);
  940. const callable = (n: Node) => n.kind === 'function' || n.kind === 'method';
  941. const valueMember = (n: Node) =>
  942. callable(n) || n.kind === 'property' || n.kind === 'variable' || n.kind === 'constant';
  943. const accepts = ref.referenceKind === 'calls' ? callable : valueMember;
  944. const inside = inFile.filter((n) => n.id !== container.id && rangeWithin(n, container));
  945. let candidates = inside.filter((n) => n.name === member && accepts(n));
  946. if (candidates.length === 0) return null;
  947. // Drop a candidate nested inside ANOTHER callable's body within the literal
  948. // (`{ run() { const call = () => {}; } }` — `call` is `run`'s local, not a
  949. // member). Strict containment: an identically-ranged sibling node for the
  950. // same member (a property node over an arrow function) is not a body.
  951. const bodies = inside.filter(callable);
  952. candidates = candidates.filter(
  953. (c) => !bodies.some((b) => b.id !== c.id && !sameRange(b, c) && rangeWithin(c, b))
  954. );
  955. if (candidates.length === 0) return null;
  956. // Several survivors (a property AND a function for one arrow member, say):
  957. // a callable first, then the earliest in source order.
  958. candidates.sort((a, b) => {
  959. const ca = callable(a) ? 0 : 1;
  960. const cb = callable(b) ? 0 : 1;
  961. if (ca !== cb) return ca - cb;
  962. return a.startLine - b.startLine || a.startColumn - b.startColumn;
  963. });
  964. return {
  965. original: ref,
  966. targetNodeId: candidates[0]!.id,
  967. confidence,
  968. resolvedBy,
  969. };
  970. }
  971. // Exported for the precedence unit tests (#1079): they assert the
  972. // preferredFqn → same-file → matches[0] ordering directly.
  973. export function resolveMethodOnType(
  974. typeName: string,
  975. methodName: string,
  976. ref: UnresolvedRef,
  977. context: ResolutionContext,
  978. confidence: number,
  979. resolvedBy: ResolvedRef['resolvedBy'],
  980. /**
  981. * Optional FQN that identifies WHICH class declaration `typeName`
  982. * refers to in the caller's file. When multiple candidates share
  983. * the same qualifiedName (`FooConverter::convert` in both
  984. * `dao/converter/` and `service/converter/`), the FQN's
  985. * file-path-suffix picks the right one — the disambiguation
  986. * signal Java imports carry but the call site doesn't (#314).
  987. */
  988. preferredFqn?: string,
  989. /** Recursion guard for the supertype/conformance walk. */
  990. depth = 0,
  991. ): ResolvedRef | null {
  992. // Look up methods by name and match by qualifiedName ending in
  993. // `<typeName>::<methodName>`. This works whether the method is defined
  994. // in-class (`class Foo { int bar() { ... } }`) or out-of-line in a separate
  995. // file (`int Foo::bar() { ... }` in foo.cpp while class Foo is in foo.hpp).
  996. // The previous same-file approach missed the latter — the typical C++ layout.
  997. // Prefer the context's per-(type, method) memo: the raw name lookup fetches
  998. // EVERY node sharing the method name — tens of thousands of rows for a
  999. // collision-heavy Java name like `execute` — and re-filtering that per ref
  1000. // was a dominant term in the #1122 watchdog kill on large repos. Only the
  1001. // ref-independent filter is memoized; per-ref disambiguation stays below.
  1002. let matches: Node[];
  1003. if (context.getMethodMatches) {
  1004. matches = context.getMethodMatches(typeName, methodName, ref.language);
  1005. } else {
  1006. const methodCandidates = context.getNodesByName(methodName);
  1007. const want = `${typeName}::${methodName}`;
  1008. matches = [];
  1009. for (const m of methodCandidates) {
  1010. if (m.kind !== 'method') continue;
  1011. if (!sameLanguageFamily(m.language, ref.language)) continue;
  1012. const qn = m.qualifiedName;
  1013. if (qn === want || qn.endsWith(`::${want}`)) {
  1014. matches.push(m);
  1015. }
  1016. }
  1017. }
  1018. if (matches.length === 0) {
  1019. // Conformance fallback: the method may be defined on a supertype `typeName`
  1020. // extends, or on a protocol / trait it conforms to (e.g. a Swift protocol-
  1021. // extension method, a C# default-interface or extension method, a Kotlin
  1022. // extension on a supertype). Walk supertypes transitively (depth-capped) via
  1023. // the resolved implements/extends edges — empty in the first resolution pass,
  1024. // populated in the conformance pass. Still VALIDATED (the method must exist on
  1025. // a supertype), so a wrong inference produces no edge.
  1026. if (depth < 4 && context.getSupertypes) {
  1027. const viaSupers = nmTimedT('rmot-supers', ref, (): ResolvedRef | null => {
  1028. for (const supertype of context.getSupertypes!(typeName, ref.language)) {
  1029. const via = resolveMethodOnType(
  1030. supertype, methodName, ref, context, confidence, resolvedBy, preferredFqn, depth + 1,
  1031. );
  1032. if (via) return via;
  1033. }
  1034. return null;
  1035. });
  1036. if (viaSupers) return viaSupers;
  1037. }
  1038. return null;
  1039. }
  1040. if (matches.length > 1 && preferredFqn) {
  1041. const ext = ref.language === 'kotlin' ? '.kt' : '.java';
  1042. const fqnPath = preferredFqn.replace(/\./g, '/') + ext;
  1043. const chosen = matches.find((m) => {
  1044. const fp = m.filePath.replace(/\\/g, '/');
  1045. return fp.endsWith(fqnPath) || fp.endsWith('/' + fqnPath);
  1046. });
  1047. if (chosen) {
  1048. return {
  1049. original: ref,
  1050. targetNodeId: chosen.id,
  1051. confidence,
  1052. resolvedBy,
  1053. };
  1054. }
  1055. }
  1056. // Language-agnostic disambiguation: when several same-named methods survive
  1057. // (e.g. two files each declaring `class Logger { void log(); }` — an ODR
  1058. // clash, an anonymous-namespace type, or separate translation units), prefer
  1059. // the definition in the CALL SITE's own file. Without this, every ambiguous
  1060. // call collapses onto the first-indexed definition, so a call in `b/svc.cpp`
  1061. // wrongly points at `a/svc.cpp` (#1079). This runs AFTER the `preferredFqn`
  1062. // block, so Java/Kotlin import disambiguation — whose target is intentionally
  1063. // in ANOTHER file (#314) — is unaffected: that block returns early whenever
  1064. // an import FQN pins the class.
  1065. const ordered = preferCallSiteFile(matches, ref.filePath);
  1066. return {
  1067. original: ref,
  1068. targetNodeId: ordered[0]!.id,
  1069. confidence,
  1070. resolvedBy,
  1071. };
  1072. }
  1073. // C++ keywords/control-flow tokens that can appear right before a receiver
  1074. // (e.g. `return ptr->m()`) and must NOT be treated as a type.
  1075. const CPP_NON_TYPE_TOKENS = new Set([
  1076. 'return', 'if', 'else', 'for', 'while', 'do', 'switch', 'case', 'default',
  1077. 'break', 'continue', 'goto', 'throw', 'new', 'delete', 'co_await', 'co_yield',
  1078. 'co_return', 'static_cast', 'const_cast', 'dynamic_cast', 'reinterpret_cast',
  1079. 'sizeof', 'alignof', 'typeid', 'and', 'or', 'not', 'xor',
  1080. ]);
  1081. function normalizeCppTypeName(typeName: string): string | null {
  1082. const normalized = typeName
  1083. .replace(/\b(const|volatile|mutable|typename|class|struct)\b/g, ' ')
  1084. .replace(/[&*]+/g, ' ')
  1085. .replace(/<[^>]*>/g, ' ')
  1086. .replace(/\s+/g, ' ')
  1087. .trim();
  1088. if (!normalized) return null;
  1089. const parts = normalized.split(/::/).filter(Boolean);
  1090. const last = parts[parts.length - 1];
  1091. if (!last) return null;
  1092. if (CPP_NON_TYPE_TOKENS.has(last)) return null;
  1093. return last;
  1094. }
  1095. // Declarator regex: matches `Type receiver`, `Type* receiver`, `Type *receiver`,
  1096. // `Type*receiver`, `Type<X> receiver`, etc., REQUIRING a declarator terminator
  1097. // (`;`, `=`, `,`, `)`, `[`, `{`, `(`, or end-of-line) after the receiver. The
  1098. // terminator rules out uses like `return receiver->m()` where the preceding
  1099. // token is a keyword, not a type.
  1100. function buildDeclaratorRegex(escapedReceiver: string): RegExp {
  1101. return new RegExp(
  1102. `([A-Za-z_][\\w:]*(?:\\s*<[^;=(){}]+>)?(?:\\s*[*&]+)?)\\s*\\b${escapedReceiver}\\b\\s*(?=[;=,)\\[{(]|$)`,
  1103. );
  1104. }
  1105. function inferCppReceiverType(
  1106. receiverName: string,
  1107. ref: UnresolvedRef,
  1108. context: ResolutionContext,
  1109. depth = 0,
  1110. ): string | null {
  1111. // Per-file lines cache when available — this runs per `receiver->method()`
  1112. // ref and re-splitting the file each time is the same quadratic as the
  1113. // shared inferrer's (#1122).
  1114. const lines = context.getFileLines
  1115. ? context.getFileLines(ref.filePath)
  1116. : (context.readFile(ref.filePath)?.split(/\r?\n/) ?? null);
  1117. if (!lines || lines.length === 0) return null;
  1118. const callLineIndex = Math.max(0, Math.min(lines.length - 1, ref.line - 1));
  1119. const escapedReceiver = receiverName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  1120. const receiverPattern = new RegExp(`\\b${escapedReceiver}\\b`);
  1121. const declaratorRegex = buildDeclaratorRegex(escapedReceiver);
  1122. for (let i = callLineIndex; i >= 0; i--) {
  1123. const line = lines[i];
  1124. if (!line || !receiverPattern.test(line)) continue;
  1125. const declaratorMatch = line.match(declaratorRegex);
  1126. if (declaratorMatch) {
  1127. const normalized = normalizeCppTypeName(declaratorMatch[1] ?? '');
  1128. if (normalized === 'auto') {
  1129. // `auto x = Foo::instance();` — the declared type is deduced; recover it
  1130. // from the initializer (call return type / construction) (#645).
  1131. const initType = inferCppAutoInitializerType(line, receiverName, ref, context, depth);
  1132. if (initType) return initType;
  1133. // No usable initializer on this line — keep scanning earlier ones.
  1134. } else if (normalized) {
  1135. return normalized;
  1136. }
  1137. }
  1138. }
  1139. const headerCandidates = [
  1140. ref.filePath.replace(/\.(?:c|cc|cpp|cxx)$/i, '.h'),
  1141. ref.filePath.replace(/\.(?:c|cc|cpp|cxx)$/i, '.hpp'),
  1142. ref.filePath.replace(/\.(?:c|cc|cpp|cxx)$/i, '.hxx'),
  1143. ].filter((candidate, index, arr) => arr.indexOf(candidate) === index && candidate !== ref.filePath);
  1144. for (const headerPath of headerCandidates) {
  1145. if (!context.fileExists(headerPath)) continue;
  1146. const headerLines = context.getFileLines
  1147. ? context.getFileLines(headerPath)
  1148. : (context.readFile(headerPath)?.split(/\r?\n/) ?? null);
  1149. if (!headerLines) continue;
  1150. for (const line of headerLines) {
  1151. if (!receiverPattern.test(line)) continue;
  1152. const declaratorMatch = line.match(declaratorRegex);
  1153. if (!declaratorMatch) continue;
  1154. const normalized = normalizeCppTypeName(declaratorMatch[1] ?? '');
  1155. if (normalized && normalized !== 'auto') return normalized;
  1156. }
  1157. }
  1158. return null;
  1159. }
  1160. /**
  1161. * Last `::`-separated segment of a (possibly namespace-qualified) C++ name.
  1162. */
  1163. function cppLastSegment(name: string): string {
  1164. const parts = name.split('::').filter(Boolean);
  1165. return parts[parts.length - 1] ?? name;
  1166. }
  1167. /**
  1168. * Return type captured at extraction for `Class::method` (or a free function),
  1169. * read off the indexed node's `returnType` — used by the C++ (#645) and PHP
  1170. * (#608) chained-call resolvers. Language-filtered. Null when not indexed or no
  1171. * return type was recorded (a `void`/primitive return).
  1172. */
  1173. function lookupCalleeReturnType(
  1174. callee: string,
  1175. ref: UnresolvedRef,
  1176. context: ResolutionContext,
  1177. ): string | null {
  1178. let method = callee;
  1179. let cls: string | null = null;
  1180. if (callee.includes('::')) {
  1181. const parts = callee.split('::').filter(Boolean);
  1182. method = parts[parts.length - 1] ?? callee;
  1183. cls = parts.slice(0, -1).join('::');
  1184. }
  1185. const candidates = context.getNodesByName(method).filter(
  1186. (n) =>
  1187. (n.kind === 'method' || n.kind === 'function') &&
  1188. n.language === ref.language &&
  1189. !!n.returnType,
  1190. );
  1191. if (cls) {
  1192. const want = `${cls}::${method}`;
  1193. // The call site may name the class with MORE namespace qualification than
  1194. // the stored node (`details::registry::instance` at the call vs
  1195. // `registry::instance` on the node — the receiver type only carries the
  1196. // immediate class), or LESS. Accept an exact match or either being a
  1197. // namespace-suffix of the other; the shared `::<class>::<method>` tail keeps
  1198. // it specific.
  1199. const m = candidates.find(
  1200. (n) =>
  1201. n.qualifiedName === want ||
  1202. n.qualifiedName.endsWith(`::${want}`) ||
  1203. want.endsWith(`::${n.qualifiedName}`),
  1204. );
  1205. return m?.returnType ?? null;
  1206. }
  1207. return candidates.find((n) => n.kind === 'function')?.returnType ?? null;
  1208. }
  1209. /** Does the graph contain an aggregate type named `name`'s last segment? */
  1210. function cppClassExists(name: string, ref: UnresolvedRef, context: ResolutionContext): boolean {
  1211. const last = cppLastSegment(name);
  1212. return context
  1213. .getNodesByName(last)
  1214. .some((n) => (n.kind === 'class' || n.kind === 'struct' || n.kind === 'union') && n.language === ref.language);
  1215. }
  1216. /**
  1217. * Infer the class produced by a C++ call/construction expression, using return
  1218. * types captured at extraction (#645). Handles, in order:
  1219. * - `make_unique<T>()` / `make_shared<T>()` → T
  1220. * - single-level member call `recv.method()` → recv's type, then method's return
  1221. * - `Class::method()` / free `func()` → the callee's recorded return type
  1222. * - direct construction `Type()` / `ns::Type()` → Type
  1223. * Returns null when undeterminable. Callers MUST still validate the outer method
  1224. * exists on the result before creating an edge, so a wrong guess stays silent.
  1225. */
  1226. function resolveCppCallResultType(
  1227. inner: string,
  1228. ref: UnresolvedRef,
  1229. context: ResolutionContext,
  1230. depth = 0,
  1231. ): string | null {
  1232. if (depth > 3) return null; // guard against pathological mutual recursion
  1233. const expr = inner.trim();
  1234. const make = expr.match(/(?:^|::)(?:make_unique|make_shared)\s*<\s*([A-Za-z_]\w*)/);
  1235. if (make) return make[1] ?? null;
  1236. // Single-level member call `recv.method` (the `manager.view().render()` shape).
  1237. const dotIdx = expr.lastIndexOf('.');
  1238. if (dotIdx > 0) {
  1239. const recv = expr.slice(0, dotIdx);
  1240. const method = expr.slice(dotIdx + 1);
  1241. if (recv.includes('.') || recv.includes('(') || recv.includes('::')) return null; // single level only
  1242. const recvType = inferCppReceiverType(recv, ref, context, depth + 1);
  1243. if (!recvType) return null;
  1244. return lookupCalleeReturnType(`${recvType}::${method}`, ref, context);
  1245. }
  1246. const ret = lookupCalleeReturnType(expr, ref, context);
  1247. if (ret) return ret;
  1248. // Direct construction — the callee itself names a class/struct.
  1249. if (cppClassExists(expr, ref, context)) return cppLastSegment(expr);
  1250. return null;
  1251. }
  1252. /**
  1253. * Recover the type of an `auto`-declared local from its initializer on the
  1254. * declaration line — `auto x = Foo::instance();`, `auto w = make_unique<W>();`,
  1255. * `auto p = new W();`, `auto w = Widget();` (#645).
  1256. */
  1257. function inferCppAutoInitializerType(
  1258. line: string,
  1259. receiverName: string,
  1260. ref: UnresolvedRef,
  1261. context: ResolutionContext,
  1262. depth: number,
  1263. ): string | null {
  1264. const escaped = receiverName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  1265. const m = line.match(new RegExp(`\\b${escaped}\\b\\s*=\\s*([^;]+)`));
  1266. if (!m || !m[1]) return null;
  1267. const init = m[1].trim();
  1268. const neu = init.match(/^new\s+([A-Za-z_][\w:]*)/);
  1269. if (neu && neu[1]) return cppLastSegment(neu[1]);
  1270. // A call or construction: `Foo(...)`, `A::b(...)`, `make_unique<T>(...)`.
  1271. const call = init.match(/^([A-Za-z_][\w:]*(?:\s*<[^>;]*>)?)\s*\(/);
  1272. if (call && call[1]) return resolveCppCallResultType(call[1].replace(/\s+/g, ''), ref, context, depth + 1);
  1273. return null;
  1274. }
  1275. /**
  1276. * Resolve a C++ chained call whose receiver is itself a call — encoded by the
  1277. * extractor as `<innerCallee>().<method>` (#645). The receiver's type is what
  1278. * the inner call returns; the outer method is then resolved and VALIDATED on it
  1279. * (resolveMethodOnType requires `cls::method` to exist), so a wrong inference
  1280. * produces no edge rather than a wrong one.
  1281. */
  1282. export function matchCppCallChain(
  1283. ref: UnresolvedRef,
  1284. context: ResolutionContext,
  1285. ): ResolvedRef | null {
  1286. const m = ref.referenceName.match(/^(.+)\(\)\.(\w+)$/);
  1287. if (!m || !m[1] || !m[2]) return null;
  1288. const cls = resolveCppCallResultType(m[1], ref, context);
  1289. if (!cls) return null;
  1290. return resolveMethodOnType(cls, m[2], ref, context, 0.85, 'instance-method');
  1291. }
  1292. /**
  1293. * Resolve a `::`-scoped factory chain whose receiver is a scoped/static call —
  1294. * PHP `Cls::for($x)->method()` (#608, the per-credential Laravel client idiom) or
  1295. * Rust `Foo::new().bar()` (an associated-function call) — both encoded by the
  1296. * extractor as `Cls::factory().method`. The receiver's type is what `Cls::factory`
  1297. * returns: a `self` marker (PHP `: self`/`: static`, Rust `-> Self`) resolves to
  1298. * the factory's own type, a concrete return type to that type. The outer method is
  1299. * then resolved and VALIDATED on it (resolveMethodOnType requires the method to
  1300. * exist on the type or a supertype it conforms to), so a wrong inference yields no
  1301. * edge rather than a wrong one. Shared by the `::`-receiver languages (PHP, Rust).
  1302. */
  1303. export function matchScopedCallChain(
  1304. ref: UnresolvedRef,
  1305. context: ResolutionContext,
  1306. ): ResolvedRef | null {
  1307. const m = ref.referenceName.match(/^(.+)\(\)\.(\w+)$/);
  1308. if (!m || !m[1] || !m[2]) return null;
  1309. const inner = m[1];
  1310. const method = m[2];
  1311. if (!inner.includes('::')) return null; // only static-factory (`Cls::method`) chains
  1312. const factoryClass = inner.slice(0, inner.lastIndexOf('::'));
  1313. const ret = lookupCalleeReturnType(inner, ref, context);
  1314. if (!ret) return null;
  1315. // `self` (the extractor's marker for self/static/$this) → the factory's class.
  1316. const resolvedClass = ret === 'self' ? factoryClass : ret;
  1317. return resolveMethodOnType(resolvedClass, method, ref, context, 0.85, 'instance-method');
  1318. }
  1319. /**
  1320. * Languages where an unprefixed capitalized call `Foo(args)` constructs the
  1321. * class (so a `Foo(args).method()` receiver's type is `Foo`). Java/C# need `new`,
  1322. * so a bare `Foo()` there is a method call, not construction — excluded. Scala's
  1323. * `Foo(args)` is a case-class / companion `apply`, which conventionally returns
  1324. * `Foo` — and resolveMethodOnType validates, so a non-conventional `apply` that
  1325. * returns another type simply yields no edge rather than a wrong one. Pascal/Delphi:
  1326. * a `TFoo(x)` is a TYPECAST whose result is a `TFoo`, so `TFoo(x).method()` resolves
  1327. * the method on `TFoo` — same shape, same validation.
  1328. */
  1329. const CONSTRUCTS_VIA_BARE_CALL = new Set(['kotlin', 'swift', 'scala', 'dart', 'pascal']);
  1330. /**
  1331. * Resolve a dotted chained call whose receiver is a static factory / fluent call —
  1332. * `Foo.getInstance().bar()`, encoded by the extractor as `Foo.getInstance().bar`
  1333. * (#645/#608 mechanism). The receiver's type is what `Foo.getInstance` returns
  1334. * (its declared return type); the outer method is then resolved and VALIDATED on
  1335. * it (resolveMethodOnType requires `Type::method` to exist), so a wrong inference
  1336. * yields no edge rather than a wrong one (e.g. a same-named `bar()` on an
  1337. * unrelated class is never matched). Shared by the dot-notation languages
  1338. * (Java, Kotlin, C#, Swift) — same receiver shape, same `Class::method` qualified names.
  1339. */
  1340. export function matchDottedCallChain(
  1341. ref: UnresolvedRef,
  1342. context: ResolutionContext,
  1343. ): ResolvedRef | null {
  1344. const m = ref.referenceName.match(/^(.+)\(\)\.(\w+)$/);
  1345. if (!m || !m[1] || !m[2]) return null;
  1346. const inner = m[1]; // `Foo.getInstance`
  1347. const method = m[2]; // `bar`
  1348. const lastDot = inner.lastIndexOf('.');
  1349. if (lastDot <= 0) {
  1350. // Go: bare package-level factory FUNCTION `New().method()` — the receiver's
  1351. // type is what `New` returns; resolve the method on that.
  1352. if (ref.language === 'go') {
  1353. const ret = lookupCalleeReturnType(inner, ref, context);
  1354. if (ret) {
  1355. return resolveMethodOnType(ret, method, ref, context, 0.85, 'instance-method', importedFqnOf(ret, ref, context));
  1356. }
  1357. // `inner` isn't a function with a captured return type — typically a
  1358. // package-level VARIABLE holding a function value (e.g. gin's `engine()`),
  1359. // whose type we can't recover. Fall back to bare-name resolution of the
  1360. // method so we don't DROP an edge the un-re-encoded bare path would have
  1361. // found. (When `inner` IS a real factory function but the method doesn't
  1362. // exist on its return type, `ret` is truthy and we returned no edge above —
  1363. // the absent-method safety guarantee is preserved.)
  1364. //
  1365. // CRITICAL: resolve the TARGET via a synthetic bare-name ref, but return the
  1366. // match tied to the ORIGINAL `ref` (referenceName `inner().method`). The
  1367. // batched resolver (resolveAndPersistBatched) reads unresolved rows from
  1368. // offset 0 every pass and relies on the post-batch cleanup (row-id delete
  1369. // for DB-loaded refs, referenceName-keyed delete otherwise, #1269) to
  1370. // clear each resolved row so the batch empties. If we propagated the
  1371. // synthetic ref's bare `method` as `.original`, a key-based delete
  1372. // would never match the stored `inner().method` row, the batch would
  1373. // never drain, and the loop would re-resolve + re-insert forever (a runaway
  1374. // that grew gin's graph to 5M edges / 1.4 GB before this fix).
  1375. const bareRef = { ...ref, referenceName: method };
  1376. const bareMatch = matchByExactName(bareRef, context) ?? matchFuzzy(bareRef, context);
  1377. return bareMatch ? { ...bareMatch, original: ref } : null;
  1378. }
  1379. // Constructor receiver `Foo(args).method()` (encoded `Foo().method`): a bare,
  1380. // capitalized inner is a class construction, so the receiver's type is the
  1381. // class itself — resolve the method on it. Only in languages where an
  1382. // unprefixed capitalized call constructs the class (Kotlin, Swift); in Java/C#
  1383. // a bare `Foo()` is a method call (constructors need `new`), so we must not
  1384. // assume construction. A lowercase bare inner is a top-level `factory().method()`
  1385. // whose type we can't recover — bail.
  1386. if (!CONSTRUCTS_VIA_BARE_CALL.has(ref.language) || !/^[A-Z]/.test(inner)) return null;
  1387. return resolveMethodOnType(inner, method, ref, context, 0.85, 'instance-method', importedFqnOf(inner, ref, context));
  1388. }
  1389. // Factory/fluent receiver `Receiver.factory(args).method()`: the receiver's
  1390. // type is what `Receiver.factory` returns (its declared return type).
  1391. const factoryClass = inner.slice(0, lastDot).split('.').pop(); // simple class name
  1392. const factoryMethod = inner.slice(lastDot + 1);
  1393. if (!factoryClass || !factoryMethod) return null;
  1394. const ret = lookupCalleeReturnType(`${factoryClass}::${factoryMethod}`, ref, context);
  1395. if (!ret) {
  1396. // Objective-C: a class-message factory — `[X alloc]`, `[X new]`,
  1397. // `[X sharedFoo]` — returns an instance of the RECEIVER class `X` by
  1398. // convention (`instancetype`). So when the factory's own return type isn't
  1399. // recoverable (its selector returns `instancetype`, or `alloc`/`new` aren't
  1400. // user-defined nodes at all), the receiver's type is the class `X` itself.
  1401. // This resolves the ubiquitous `[[X alloc] init]` and singleton chains.
  1402. // resolveMethodOnType validates against X (and its supertypes), so a class
  1403. // whose method actually lives elsewhere yields NO edge, not a wrong one — and
  1404. // crucially this does NOT fire when a concrete return type WAS captured but
  1405. // simply lacks the method (that already returned null above: absent-method
  1406. // safety, so a same-named decoy is still never matched).
  1407. if (ref.language === 'objc' && /^[A-Z]/.test(factoryClass)) {
  1408. return resolveMethodOnType(factoryClass, method, ref, context, 0.8, 'instance-method', importedFqnOf(factoryClass, ref, context));
  1409. }
  1410. // Pascal/Delphi: the extractor only re-encodes a `TFoo`/`IFoo`-prefixed chain
  1411. // (the type-naming convention), so `factoryClass` is always a real class here.
  1412. // A factory whose return type wasn't captured is a CONSTRUCTOR
  1413. // (`TFileMem.Create().SetCachePerformance` — `constructor Create` has no `:
  1414. // TBar` annotation but returns its own class) or an unannotated function. In
  1415. // both cases the receiver's type is the class itself, so resolve the method on
  1416. // `factoryClass`. resolveMethodOnType validates against it (and its
  1417. // supertypes), so a wrong inference yields no edge — and this never fires when
  1418. // a return type WAS captured but lacks the method (absent-method safety above).
  1419. if (ref.language === 'pascal' && /^[TI]/.test(factoryClass)) {
  1420. return resolveMethodOnType(factoryClass, method, ref, context, 0.8, 'instance-method', importedFqnOf(factoryClass, ref, context));
  1421. }
  1422. return null;
  1423. }
  1424. return resolveMethodOnType(ret, method, ref, context, 0.85, 'instance-method', importedFqnOf(ret, ref, context));
  1425. }
  1426. /**
  1427. * When several classes share a simple type name, the caller file's import of
  1428. * that type is the only signal that names WHICH one (#314). Returns the imported
  1429. * FQN for `typeName` in the ref's file, or undefined.
  1430. */
  1431. function importedFqnOf(
  1432. typeName: string,
  1433. ref: UnresolvedRef,
  1434. context: ResolutionContext,
  1435. ): string | undefined {
  1436. const imports = context.getImportMappings(ref.filePath, ref.language);
  1437. return imports.find((i) => i.localName === typeName)?.source;
  1438. }
  1439. /**
  1440. * Java/Kotlin: infer a receiver's declared type by walking field declarations
  1441. * in the class enclosing the call site. The field's `signature` is already in
  1442. * the form "<TypeName> <fieldName>" (set by tree-sitter.ts extractField), so we
  1443. * pull the type from there. Handles Spring `@Resource UserBO userbo;` /
  1444. * `@Autowired private UserService userService;` where the receiver field name
  1445. * doesn't match the class name by Java naming convention.
  1446. *
  1447. * Returns the bare type name (generics stripped, dotted package stripped) or
  1448. * null when no matching field is in the enclosing class.
  1449. */
  1450. function inferJavaFieldReceiverType(
  1451. receiverName: string,
  1452. ref: UnresolvedRef,
  1453. context: ResolutionContext,
  1454. ): string | null {
  1455. const inFile = context.getNodesInFile(ref.filePath);
  1456. if (inFile.length === 0) return null;
  1457. // Find the class enclosing the call line (tightest match by latest start).
  1458. let enclosing: Node | null = null;
  1459. for (const n of inFile) {
  1460. if (n.kind !== 'class' && n.kind !== 'interface') continue;
  1461. if (n.language !== ref.language) continue;
  1462. const end = n.endLine ?? n.startLine;
  1463. if (n.startLine <= ref.line && end >= ref.line) {
  1464. if (!enclosing || n.startLine >= enclosing.startLine) enclosing = n;
  1465. }
  1466. }
  1467. if (!enclosing) return null;
  1468. const enclosingEnd = enclosing.endLine ?? enclosing.startLine;
  1469. const field = inFile.find(
  1470. (n) =>
  1471. n.kind === 'field' &&
  1472. n.name === receiverName &&
  1473. n.language === ref.language &&
  1474. n.startLine >= enclosing.startLine &&
  1475. (n.endLine ?? n.startLine) <= enclosingEnd,
  1476. );
  1477. if (!field || !field.signature) return null;
  1478. // Signature shape: "<TypeName> <fieldName>" (extractField). Pull the type,
  1479. // strip generics + dotted package, drop array/varargs markers.
  1480. const beforeName = field.signature.slice(
  1481. 0,
  1482. field.signature.lastIndexOf(field.name),
  1483. );
  1484. const typeRaw = beforeName.trim();
  1485. if (!typeRaw) return null;
  1486. const typeNoGenerics = typeRaw.replace(/<[^>]*>/g, '').trim();
  1487. const typeNoArray = typeNoGenerics.replace(/\[\s*\]/g, '').replace(/\.\.\.$/, '').trim();
  1488. const parts = typeNoArray.split(/[.\s]+/).filter(Boolean);
  1489. const lastPart = parts[parts.length - 1];
  1490. if (!lastPart) return null;
  1491. if (!/^[A-Z]/.test(lastPart)) return null; // primitives / lowercase → skip
  1492. return lastPart;
  1493. }
  1494. // ── Local-variable receiver-type inference (#1108) ──────────────────────────
  1495. //
  1496. // Instance calls through a local variable (`const lg = new Logger(); lg.log()`)
  1497. // only resolved in C++ before this — no other language could learn the
  1498. // receiver's type. Local variables are not indexed as nodes (node-explosion),
  1499. // so, like the C++ inferrer above, we read the enclosing function's source and
  1500. // match the receiver's declaration/initializer to recover its type. The type is
  1501. // then handed to resolveMethodOnType, which VALIDATES that the type actually
  1502. // declares the method, so a mis-inference produces NO edge — the safety net
  1503. // that lets the patterns below stay simple. C++ keeps its dedicated inferrer
  1504. // (header scan + `auto`); this covers every other language.
  1505. // Tokens a loose pattern might capture that are never a user-defined type.
  1506. const NON_TYPE_RECEIVER_TOKENS = new Set([
  1507. 'this', 'self', 'super', 'new', 'return', 'await', 'yield', 'typeof',
  1508. 'null', 'nil', 'None', 'true', 'false', 'True', 'False', 'undefined',
  1509. ]);
  1510. /**
  1511. * Normalize a captured type expression to a simple type name: drop generic
  1512. * args and pointer/ref markers, take the last `.`/`::`-qualified segment, and
  1513. * reject obvious non-types.
  1514. */
  1515. export function normalizeInferredTypeName(raw: string): string | null {
  1516. const cleaned = raw.replace(/<[^>]*>/g, '').replace(/[&*]/g, '').trim();
  1517. const seg = cleaned.split(/[.:]+/).filter(Boolean).pop();
  1518. if (!seg) return null;
  1519. if (NON_TYPE_RECEIVER_TOKENS.has(seg)) return null;
  1520. return seg;
  1521. }
  1522. /**
  1523. * Per-language patterns that recover a local variable's (or typed parameter's)
  1524. * type from its declaration/initializer. Each regex captures the type in group
  1525. * 1; `r` is the already-escaped receiver name. Ordered most-specific first.
  1526. * PascalCase is required in the capture where the language convention allows,
  1527. * as a cheap false-positive guard on top of resolveMethodOnType's validation.
  1528. */
  1529. /**
  1530. * Compiled-pattern memo for the receiver-type pattern builders below. They
  1531. * run for EVERY `receiver.method()` ref the matcher attempts, compiling 2–4
  1532. * fresh RegExp objects per call — and receivers repeat massively (`self`
  1533. * alone accounts for tens of thousands of refs on a Lua repo, measured 41µs
  1534. * per methodCall miss on kong with compilation a large slice). The patterns
  1535. * are a pure function of (language, receiver) and non-global (`.match()`
  1536. * never touches lastIndex), so shared instances are behavior-identical.
  1537. * FIFO-capped with no per-get mutation (the §7a.6 LRU-churn lesson): a hit
  1538. * costs one Map lookup, overflow evicts oldest, and an evicted entry simply
  1539. * recompiles exactly as every call did before this memo.
  1540. */
  1541. const PATTERN_MEMO = new Map<string, RegExp[]>();
  1542. const PATTERN_MEMO_CAP = 8192;
  1543. /**
  1544. * Per-context incremental receiver-scan states for inferLocalReceiverType
  1545. * (see the memo comment there). Keyed (file, scopeStart, language, receiver);
  1546. * entries are a few dozen bytes, count is bounded by distinct receiver uses
  1547. * (same order as the context's other per-file caches). MUST drop whenever the
  1548. * context's file caches drop — the states are derived from file lines — so
  1549. * ReferenceResolver.clearCaches calls clearNameMatcherMemos alongside
  1550. * clearImportResolverMemos.
  1551. */
  1552. type InferScanState = { hi: number; ansIdx: number; ansType: string | null };
  1553. const INFER_SCAN_STATES = new WeakMap<ResolutionContext, Map<string, InferScanState>>();
  1554. function getInferScanStates(context: ResolutionContext): Map<string, InferScanState> {
  1555. let m = INFER_SCAN_STATES.get(context);
  1556. if (!m) {
  1557. m = new Map();
  1558. INFER_SCAN_STATES.set(context, m);
  1559. }
  1560. return m;
  1561. }
  1562. /** Drop the per-context scan states (see ReferenceResolver.clearCaches). */
  1563. export function clearNameMatcherMemos(context: ResolutionContext): void {
  1564. INFER_SCAN_STATES.delete(context);
  1565. C_STATIC_MEMO.delete(context);
  1566. RUST_TRAIT_IMPL_MEMO.delete(context);
  1567. SEALED_MODULES.delete(context);
  1568. LOCAL_BINDING_MEMO.delete(context);
  1569. SELECTOR_NAMES.delete(context);
  1570. }
  1571. function memoPatterns(key: string, build: () => RegExp[]): RegExp[] {
  1572. const hit = PATTERN_MEMO.get(key);
  1573. if (hit) return hit;
  1574. const patterns = build();
  1575. if (PATTERN_MEMO.size >= PATTERN_MEMO_CAP) {
  1576. const oldest = PATTERN_MEMO.keys().next().value;
  1577. if (oldest !== undefined) PATTERN_MEMO.delete(oldest);
  1578. }
  1579. PATTERN_MEMO.set(key, patterns);
  1580. return patterns;
  1581. }
  1582. export function localReceiverTypePatterns(language: Language, r: string): RegExp[] {
  1583. return memoPatterns(`${language}|${r}`, () => buildLocalReceiverTypePatterns(language, r));
  1584. }
  1585. function buildLocalReceiverTypePatterns(language: Language, r: string): RegExp[] {
  1586. switch (language) {
  1587. case 'typescript':
  1588. case 'javascript':
  1589. case 'tsx':
  1590. case 'jsx':
  1591. case 'arkts':
  1592. return [
  1593. new RegExp(`\\b${r}\\b\\s*=\\s*new\\s+([A-Za-z_$][\\w.$]*)`), // = new Logger()
  1594. // No keyword requirement, so this matches BOTH a local annotation
  1595. // (`const lg: Logger`) and a typed parameter (`function use(lg: Logger)`
  1596. // / `(lg: Logger) =>`) — the parameter case the old `const|let|var`
  1597. // prefix excluded (#1125). Mirrors Kotlin/Swift/Scala; the capture stops
  1598. // at `<` so a generic-typed param (`repo: Repository<User>`) still yields
  1599. // `Repository`. resolveMethodOnType validates the type actually declares
  1600. // the method, so the looser match produces no edge on a mis-inference.
  1601. new RegExp(`\\b${r}\\b\\s*:\\s*([A-Z][\\w.$]*)`), // lg: Logger (annotation or typed param)
  1602. ];
  1603. case 'python':
  1604. return [
  1605. new RegExp(`\\b${r}\\b\\s*=\\s*([A-Z][\\w.]*)\\s*\\(`), // lg = Logger(...)
  1606. // A quoted forward reference (`lg: "Logger"`, `lg: 'pkg.Logger'`) is the
  1607. // same annotation — and what every file under `from __future__ import
  1608. // annotations` or with a not-yet-defined class writes. The unquoted
  1609. // pattern below stopped at the quote and read no type at all, so the
  1610. // call produced no edge (#1684). Tried first: it is the stricter shape.
  1611. new RegExp(`\\b${r}\\b\\s*:\\s*["']([A-Z][\\w.]*)["']`), // lg: "Logger"
  1612. new RegExp(`\\b${r}\\b\\s*:\\s*([A-Z][\\w.]*)`), // lg: Logger (PEP 526)
  1613. ];
  1614. case 'java':
  1615. return [
  1616. new RegExp(`\\b${r}\\b\\s*=\\s*new\\s+([A-Za-z_][\\w.]*)`), // = new Logger()
  1617. new RegExp(`\\b([A-Z][\\w.]*)\\s+${r}\\b\\s*[=;,)]`), // Logger lg; / param
  1618. ];
  1619. case 'kotlin':
  1620. return [
  1621. new RegExp(`\\b${r}\\b\\s*=\\s*([A-Z][\\w.]*)\\s*\\(`), // val lg = Logger(...)
  1622. new RegExp(`\\b${r}\\b\\s*:\\s*([A-Z][\\w.]*)`), // val lg: Logger / param
  1623. ];
  1624. case 'csharp':
  1625. return [
  1626. new RegExp(`\\b${r}\\b\\s*=\\s*new\\s+([A-Za-z_][\\w.]*)`), // = new Logger()
  1627. new RegExp(`\\b([A-Z][\\w.]*)\\s+${r}\\b\\s*[=;,)]`), // Logger lg; / param
  1628. ];
  1629. case 'swift':
  1630. return [
  1631. new RegExp(`\\b${r}\\b\\s*=\\s*([A-Z][\\w.]*)\\s*\\(`), // let lg = Logger(...)
  1632. new RegExp(`\\b${r}\\b\\s*:\\s*([A-Z][\\w.]*)`), // let lg: Logger / param
  1633. ];
  1634. case 'rust':
  1635. return [
  1636. new RegExp(`\\blet\\s+(?:mut\\s+)?${r}\\b(?:\\s*:[^=]+)?=\\s*&?(?:mut\\s+)?([A-Z][\\w]*)`), // let lg = Logger::new()/Logger{}/Logger
  1637. // No `let`, so this covers a `let lg: Logger` binding AND a typed
  1638. // parameter (`fn use(lg: &Logger)`, a closure `|lg: Logger|`) — the
  1639. // parameter case the old `let`-anchored pattern excluded (#1125).
  1640. new RegExp(`\\b${r}\\s*:\\s*&?(?:mut\\s+)?([A-Z][\\w]*)`), // lg: Logger (binding or typed param)
  1641. ];
  1642. case 'go':
  1643. return [
  1644. new RegExp(`\\b${r}\\b\\s*:=\\s*&?([A-Za-z_][\\w.]*)\\s*{`), // lg := Logger{} / &Logger{}
  1645. new RegExp(`\\bvar\\s+${r}\\s+\\*?([A-Za-z_][\\w.]*)`), // var lg Logger / *Logger
  1646. // A typed parameter / method receiver (`func use(lg Logger)`,
  1647. // `func (l Logger) M()`) — name-before-type with no `var`/`:=` (#1125).
  1648. // PascalCase-guarded (unlike the anchored patterns above) to keep the
  1649. // keyword-free `ident Type` shape from matching unrelated pairs; the
  1650. // enclosing-scope bound already excludes package-level struct fields.
  1651. new RegExp(`\\b${r}\\s+\\*?([A-Z][\\w.]*)`), // func use(lg Logger) / (l Logger)
  1652. ];
  1653. case 'ruby':
  1654. return [
  1655. new RegExp(`\\b${r}\\b\\s*=\\s*([A-Z][\\w:]*)\\.new\\b`), // lg = Logger.new
  1656. ];
  1657. case 'scala':
  1658. return [
  1659. new RegExp(`\\b${r}\\b\\s*=\\s*(?:new\\s+)?([A-Z][\\w.]*)`), // val lg = new Logger / Logger(...)
  1660. new RegExp(`\\b${r}\\b\\s*:\\s*([A-Z][\\w.]*)`), // val lg: Logger / param
  1661. ];
  1662. case 'dart':
  1663. return [
  1664. new RegExp(`\\b${r}\\b\\s*=\\s*([A-Z][\\w.]*)\\s*\\(`), // var lg = Logger(...)
  1665. // Trailing `[=;,)]` (not just `[=;]`) so a typed parameter — `Logger lg)`
  1666. // / `Logger lg,` — matches too, not only `Logger lg = ...` / `Logger lg;`
  1667. // (#1125). Mirrors Java/C#.
  1668. new RegExp(`\\b([A-Z][\\w.]*)\\s+${r}\\b\\s*[=;,)]`), // Logger lg = ... / param
  1669. ];
  1670. case 'php':
  1671. return [
  1672. new RegExp(`\\$?${r}\\b\\s*=\\s*new\\s+([A-Za-z_\\\\][\\w\\\\]*)`), // $lg = new Logger()
  1673. // A typed parameter (`function use(Logger $lg)`, `?Logger $lg`,
  1674. // `\\App\\Logger $lg`, `&$lg` by-ref) and a typed `catch (E $e)` — the
  1675. // type sits before the `$`-variable (#1125). Namespace `\\` allowed.
  1676. new RegExp(`\\b([A-Za-z_\\\\][\\w\\\\]*)\\s+&?\\$${r}\\b`), // Logger $lg (typed param)
  1677. ];
  1678. case 'lua':
  1679. case 'luau':
  1680. return [
  1681. new RegExp(`\\b${r}\\b\\s*=\\s*([A-Z][\\w]*)\\.new\\b`), // local lg = Logger.new()
  1682. new RegExp(`\\b${r}\\b\\s*=\\s*([A-Z][\\w]*)\\s*\\(`), // local lg = Logger(...) (callable table)
  1683. // Luau annotation (`local lg: Logger`) / typed param — but Lua's
  1684. // method-call syntax is the IDENTICAL `receiver:Name` shape, and the
  1685. // backward scan starts on the call's own line, so without a gate any
  1686. // PascalCase method call (`lg:Log()`, the Roblox convention)
  1687. // self-matches as "type = Log" before the scan reaches the real
  1688. // declaration (#1124). The lookahead rejects a capture followed by
  1689. // any of Lua's three call forms — `(args)`, `"s"`/`'s'`/`[[s]]`,
  1690. // `{t}` — and its leading `[\w.]` alternative stops backtracking from
  1691. // shrinking the capture to dodge the gate (`lg:Log()` would otherwise
  1692. // still match, as `Lo`).
  1693. new RegExp(`\\b${r}\\b\\s*:\\s*([A-Z][\\w.]*)(?![\\w.]|\\s*[({"'\\[])`), // local lg: Logger / typed param
  1694. ];
  1695. case 'r':
  1696. return [
  1697. new RegExp(`\\b${r}\\b\\s*(?:<-|<<-|=)\\s*([A-Z][\\w.]*)\\$new\\b`), // lg <- Logger$new() (R6)
  1698. ];
  1699. case 'pascal':
  1700. return [
  1701. new RegExp(`\\b${r}\\b\\s*:\\s*([A-Z][\\w]*)`), // var lg: TLogger / param lg: TLogger
  1702. new RegExp(`\\b${r}\\b\\s*:=\\s*([A-Z][\\w.]*)\\.Create\\b`), // lg := TLogger.Create
  1703. ];
  1704. case 'cfml':
  1705. case 'cfscript':
  1706. return [
  1707. // svc = new UserService() / new path.to.UserService() — dotted component
  1708. // paths reduce to their final segment via normalizeInferredTypeName.
  1709. // Also matches inside tag markup (`<cfset svc = new UserService()>`)
  1710. // since the scan reads raw source lines.
  1711. new RegExp(`\\b${r}\\b\\s*=\\s*new\\s+([A-Za-z_][\\w.]*)`),
  1712. // The classic form: svc = createObject("component", "path.to.UserService")
  1713. // (casing of createObject varies in the wild), plus the modern
  1714. // single-argument form createObject("path.to.UserService").
  1715. new RegExp(`\\b${r}\\b\\s*=\\s*[Cc]reate[Oo]bject\\s*\\(\\s*["']component["']\\s*,\\s*["']([\\w.]+)["']`),
  1716. new RegExp(`\\b${r}\\b\\s*=\\s*[Cc]reate[Oo]bject\\s*\\(\\s*["']([\\w.]+)["']\\s*\\)`),
  1717. // Typed cfscript parameter: `function save(UserService svc)` /
  1718. // `required UserService svc` — CFML's built-in types (string, numeric,
  1719. // any, struct…) are lowercase by convention, so the PascalCase guard
  1720. // excludes them.
  1721. new RegExp(`\\b([A-Z][\\w.]*)\\s+${r}\\b\\s*[=;,)]`),
  1722. // Tag-form typed argument, either attribute order:
  1723. // <cfargument name="svc" type="path.to.UserService">
  1724. new RegExp(`\\bcfargument[^>\\n]*\\bname\\s*=\\s*["']${r}["'][^>\\n]*\\btype\\s*=\\s*["']([\\w.]+)["']`, 'i'),
  1725. new RegExp(`\\bcfargument[^>\\n]*\\btype\\s*=\\s*["']([\\w.]+)["'][^>\\n]*\\bname\\s*=\\s*["']${r}["']`, 'i'),
  1726. // Component property (incl. WireBox DI): `property name="svc"
  1727. // inject="UserService";` / `<cfproperty name="svc" type="UserService">`,
  1728. // either attribute order. An inject DSL value with a namespace
  1729. // (`inject="svc@core"`) captures only the leading name and simply
  1730. // fails type-validation — no edge, never a wrong one.
  1731. new RegExp(`\\b(?:cf)?property\\b[^;\\n]*\\bname\\s*=\\s*["']${r}["'][^;\\n]*\\b(?:type|inject)\\s*=\\s*["']([\\w.]+)["']`, 'i'),
  1732. new RegExp(`\\b(?:cf)?property\\b[^;\\n]*\\b(?:type|inject)\\s*=\\s*["']([\\w.]+)["'][^;\\n]*\\bname\\s*=\\s*["']${r}["']`, 'i'),
  1733. ];
  1734. default:
  1735. return [];
  1736. }
  1737. }
  1738. /** 1-based start line of the tightest function/method enclosing the call. */
  1739. function enclosingScopeStartLine(ref: UnresolvedRef, context: ResolutionContext): number {
  1740. let start = 1;
  1741. for (const n of context.getNodesInFile(ref.filePath)) {
  1742. if (n.kind !== 'function' && n.kind !== 'method') continue;
  1743. if (n.language !== ref.language) continue;
  1744. const end = n.endLine ?? n.startLine;
  1745. if (n.startLine <= ref.line && end >= ref.line && n.startLine >= start) {
  1746. start = n.startLine;
  1747. }
  1748. }
  1749. return start;
  1750. }
  1751. /**
  1752. * Infer a receiver's type from its local declaration/initializer in the
  1753. * enclosing function body. Language-dispatched; returns null for languages
  1754. * without patterns or when no declaration is found. Bounded to the enclosing
  1755. * scope so a same-named variable in another function can't leak in.
  1756. */
  1757. function inferLocalReceiverType(
  1758. receiverName: string,
  1759. ref: UnresolvedRef,
  1760. context: ResolutionContext,
  1761. ): string | null {
  1762. // CFML scope prefixes: `variables.svc` / `this.svc` name a COMPONENT-scoped
  1763. // field whose assignment or `property` declaration usually lives outside the
  1764. // calling function (the init-pseudoconstructor / WireBox-injection pattern),
  1765. // and `local.svc` is an explicit function-local. Strip the prefix so the
  1766. // declaration patterns match (`variables.svc = new X()`, `property
  1767. // name="svc" …`, `var svc = …` all bind the bare name), and widen the scan
  1768. // to the whole file for the component-scoped forms — nearest-declaration-
  1769. // backward still wins, so a function-local shadowing the field is preferred.
  1770. let scanReceiver = receiverName;
  1771. let componentScoped = false;
  1772. if (ref.language === 'cfml' || ref.language === 'cfscript') {
  1773. const scoped = receiverName.match(/^(variables|this|local|arguments)\.(.+)$/i);
  1774. if (scoped) {
  1775. scanReceiver = scoped[2]!;
  1776. const scope = scoped[1]!.toLowerCase();
  1777. componentScoped = scope === 'variables' || scope === 'this';
  1778. }
  1779. }
  1780. // PHP `$this->prop` receiver — the property's declaration lives outside the
  1781. // calling method (a promoted constructor parameter `private readonly Foo $prop`,
  1782. // a typed property `private Foo $prop;`, or a classic constructor parameter
  1783. // `Foo $prop` assigned in __construct). Strip the prefix and widen the scan to
  1784. // the whole file (the constructor may sit below the calling method), but —
  1785. // unlike CFML's scopes above — switch to PROPERTY-shaped patterns: a plain
  1786. // `$prop` local or parameter lives in a different namespace than `$this->prop`
  1787. // and can never shadow it, so the generic local patterns would type the
  1788. // property from unrelated same-named variables in other methods (a wrong
  1789. // 0.9-confidence edge, not a missing one).
  1790. let phpProperty = false;
  1791. if (ref.language === 'php') {
  1792. const scoped = receiverName.match(/^this->(.+)$/);
  1793. if (scoped) {
  1794. scanReceiver = scoped[1]!;
  1795. componentScoped = true;
  1796. phpProperty = true;
  1797. }
  1798. }
  1799. const escapedReceiver = scanReceiver.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  1800. const patterns = phpProperty
  1801. ? phpPropertyTypePatterns(escapedReceiver)
  1802. : localReceiverTypePatterns(ref.language, escapedReceiver);
  1803. if (patterns.length === 0) return null;
  1804. // Split through the context's per-file lines cache when available: this runs
  1805. // for EVERY `receiver.method()` ref, and re-splitting the whole file per ref
  1806. // was ~20% of total index CPU on Java-heavy repos (#1122).
  1807. const lines = context.getFileLines
  1808. ? context.getFileLines(ref.filePath)
  1809. : (context.readFile(ref.filePath)?.split(/\r?\n/) ?? null);
  1810. if (!lines || lines.length === 0) return null;
  1811. const callIdx = Math.max(0, Math.min(lines.length - 1, ref.line - 1));
  1812. const startIdx = componentScoped
  1813. ? 0
  1814. : Math.max(0, enclosingScopeStartLine(ref, context) - 1);
  1815. const matchLine = (i: number): string | null => {
  1816. const line = lines[i];
  1817. if (!line) return null;
  1818. // A generated/minified line (one multi-KB statement) is not something a
  1819. // human-written local declaration lives on, and regexing it per ref is
  1820. // pure waste — skip it rather than scan it.
  1821. if (line.length > 10_000) return null;
  1822. for (const re of patterns) {
  1823. const m = line.match(re);
  1824. if (m && m[1]) {
  1825. const type = normalizeInferredTypeName(m[1]);
  1826. if (type) return type;
  1827. }
  1828. }
  1829. return null;
  1830. };
  1831. // Incremental-scan memo (INFER_SCAN_STATES): this scan runs for EVERY
  1832. // `receiver.method()` ref and was measured at 61µs/ref on kong (2.4s of
  1833. // worker time, 99% misses — `self:` calls hunting a declaration Lua never
  1834. // writes). Refs for the same (file, scope, receiver) arrive in ~ascending
  1835. // line order, and the scan is a pure function of the file's immutable
  1836. // lines, so each line pays its regex matches ONCE per key instead of once
  1837. // per ref: query(c) = highest matching line in [startIdx..c]; a monotonic
  1838. // call extends the stored watermark by scanning only (hi..c] (the region
  1839. // at-or-below the previous answer is already proven empty above it); a
  1840. // non-monotonic call (rare — refs are rowid-ordered) falls back to the
  1841. // plain bounded scan and leaves the state alone. componentScoped is keyed
  1842. // out — its position-independent whole-file sweep below has different
  1843. // semantics.
  1844. if (!componentScoped) {
  1845. const states = getInferScanStates(context);
  1846. const key = `${ref.filePath}|${startIdx}|${ref.language}|${scanReceiver}`;
  1847. const state = states.get(key);
  1848. if (!state) {
  1849. for (let i = callIdx; i >= startIdx; i--) {
  1850. const type = matchLine(i);
  1851. if (type) {
  1852. states.set(key, { hi: callIdx, ansIdx: i, ansType: type });
  1853. return type;
  1854. }
  1855. }
  1856. states.set(key, { hi: callIdx, ansIdx: -1, ansType: null });
  1857. return null;
  1858. }
  1859. if (callIdx >= state.hi) {
  1860. for (let i = callIdx; i > state.hi; i--) {
  1861. const type = matchLine(i);
  1862. if (type) {
  1863. state.ansIdx = i;
  1864. state.ansType = type;
  1865. break;
  1866. }
  1867. }
  1868. state.hi = callIdx;
  1869. return state.ansIdx >= startIdx ? state.ansType : null;
  1870. }
  1871. for (let i = callIdx; i >= startIdx; i--) {
  1872. const type = matchLine(i);
  1873. if (type) return type;
  1874. }
  1875. return null;
  1876. }
  1877. // Nearest declaration wins: scan backward from the call to the scope start.
  1878. for (let i = callIdx; i >= startIdx; i--) {
  1879. const type = matchLine(i);
  1880. if (type) return type;
  1881. }
  1882. // A component-scoped field's declaration is position-independent — the
  1883. // `variables.svc = new X()` pseudoconstructor assignment or `property`
  1884. // declaration may sit BELOW the calling function in the file — so when the
  1885. // backward pass finds nothing, sweep the remainder of the file too.
  1886. if (componentScoped) {
  1887. for (let i = callIdx + 1; i < lines.length; i++) {
  1888. const type = matchLine(i);
  1889. if (type) return type;
  1890. }
  1891. }
  1892. // A PHP property with no statically-typed declaration (classic pre-7.4
  1893. // style) may still be typed by what gets ASSIGNED to it — follow the
  1894. // `$this->prop = $var` assignment to the assigned variable's own typed
  1895. // declaration (a classic or multi-line constructor parameter, or a typed
  1896. // setter's parameter).
  1897. if (phpProperty) {
  1898. return inferPhpAssignedPropertyType(escapedReceiver, lines, callIdx);
  1899. }
  1900. return null;
  1901. }
  1902. /**
  1903. * Patterns that recover a PHP class property's declared type for a
  1904. * `$this->prop` receiver. Deliberately NOT localReceiverTypePatterns: only
  1905. * property-shaped declarations qualify —
  1906. * 1. a modifier-prefixed typed declaration, which covers both a typed
  1907. * property (`private ?Foo $prop;`) and a promoted constructor parameter
  1908. * (`private readonly Foo $prop`), and
  1909. * 2. the pseudoconstructor assignment (`$this->prop = new Foo(...)`).
  1910. * A bare `X $prop` parameter or `$prop = new X()` local elsewhere in the
  1911. * file must NOT match: those variables can never alias `$this->prop`.
  1912. * Union-typed properties (`Foo|Bar $prop`) yield no match and thus no edge —
  1913. * silent beats wrong. The classic untyped-property-assigned-in-constructor
  1914. * shape is handled by inferPhpAssignedPropertyType instead.
  1915. */
  1916. function phpPropertyTypePatterns(r: string): RegExp[] {
  1917. return memoPatterns(`php-prop|${r}`, () => buildPhpPropertyTypePatterns(r));
  1918. }
  1919. function buildPhpPropertyTypePatterns(r: string): RegExp[] {
  1920. return [
  1921. new RegExp(
  1922. `\\b(?:(?:private|protected|public|readonly|static|final)(?:\\(set\\))?\\s+)+\\??([A-Za-z_\\\\][\\w\\\\]*)\\s+&?\\$${r}\\b`,
  1923. ), // private readonly ?Foo $prop (typed property / promoted param)
  1924. new RegExp(`\\$this->${r}\\b\\s*=\\s*new\\s+([A-Za-z_\\\\][\\w\\\\]*)`), // $this->prop = new Foo()
  1925. ];
  1926. }
  1927. /**
  1928. * Second-chance typing for a PHP `$this->prop` receiver whose property
  1929. * declaration carries no static type (classic pre-7.4 style): find the
  1930. * `$this->prop = $var` assignment, then recover `$var`'s type from its own
  1931. * declaration WITHIN the assignment's function — the constructor's (possibly
  1932. * multi-line) parameter list, a typed setter's parameter, or a `= new X()`
  1933. * local. The backward scan stops at the enclosing `function` line (checked
  1934. * for a match first — a single-line `__construct(Foo $var) { ... }` carries
  1935. * the typed parameter itself), so a same-named variable in another method
  1936. * can never type the property.
  1937. */
  1938. function inferPhpAssignedPropertyType(
  1939. escapedProp: string,
  1940. lines: string[],
  1941. callIdx: number,
  1942. ): string | null {
  1943. const assignRe = new RegExp(`\\$this->${escapedProp}\\b\\s*=\\s*\\$(\\w+)\\b`);
  1944. const assignAt = (i: number): RegExpMatchArray | null => {
  1945. const line = lines[i];
  1946. if (!line || line.length > 10_000) return null;
  1947. return line.match(assignRe);
  1948. };
  1949. // The assignment is position-independent relative to the call — nearest-
  1950. // backward first, then sweep forward, same order as the componentScoped scan.
  1951. let assignIdx = -1;
  1952. let varName: string | null = null;
  1953. for (let i = callIdx; i >= 0; i--) {
  1954. const m = assignAt(i);
  1955. if (m) { assignIdx = i; varName = m[1]!; break; }
  1956. }
  1957. if (varName === null) {
  1958. for (let i = callIdx + 1; i < lines.length; i++) {
  1959. const m = assignAt(i);
  1960. if (m) { assignIdx = i; varName = m[1]!; break; }
  1961. }
  1962. }
  1963. if (varName === null) return null;
  1964. const varPatterns = localReceiverTypePatterns(
  1965. 'php',
  1966. varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'),
  1967. );
  1968. for (let i = assignIdx; i >= 0; i--) {
  1969. const line = lines[i];
  1970. if (line && line.length <= 10_000) {
  1971. for (const re of varPatterns) {
  1972. const m = line.match(re);
  1973. if (m && m[1]) {
  1974. const type = normalizeInferredTypeName(m[1]);
  1975. if (type) return type;
  1976. }
  1977. }
  1978. }
  1979. if (line && /\bfunction\b/.test(line)) break;
  1980. }
  1981. return null;
  1982. }
  1983. /**
  1984. * Try to resolve by method name on a class/object
  1985. */
  1986. export function matchMethodCall(
  1987. ref: UnresolvedRef,
  1988. context: ResolutionContext
  1989. ): ResolvedRef | null {
  1990. // Parse method call patterns like "obj.method" or "Class::method". The method
  1991. // part allows trailing `:` keywords so Objective-C selectors resolve
  1992. // (`SDImageCache.storeImage:`, `obj.setX:y:`); colons never appear in other
  1993. // languages' method refs, so this is a no-op for them.
  1994. // The receiver allows dots (`builder.Services.AddCoreServices`) so a CHAINED
  1995. // call resolves by its last segment — Strategy 3 below name-matches the method
  1996. // (with its existing single-candidate / receiver-overlap guards). Without this
  1997. // a multi-dot extension-method call (C# DI `builder.Services.AddCoreServices()`,
  1998. // `Guard.Against.X()`) matched no pattern and never resolved.
  1999. // C++ explicit operator call `a.operator+(b)` reaches the resolver as
  2000. // `a.operator+` (#1247) — the operator's symbol chars (`+`, `==`, `[]`, `()`)
  2001. // fail the \w method part of the plain pattern, so admit them explicitly.
  2002. // Names like `operatorTable` stay on the plain pattern (tried first); the
  2003. // operator form requires at least one non-word char after `operator`, and
  2004. // every downstream strategy compares the method part by exact string
  2005. // equality, so a stray match can't invent an edge.
  2006. const dotMatch =
  2007. ref.referenceName.match(/^([\w.]+)\.(\w+:?(?:\w+:)*)$/) ??
  2008. (ref.language === 'cpp'
  2009. ? ref.referenceName.match(/^([\w.]+)\.(operator[^\w\s.]+)$/)
  2010. : null);
  2011. const colonMatch = ref.referenceName.match(/^(\w+)::(\w+)$/);
  2012. // Lua/Luau method calls use a single colon (`lg:log`); R uses `$` (`lg$log`).
  2013. // Recognize these receiver/method separators so local-variable receiver-type
  2014. // inference (#1108) applies to them too — extraction already emits the ref in
  2015. // this shape, but the resolver otherwise only understood `.` and `::`.
  2016. const luaColonMatch = (ref.language === 'lua' || ref.language === 'luau')
  2017. ? ref.referenceName.match(/^([\w.]+):(\w+)$/)
  2018. : null;
  2019. const rDollarMatch = ref.language === 'r'
  2020. ? ref.referenceName.match(/^([\w.]+)\$(\w+)$/)
  2021. : null;
  2022. // PHP property receiver: `$this->prop->method()` reaches the resolver as
  2023. // `this->prop.method` (the extractor records the receiver's raw text with the
  2024. // leading `$` stripped). Resolve it EXCLUSIVELY through declared-type
  2025. // inference + resolveMethodOnType validation — the name-similarity strategies
  2026. // below must never see this shape, so a property whose type can't be
  2027. // recovered stays unlinked rather than guessed (a wrong inference produces no
  2028. // edge rather than a wrong one). Deeper chains (`this->a->b.method`) don't
  2029. // match the single-property pattern and stay unlinked, same as before.
  2030. const phpThisPropMatch = ref.language === 'php'
  2031. ? ref.referenceName.match(/^(this->\w+)\.(\w+)$/)
  2032. : null;
  2033. if (phpThisPropMatch) {
  2034. const [, receiver, phpMethodName] = phpThisPropMatch;
  2035. const inferredType = inferLocalReceiverType(receiver!, ref, context);
  2036. if (!inferredType) return null;
  2037. return resolveMethodOnType(
  2038. inferredType,
  2039. phpMethodName!,
  2040. ref,
  2041. context,
  2042. 0.9,
  2043. 'instance-method',
  2044. importedFqnOf(inferredType, ref, context),
  2045. );
  2046. }
  2047. const match = dotMatch || colonMatch || luaColonMatch || rDollarMatch;
  2048. if (!match) {
  2049. return null;
  2050. }
  2051. const [, objectOrClass, methodName] = match;
  2052. // A simple `receiver.method` / `receiver:method` / `receiver$method` shape whose
  2053. // receiver type we can try to infer from its local declaration.
  2054. const inferableReceiver = dotMatch || luaColonMatch || rDollarMatch;
  2055. // Infer the receiver's type from its local declaration/initializer in the
  2056. // enclosing scope, then resolve the method on that type (#1108). C++ keeps its
  2057. // dedicated inferrer (header scan + `auto`); every other language uses the
  2058. // shared source-based inferrer. resolveMethodOnType validates the method
  2059. // exists on the inferred type, so a mis-inference produces no edge.
  2060. if (inferableReceiver) {
  2061. const inferredType = nmTimedT('mc-infer', ref, () =>
  2062. ref.language === 'cpp'
  2063. ? inferCppReceiverType(objectOrClass!, ref, context)
  2064. : inferLocalReceiverType(objectOrClass!, ref, context));
  2065. if (inferredType) {
  2066. // Java/Kotlin: when two classes share the simple name, the file's import
  2067. // pins WHICH one (#314). Other languages disambiguate by call-site file.
  2068. const importedFqn =
  2069. ref.language === 'java' || ref.language === 'kotlin'
  2070. ? context
  2071. .getImportMappings(ref.filePath, ref.language)
  2072. .find((i) => i.localName === inferredType)?.source
  2073. : undefined;
  2074. const typedMatch = nmTimedT('mc-rmot', ref, () => resolveMethodOnType(
  2075. inferredType,
  2076. methodName!,
  2077. ref,
  2078. context,
  2079. 0.9,
  2080. 'instance-method',
  2081. importedFqn,
  2082. ));
  2083. if (typedMatch) {
  2084. return typedMatch;
  2085. }
  2086. // A known JS/TS builtin receiver is external when it has no project
  2087. // method (#1566). Inference already strips generics (`Map<K, V>` →
  2088. // `Map`); do not let Strategy 3 guess an unrelated `get`/`set`/`has`.
  2089. // Keep the validated match above for a project type shadowing a builtin.
  2090. if (ESM_FAMILY.has(ref.language) && JS_BUILT_INS.has(inferredType)) {
  2091. return null;
  2092. }
  2093. }
  2094. }
  2095. // Go 2-hop field chain `base.field.Method` (#1276): the base's type comes
  2096. // from the enclosing scope (typed parameter / method receiver / local var),
  2097. // the field's declared type from that struct's own declaration lines, and
  2098. // the method is VALIDATED on the field's type by resolveMethodOnType. This
  2099. // branch is EXCLUSIVE for chained Go receivers: when the hop can't be
  2100. // inferred or the field's type is external (`conn *sql.DB` — no project
  2101. // node), the ref stays unresolved rather than falling through to the
  2102. // bare-name strategies below, which is exactly how `target.conn.Exec(...)`
  2103. // fabricated a dependency on an unrelated local interface's same-named
  2104. // method. Chained Go receivers were never emitted before #1276, so there
  2105. // is no prior recall to preserve on the fallback path.
  2106. if (ref.language === 'go' && dotMatch && objectOrClass!.includes('.')) {
  2107. return matchGoFieldChainCall(objectOrClass!, methodName!, ref, context);
  2108. }
  2109. // Rust call through a field of the enclosing type — `self.inner.run()`,
  2110. // emitted as `self.inner.run` (#1585). Same discipline as the Go branch
  2111. // above, and EXCLUSIVE for the same reason: validated field-type inference
  2112. // or nothing. Letting this shape reach the bare-name strategies below is
  2113. // how `self.inner.run()` resolved to a same-named method on an unrelated
  2114. // type — or to the calling method itself, a self-edge the source doesn't
  2115. // contain — whenever the field's type was external or merely shared a
  2116. // method name with something nearby.
  2117. if (ref.language === 'rust' && dotMatch && objectOrClass!.startsWith('self.')) {
  2118. return matchRustSelfFieldCall(objectOrClass!.slice('self.'.length), methodName!, ref, context);
  2119. }
  2120. // TS/JS call through a field of the enclosing class — `this.mailer.send()`,
  2121. // emitted as `this.mailer.send` (#1496). Same discipline as the Rust branch
  2122. // above, and EXCLUSIVE for the same reason: the field's declared type off
  2123. // the class's own declaration, validated by resolveMethodOnType, or nothing.
  2124. // Letting the bare name through is how `this.mailer.send()` inside
  2125. // `Notifier.send()` resolved to the calling method itself — a self-edge the
  2126. // source does not contain — whenever the two shared a name.
  2127. if (
  2128. (ref.language === 'typescript' || ref.language === 'javascript' || ref.language === 'tsx' || ref.language === 'jsx') &&
  2129. dotMatch &&
  2130. objectOrClass!.startsWith('this.')
  2131. ) {
  2132. return matchTsThisFieldCall(objectOrClass!.slice('this.'.length), methodName!, ref, context);
  2133. }
  2134. // Java/Kotlin: receiver may be a field whose name doesn't match the type by
  2135. // Java naming convention (`userbo` → class `UserBO`, abbreviated). Look up
  2136. // the field in the enclosing class to get its declared type, then resolve
  2137. // the method on that type. Covers Spring `@Resource`/`@Autowired` field
  2138. // injection where the field type is the concrete bean class.
  2139. if ((ref.language === 'java' || ref.language === 'kotlin') && dotMatch) {
  2140. const inferredType = inferJavaFieldReceiverType(objectOrClass!, ref, context);
  2141. if (inferredType) {
  2142. // When two classes share the same simple name, the caller file's
  2143. // import is the only signal that names WHICH one — pass the
  2144. // imported FQN so resolveMethodOnType can disambiguate (#314).
  2145. const imports = context.getImportMappings(ref.filePath, ref.language);
  2146. const importedFqn = imports.find((i) => i.localName === inferredType)?.source;
  2147. const typedMatch = nmTimedT('mc-rmot', ref, () => resolveMethodOnType(
  2148. inferredType,
  2149. methodName!,
  2150. ref,
  2151. context,
  2152. 0.9,
  2153. 'instance-method',
  2154. importedFqn,
  2155. ));
  2156. if (typedMatch) {
  2157. return typedMatch;
  2158. }
  2159. }
  2160. }
  2161. // Object-literal namespace receiver (#1573): `api.call()` where `api` is a
  2162. // same-file `const api = { call() {…}, get: () => {…} }`. Its members are
  2163. // plain functions with bare names inside the constant's extent — no
  2164. // `Container::member` qualified name — so none of the class-shaped
  2165. // strategies below can see them (Strategy 3 only considers `method`
  2166. // kinds) and the call resolved to nothing at all. Same file only: a
  2167. // cross-file use reaches the same helper through the import path.
  2168. if (dotMatch && !objectOrClass!.includes('.') && OBJECT_LITERAL_LANGUAGES.has(ref.language)) {
  2169. const literalMatch = nmTimedT('mc-literal', ref, (): ResolvedRef | null => {
  2170. const holders = preferCallSiteFile(context.getNodesByName(objectOrClass!), ref.filePath).filter(
  2171. (n) => (n.kind === 'constant' || n.kind === 'variable') && n.filePath === ref.filePath
  2172. );
  2173. for (const holder of holders) {
  2174. const hit = resolveObjectLiteralMember(holder, methodName!, ref, context, 0.85, 'instance-method');
  2175. if (hit) return hit;
  2176. }
  2177. return null;
  2178. });
  2179. if (literalMatch) return literalMatch;
  2180. }
  2181. // Strategy 1: Direct class name match (existing logic). When the receiver
  2182. // names a class that exists in several files (`Logger.log()` / `Logger::log()`
  2183. // with a `Logger` in both `a/` and `b/`), try the class in the call site's
  2184. // own file first — otherwise the first-indexed class wins and a call in `b/`
  2185. // resolves to `a/`'s method (#1079).
  2186. const strat1 = nmTimedT('mc-class', ref, (): ResolvedRef | null => {
  2187. const classCandidates = preferCallSiteFile(
  2188. context.getNodesByName(objectOrClass!),
  2189. ref.filePath,
  2190. );
  2191. for (const classNode of classCandidates) {
  2192. if (classNode.kind === 'class' || classNode.kind === 'struct' || classNode.kind === 'union' || classNode.kind === 'interface') {
  2193. // Skip cross-language class matches
  2194. if (classNode.language !== ref.language) continue;
  2195. const nodesInFile = context.getNodesInFile(classNode.filePath);
  2196. const methodNode = nodesInFile.find(
  2197. (n) =>
  2198. n.kind === 'method' &&
  2199. n.name === methodName &&
  2200. n.qualifiedName.includes(classNode.name)
  2201. );
  2202. if (methodNode) {
  2203. return {
  2204. original: ref,
  2205. targetNodeId: methodNode.id,
  2206. confidence: 0.85,
  2207. resolvedBy: 'qualified-name',
  2208. };
  2209. }
  2210. }
  2211. }
  2212. return null;
  2213. });
  2214. if (strat1) return strat1;
  2215. // Strategy 2: Instance variable receiver - try capitalized form to find class
  2216. // e.g., "permissionEngine" → look for classes containing "PermissionEngine"
  2217. const capitalizedReceiver = objectOrClass!.charAt(0).toUpperCase() + objectOrClass!.slice(1);
  2218. if (capitalizedReceiver !== objectOrClass) {
  2219. const strat2 = nmTimedT('mc-capital', ref, (): ResolvedRef | null => {
  2220. const fuzzyClassCandidates = preferCallSiteFile(
  2221. context.getNodesByName(capitalizedReceiver),
  2222. ref.filePath,
  2223. );
  2224. for (const classNode of fuzzyClassCandidates) {
  2225. if (classNode.kind === 'class' || classNode.kind === 'struct' || classNode.kind === 'union' || classNode.kind === 'interface') {
  2226. // Skip cross-language class matches
  2227. if (classNode.language !== ref.language) continue;
  2228. const nodesInFile = context.getNodesInFile(classNode.filePath);
  2229. const methodNode = nodesInFile.find(
  2230. (n) =>
  2231. n.kind === 'method' &&
  2232. n.name === methodName &&
  2233. n.qualifiedName.includes(classNode.name)
  2234. );
  2235. if (methodNode) {
  2236. return {
  2237. original: ref,
  2238. targetNodeId: methodNode.id,
  2239. confidence: 0.8,
  2240. resolvedBy: 'instance-method',
  2241. };
  2242. }
  2243. }
  2244. }
  2245. return null;
  2246. });
  2247. if (strat2) return strat2;
  2248. }
  2249. // Strategy 3: Find methods by name across the codebase, match by receiver
  2250. // name similarity with the containing class. Handles abbreviated variable
  2251. // names like permissionEngine → PermissionRuleEngine.
  2252. if (methodName) {
  2253. const strat3 = nmTimedT('mc-byname', ref, (): ResolvedRef | null => {
  2254. const methodCandidates = context.getNodesByName(methodName!);
  2255. // Ubiquitous-method ceiling (#999): a method name re-declared across a
  2256. // vendored theme/SDK (Metronic's `init`/`update`/… on every widget) yields
  2257. // K candidates that receiver-word overlap can't reliably disambiguate —
  2258. // and filtering + scoring all K per call is the O(K²) cost that wedged
  2259. // "Resolving refs" for 15-28 min. Bail before the O(K) work; Strategy 1/2
  2260. // (class-name match) already had their precise shot above.
  2261. if (methodCandidates.length > AMBIGUOUS_NAME_CEILING) {
  2262. return null;
  2263. }
  2264. const methods = methodCandidates.filter(
  2265. (n) => n.kind === 'method' && n.name === methodName
  2266. );
  2267. // Filter to same-language candidates first
  2268. const sameLanguageMethods = methods.filter(m => m.language === ref.language);
  2269. const targetMethods = sameLanguageMethods.length > 0 ? sameLanguageMethods : methods;
  2270. // If only one same-language method with this name exists, use it
  2271. if (targetMethods.length === 1 && targetMethods[0]!.language === ref.language) {
  2272. return {
  2273. original: ref,
  2274. targetNodeId: targetMethods[0]!.id,
  2275. confidence: 0.7,
  2276. resolvedBy: 'instance-method',
  2277. };
  2278. }
  2279. // Multiple methods: score by receiver name word overlap with class name
  2280. if (targetMethods.length > 1) {
  2281. const receiverWords = splitCamelCase(objectOrClass!);
  2282. let bestMatch: typeof targetMethods[0] | undefined;
  2283. let bestScore = 0;
  2284. // Same-file candidates first, so a score tie (`score > bestScore` keeps
  2285. // the first seen) resolves to the call site's own file rather than the
  2286. // first-indexed duplicate (#1079).
  2287. for (const method of preferCallSiteFile(targetMethods, ref.filePath)) {
  2288. const classWords = splitCamelCase(method.qualifiedName);
  2289. let score = receiverWords.filter(w =>
  2290. classWords.some(cw => cw.toLowerCase() === w.toLowerCase())
  2291. ).length;
  2292. // Bonus for same language
  2293. if (method.language === ref.language) score += 1;
  2294. if (score > bestScore) {
  2295. bestScore = score;
  2296. bestMatch = method;
  2297. }
  2298. }
  2299. if (bestMatch && bestScore >= 2) {
  2300. return {
  2301. original: ref,
  2302. targetNodeId: bestMatch.id,
  2303. confidence: 0.65,
  2304. resolvedBy: 'instance-method',
  2305. };
  2306. }
  2307. }
  2308. return null;
  2309. });
  2310. if (strat3) return strat3;
  2311. }
  2312. return null;
  2313. }
  2314. /** Go builtin/primitive field types that can never carry a project method. */
  2315. const GO_BUILTIN_FIELD_TYPES = new Set([
  2316. 'string', 'bool', 'byte', 'rune', 'error', 'any',
  2317. 'int', 'int8', 'int16', 'int32', 'int64',
  2318. 'uint', 'uint8', 'uint16', 'uint32', 'uint64', 'uintptr',
  2319. 'float32', 'float64', 'complex64', 'complex128',
  2320. 'chan', 'map', 'func', 'struct', 'interface',
  2321. ]);
  2322. /**
  2323. * Resolve a Go 2-hop field-chain call `base.field.Method(...)` (#1276):
  2324. * `target.conn.Exec("insert")` where `func (target *Target) Write()` and
  2325. * `type Target struct { conn *sql.DB }`. Two inference hops, both read from
  2326. * source the same way #1108 does:
  2327. * 1. `base`'s type from the enclosing scope (method receiver, typed
  2328. * parameter, or local declaration) via inferLocalReceiverType;
  2329. * 2. `field`'s declared type from the struct's own declaration lines.
  2330. * The method is then resolved AND VALIDATED on the field's type. A field
  2331. * whose type has no project node (`sql.DB`, any external dependency) yields
  2332. * null — the caller treats this branch as exclusive for chained Go
  2333. * receivers, so the ref stays unresolved instead of name-guessing.
  2334. */
  2335. function matchGoFieldChainCall(
  2336. receiverChain: string,
  2337. methodName: string,
  2338. ref: UnresolvedRef,
  2339. context: ResolutionContext
  2340. ): ResolvedRef | null {
  2341. const segs = receiverChain.split('.');
  2342. if (segs.length !== 2 || !segs[0] || !segs[1]) return null;
  2343. const [base, field] = segs;
  2344. const baseType = inferLocalReceiverType(base!, ref, context);
  2345. if (!baseType) return null;
  2346. const fieldEsc = field!.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  2347. const fieldTypeRe = new RegExp(`\\b${fieldEsc}\\s+\\*?\\[?\\]?([A-Za-z_][\\w.]*)`);
  2348. const structs = preferCallSiteFile(context.getNodesByName(baseType), ref.filePath).filter(
  2349. (n) => (n.kind === 'struct' || n.kind === 'class') && n.language === 'go'
  2350. );
  2351. for (const s of structs) {
  2352. const source = context.readFile(s.filePath);
  2353. if (!source) continue;
  2354. // Only the struct's own declaration lines — a same-named identifier
  2355. // elsewhere in the file can't donate a type. Matched LINE BY LINE with
  2356. // comments stripped: chi's `Mux` has a doc comment reading "the tree
  2357. // router" right above `tree *node`, and a whole-block match captured
  2358. // `router` from the prose instead of `node` from the field.
  2359. const declLines = source.split('\n').slice(Math.max(0, s.startLine - 1), s.endLine);
  2360. for (const rawLine of declLines) {
  2361. const line = rawLine.replace(/\/\/.*$/, '').replace(/\/\*.*?\*\//g, '');
  2362. const m = line.match(fieldTypeRe);
  2363. if (!m || !m[1]) continue;
  2364. const rawType = m[1];
  2365. // A package-qualified field type (`http.Handler`, `sql.DB`) is only
  2366. // followed when the package is IN-MODULE: stripping the qualifier and
  2367. // matching the bare name would conflate a stdlib/third-party type with
  2368. // any same-named project type — on chi, `handler http.Handler` bound
  2369. // to an example app's unrelated local `Handler`. That is the exact
  2370. // fabrication this matcher exists to prevent (#1276).
  2371. if (rawType.includes('.')) {
  2372. const pkg = rawType.split('.')[0]!;
  2373. const mod = context.getGoModule?.();
  2374. const imp = context
  2375. .getImportMappings(s.filePath, 'go')
  2376. .find((i) => i.localName === pkg);
  2377. const inModule =
  2378. !!mod &&
  2379. !!imp &&
  2380. (imp.source === mod.modulePath || imp.source.startsWith(mod.modulePath + '/'));
  2381. if (!inModule) continue;
  2382. }
  2383. // Unexported (lowercase) types are idiomatic Go and stay eligible —
  2384. // chi's `mx.tree.FindRoute()` chains through `tree *node`. A
  2385. // mis-capture is harmless: resolveMethodOnType only returns a
  2386. // validated `<type>::<method>` match.
  2387. const fieldType = rawType.split('.').pop();
  2388. if (!fieldType || !/^[A-Za-z_]/.test(fieldType) || GO_BUILTIN_FIELD_TYPES.has(fieldType)) continue;
  2389. const resolved = resolveMethodOnType(fieldType, methodName, ref, context, 0.85, 'instance-method');
  2390. if (resolved) return resolved;
  2391. }
  2392. }
  2393. return null;
  2394. }
  2395. // Rust primitives and the prelude's own types: a field of one of these never
  2396. // names a project type, so a `self.<field>.<method>()` on it stays unresolved.
  2397. const RUST_NON_PROJECT_FIELD_TYPES = new Set([
  2398. 'bool', 'char', 'str', 'String',
  2399. 'i8', 'i16', 'i32', 'i64', 'i128', 'isize',
  2400. 'u8', 'u16', 'u32', 'u64', 'u128', 'usize',
  2401. 'f32', 'f64',
  2402. 'Self', 'self',
  2403. ]);
  2404. /**
  2405. * Reduce a Rust field's declared type text to the simple name of the type a
  2406. * method call on that field auto-derefs to, or null when there is none we can
  2407. * name. Only the layers Rust's method-call auto-deref looks through are
  2408. * unwrapped: references (`&`, `&'a mut`) and the owning smart pointers
  2409. * (`Box`, `Rc`, `Arc`) — `self.inner.run()` with `inner: Box<Inner>` calls
  2410. * `Inner::run`. Containers that do NOT auto-deref to their parameter
  2411. * (`Option<Inner>`, `Vec<Inner>`, `Mutex<Inner>`, `RefCell<Inner>`) keep their
  2412. * own name and, having no project node, resolve to nothing — `self.items.push()`
  2413. * must never become `Inner::push`. A trait object (`Box<dyn Source>`) yields
  2414. * the trait, whose method node the interface-impl synthesizer fans out. A
  2415. * generic parameter (`T`), a primitive, a tuple / array / raw pointer / fn
  2416. * type, or a non-identifier yields null.
  2417. */
  2418. export function rustFieldTypeName(raw: string): string | null {
  2419. let t = raw.trim();
  2420. for (;;) {
  2421. const before = t;
  2422. t = t.replace(/^&\s*(?:'\w+\s+)?(?:mut\s+)?/, '');
  2423. t = t.replace(/^(?:Box|Rc|Arc)\s*<\s*/, '');
  2424. t = t.replace(/^(?:dyn|impl)\s+/, '');
  2425. if (t === before) break;
  2426. }
  2427. // Drop generic args, the closing `>`s of unwrapped pointers, and trait-object
  2428. // bounds (`dyn Source + Send`); keep the last path segment.
  2429. t = t.replace(/[<>+].*$/, '').trim();
  2430. const seg = t.split('::').filter(Boolean).pop();
  2431. if (!seg || !/^[A-Za-z_]\w*$/.test(seg)) return null;
  2432. if (RUST_NON_PROJECT_FIELD_TYPES.has(seg)) return null;
  2433. if (/^[A-Z]$/.test(seg)) return null; // bare single-letter generic parameter
  2434. return seg;
  2435. }
  2436. /**
  2437. * Resolve a Rust call through a field of the enclosing type —
  2438. * `self.inner.run()`, emitted by the extractor as `self.inner.run` (#1585).
  2439. * Mirrors the Go 2-hop precedent above (#1276): the owner type is the calling
  2440. * method's qualified-name prefix (`Outer::run` → `Outer`), the field's declared
  2441. * type comes from the owner struct's OWN declaration lines, and the method is
  2442. * resolved AND VALIDATED on that type by resolveMethodOnType. The caller
  2443. * treats this branch as exclusive for `self.<field>` receivers: a field whose
  2444. * type is external (`std::vec::IntoIter`, `regex::Regex`), a generic
  2445. * parameter, or not declared where we can see it yields null and the ref stays
  2446. * unresolved. Rust struct fields are not graph nodes, so the declaration text
  2447. * is the only place the type lives.
  2448. */
  2449. function matchRustSelfFieldCall(
  2450. field: string,
  2451. methodName: string,
  2452. ref: UnresolvedRef,
  2453. context: ResolutionContext,
  2454. ): ResolvedRef | null {
  2455. // The extractor only ever emits a single field hop; anything else is not ours.
  2456. if (!field || field.includes('.')) return null;
  2457. const caller = context.getNodeById?.(ref.fromNodeId);
  2458. if (!caller) return null;
  2459. const sep = caller.qualifiedName.lastIndexOf('::');
  2460. if (sep <= 0) return null; // a free fn has no `self`
  2461. const owner = caller.qualifiedName.slice(0, sep).split('::').pop();
  2462. if (!owner) return null;
  2463. const owners = preferCallSiteFile(context.getNodesByName(owner), ref.filePath).filter(
  2464. (n) =>
  2465. (n.kind === 'struct' || n.kind === 'union' || n.kind === 'class') &&
  2466. n.language === 'rust'
  2467. );
  2468. const fieldEsc = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  2469. // `pub inner: Inner,` / `inner: Box<dyn Source>,` / `pub(crate) inner: T }` —
  2470. // the type text runs to the field separator. A comma inside generic args
  2471. // (`HashMap<K, V>`) truncates the capture, which rustFieldTypeName then
  2472. // reduces to the container's own name — exactly the non-deref case it
  2473. // refuses anyway.
  2474. const fieldRe = new RegExp(`\\b${fieldEsc}\\s*:\\s*([^,{}]+)`);
  2475. for (const s of owners) {
  2476. const source = context.readFile(s.filePath);
  2477. if (!source) continue;
  2478. // Only the struct's own declaration lines, comment-stripped line by line —
  2479. // same discipline as the Go helper: prose or a same-named identifier
  2480. // elsewhere in the file can never donate a type.
  2481. const declLines = source.split('\n').slice(Math.max(0, s.startLine - 1), s.endLine);
  2482. for (const rawLine of declLines) {
  2483. const line = rawLine.replace(/\/\/.*$/, '').replace(/\/\*.*?\*\//g, '');
  2484. const m = line.match(fieldRe);
  2485. if (!m || !m[1]) continue;
  2486. const fieldType = rustFieldTypeName(m[1]);
  2487. // The field is declared here; whether or not its type names a project
  2488. // symbol, this owner is the answer — no other same-named struct applies.
  2489. if (!fieldType) return null;
  2490. return resolveMethodOnType(fieldType, methodName, ref, context, 0.85, 'instance-method');
  2491. }
  2492. }
  2493. return null;
  2494. }
  2495. /**
  2496. * Resolve a TS/JS `this.<field>.<method>()` call (#1496) through the field's
  2497. * declared type, read off the ENCLOSING class's own declaration lines:
  2498. * a field or constructor-parameter property (`private mailer: Mailer`,
  2499. * `mailer?: Mailer`, `readonly mailer: Mailer`) or an initializer
  2500. * (`mailer = new Mailer()`, `this.mailer = new Mailer()`). The method is then
  2501. * VALIDATED on that type by resolveMethodOnType. Null — never a bare-name
  2502. * fallback — when the field is not declared there or its type is external,
  2503. * a builtin (`this.items.push()`) or not spelled out.
  2504. */
  2505. function matchTsThisFieldCall(
  2506. field: string,
  2507. methodName: string,
  2508. ref: UnresolvedRef,
  2509. context: ResolutionContext,
  2510. ): ResolvedRef | null {
  2511. if (!field || field.includes('.')) return null;
  2512. const caller = context.getNodeById?.(ref.fromNodeId);
  2513. if (!caller) return null;
  2514. const sep = caller.qualifiedName.lastIndexOf('::');
  2515. if (sep <= 0) return null; // not inside a class
  2516. const owner = caller.qualifiedName.slice(0, sep).split('::').pop();
  2517. if (!owner) return null;
  2518. const owners = preferCallSiteFile(context.getNodesByName(owner), ref.filePath).filter(
  2519. (n) => (n.kind === 'class' || n.kind === 'component') && sameLanguageFamily(n.language, ref.language)
  2520. );
  2521. const fieldEsc = field.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  2522. const patterns: Array<{ re: RegExp; valueType: boolean }> = [
  2523. // `storage: typeof DraftHubStorage` — the type OF a value: an object
  2524. // literal used as a namespace. Its members are bare-named functions inside
  2525. // the constant's extent (#1573), so they are found by containment, not by
  2526. // `Type::method`. Tried first: the declared-type pattern below would
  2527. // otherwise capture the word `typeof`.
  2528. {
  2529. re: new RegExp(`\\b${fieldEsc}\\b\\s*[?!]?\\s*:\\s*(?:readonly\\s+)?typeof\\s+([A-Za-z_$][\\w.$]*)`),
  2530. valueType: true,
  2531. },
  2532. // `private readonly mailer?: Mailer` — a class field or a constructor
  2533. // parameter property; the capture stops at `<`, `[` or `|`, so a generic
  2534. // or union type yields its head and resolveMethodOnType decides.
  2535. {
  2536. re: new RegExp(`\\b${fieldEsc}\\b\\s*[?!]?\\s*:\\s*(?:readonly\\s+)?([A-Za-z_$][\\w.$]*)`),
  2537. valueType: false,
  2538. },
  2539. // `mailer = new Mailer()` / `this.mailer = new Mailer()`
  2540. { re: new RegExp(`\\b${fieldEsc}\\b\\s*=\\s*new\\s+([A-Za-z_$][\\w.$]*)`), valueType: false },
  2541. ];
  2542. for (const cls of owners) {
  2543. const source = context.readFile(cls.filePath);
  2544. if (!source) continue;
  2545. const declLines = source.split('\n').slice(Math.max(0, cls.startLine - 1), cls.endLine);
  2546. for (const rawLine of declLines) {
  2547. const line = rawLine.replace(/\/\/.*$/, '').replace(/\/\*.*?\*\//g, '');
  2548. for (const { re, valueType } of patterns) {
  2549. const m = line.match(re);
  2550. if (!m || !m[1]) continue;
  2551. if (valueType) {
  2552. // The value's declaration may live in another file (it is imported);
  2553. // the call site's file is preferred when several share the name.
  2554. const holderName = m[1].split('.').pop()!;
  2555. const holders = preferCallSiteFile(context.getNodesByName(holderName), ref.filePath).filter(
  2556. (n) => (n.kind === 'constant' || n.kind === 'variable') && sameLanguageFamily(n.language, ref.language)
  2557. );
  2558. for (const holder of holders) {
  2559. const hit = resolveObjectLiteralMember(holder, methodName, ref, context, 0.85, 'instance-method');
  2560. if (hit) return hit;
  2561. }
  2562. return null;
  2563. }
  2564. // `ns.Mailer` → `Mailer`; a primitive or builtin names no project type.
  2565. const typeName = m[1].split('.').pop()!;
  2566. if (!/^[A-Z]/.test(typeName)) return null;
  2567. // Two apps in one repo may each declare a `UserService`. The bare-name
  2568. // path this replaces broke that tie by directory proximity, so keep the
  2569. // same signal: among the type's declarations of the method, prefer the
  2570. // one closest to the call site's directory (its own app), never index
  2571. // order. resolveMethodOnType still answers the single-declaration and
  2572. // supertype cases.
  2573. const declared = context
  2574. .getNodesByName(methodName)
  2575. .filter(
  2576. (n) =>
  2577. n.kind === 'method' &&
  2578. sameLanguageFamily(n.language, ref.language) &&
  2579. (n.qualifiedName === `${typeName}::${methodName}` || n.qualifiedName.endsWith(`::${typeName}::${methodName}`))
  2580. );
  2581. if (declared.length > 1) {
  2582. const callDirs = ref.filePath.split('/').slice(0, -1);
  2583. const shared = (fp: string) => {
  2584. const dirs = fp.split('/').slice(0, -1);
  2585. let i = 0;
  2586. while (i < dirs.length && i < callDirs.length && dirs[i] === callDirs[i]) i++;
  2587. return i;
  2588. };
  2589. const nearest = [...declared].sort((a, b) => shared(b.filePath) - shared(a.filePath) || a.filePath.localeCompare(b.filePath))[0]!;
  2590. return { original: ref, targetNodeId: nearest.id, confidence: 0.85, resolvedBy: 'instance-method' };
  2591. }
  2592. return resolveMethodOnType(typeName, methodName, ref, context, 0.85, 'instance-method');
  2593. }
  2594. }
  2595. }
  2596. return null;
  2597. }
  2598. /**
  2599. * The one fallback a TS/JS/Python call-receiver chain keeps (#1683): a STORE
  2600. * ACCESSOR. Zustand's `get()` inside the store factory and
  2601. * `useStore.getState()` outside it hand back the store whose actions are
  2602. * indexed as functions (#1573). JS/TS resolves the member within that store;
  2603. * the existing Python fallback still requires a unique callable. Nothing else
  2604. * qualifies: a chain rooted in a project value still says nothing about what
  2605. * the inner call RETURNS — `db.prepare(sql).all()` would bind to any project
  2606. * function named `all` — so it resolves to nothing, exactly like a chain
  2607. * rooted in a parameter (`d.setdefault(k, []).append(v)`).
  2608. */
  2609. function matchStoreAccessorChain(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
  2610. const m = ref.referenceName.match(/^([\w$.]+)\(\)\.(\w+)$/);
  2611. if (!m || !m[1] || !m[2]) return null;
  2612. const inner = m[1];
  2613. const method = m[2];
  2614. if (!(inner === 'get' || inner === 'getState' || inner.endsWith('.getState'))) return null;
  2615. if (JS_FAMILY.has(ref.language)) {
  2616. return resolveStoreAction(inner, method, ref, context);
  2617. }
  2618. const callables = context
  2619. .getNodesByName(method)
  2620. .filter((n) => (n.kind === 'function' || n.kind === 'method') && sameLanguageFamily(n.language, ref.language) && n.id !== ref.fromNodeId);
  2621. if (callables.length !== 1) return null;
  2622. return { original: ref, targetNodeId: callables[0]!.id, confidence: 0.6, resolvedBy: 'exact-match' };
  2623. }
  2624. /** Resolve the implementation inside the identified store, not a namesake or
  2625. * an interface signature elsewhere in the project. Import resolution already
  2626. * follows aliases/barrels; containment already excludes nested action locals. */
  2627. function resolveStoreAction(inner: string, member: string, ref: UnresolvedRef, context: ResolutionContext, selector = false): ResolvedRef | null {
  2628. let holders: Node[];
  2629. if (inner === 'get' || inner === 'getState') {
  2630. const caller = context.getNodeById?.(ref.fromNodeId);
  2631. if (!caller) return null;
  2632. holders = context.getNodesInFile(ref.filePath).filter((n) => {
  2633. if ((n.kind !== 'constant' && n.kind !== 'variable') || !rangeWithin(caller, n)) return false;
  2634. const source = context.readFile(n.filePath)?.split('\n').slice(n.startLine - 1, caller.startLine).join('\n') ?? '';
  2635. // The accessor must actually be a parameter of the enclosing factory.
  2636. return new RegExp(`\\(\\s*[\\w$]+\\s*,\\s*${inner}\\s*(?:,\\s*[\\w$]+\\s*)?\\)\\s*=>`).test(source);
  2637. });
  2638. } else {
  2639. const name = inner.slice(0, -'.getState'.length);
  2640. if (!/^[\w$]+$/.test(name)) return null;
  2641. const imported = resolveViaImport({ ...ref, referenceName: name, referenceKind: 'references' }, context);
  2642. const node = imported && context.getNodeById?.(imported.targetNodeId);
  2643. if (node && importShadowedAt(name, ref, context)) return null;
  2644. holders = node ? [node] : context.getNodesByName(name).filter((n) =>
  2645. n.filePath === ref.filePath && isLexicallyReachable(n, ref, context));
  2646. }
  2647. if (holders.length !== 1) return null;
  2648. const holder = holders[0]!;
  2649. if (selector) {
  2650. // Only a Zustand hook promises to return the selector's result. An
  2651. // arbitrary function accepting that callback is not a store binding.
  2652. const text = context.readFile(holder.filePath)?.split('\n').slice(holder.startLine - 1, holder.endLine).join('\n') ?? '';
  2653. const escaped = holder.name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  2654. const factory = new RegExp(`\\b(?:const|let)\\s+${escaped}\\s*=\\s*([\\w$]+)\\s*[<(]`).exec(text)?.[1];
  2655. if (!factory || !context.getImportMappings(holder.filePath, holder.language).some(m =>
  2656. m.localName === factory && m.source === 'zustand' && (m.exportedName === 'create' || m.isDefault))) return null;
  2657. }
  2658. return resolveObjectLiteralMember(holder, member, ref, context, 0.9, 'instance-method');
  2659. }
  2660. /** A const destructuring is a bound reference, so it is eligible even though
  2661. * arbitrary locally-bound bare calls must never guess a cross-file target. */
  2662. function matchDestructuredStoreCall(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
  2663. const source = context.readFile(ref.filePath);
  2664. if (!source?.includes('.getState')) return null;
  2665. const lines = source.split('\n');
  2666. const start = enclosingScopeStartLine(ref, context) - 1;
  2667. const before = lines.slice(start, ref.line - 1).concat(lines[ref.line - 1]!.slice(0, ref.column)).join('\n');
  2668. const code = blankStringContents(stripCommentsForRegex(before, 'typescript'));
  2669. const name = ref.referenceName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  2670. const binding = /\bconst\s*\{([^{}]*)\}\s*=\s*([\w$]+)\.getState\s*\(\s*\)/g;
  2671. // Compare block identities, not just nesting depth: a binding in a sibling
  2672. // or already-closed block is not in scope at this call.
  2673. const stackAt = (end: number): number[] => {
  2674. const stack: number[] = [];
  2675. for (let i = 0; i < end; i++) {
  2676. if (code[i] === '{') stack.push(i);
  2677. else if (code[i] === '}') stack.pop();
  2678. }
  2679. return stack;
  2680. };
  2681. const callScope = stackAt(code.length);
  2682. for (const m of [...code.matchAll(binding)].reverse()) {
  2683. // Plain named bindings only; defaults, rest and computed keys need their
  2684. // own value tracing rather than a same-name guess.
  2685. if (!m[1]!.split(',').some(part => part.trim() === ref.referenceName)) continue;
  2686. const scope = stackAt(m.index!);
  2687. if (!scope.every((pos, i) => callScope[i] === pos)) continue;
  2688. const rest = code.slice(m.index! + m[0].length);
  2689. // Keep the guard when another declaration shadows the captured const.
  2690. if (new RegExp(`\\b(?:const|let|var|function|class)\\s+(?:${name}\\b|\\{[^}]*\\b${name}\\b)`).test(rest)) return null;
  2691. return resolveStoreAction(`${m[2]}.getState`, ref.referenceName, ref, context);
  2692. }
  2693. return null;
  2694. }
  2695. /** Bound action names need not have a same-named definition (selectors may
  2696. * rename them). The resolver's symbol-existence prefilter must allow them. */
  2697. export function matchJsStoreBindingCall(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
  2698. if (!isBareJsCall(ref, context)) return null;
  2699. return matchDestructuredStoreCall(ref, context) ?? matchSelectedStoreCall(ref, context);
  2700. }
  2701. /** A qualified untyped chain is useful source evidence, not permission to
  2702. * infer a property type. Framework resolution runs before this guard. */
  2703. export function isUnresolvedJsMemberCall(ref: UnresolvedRef): boolean {
  2704. return ref.referenceKind === 'calls' && JS_FAMILY.has(ref.language) &&
  2705. !/^(?:this|window)\./.test(ref.referenceName) &&
  2706. /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*){2,}$/.test(ref.referenceName);
  2707. }
  2708. const SELECTOR_NAMES = new WeakMap<ResolutionContext, Map<string, Set<string>>>();
  2709. /** A selector returns the named action from one identified store. Keep the
  2710. * lexical block identity so closures may capture it but sibling scopes and
  2711. * shadowing parameters/declarations cannot donate a binding. */
  2712. function matchSelectedStoreCall(ref: UnresolvedRef, context: ResolutionContext): ResolvedRef | null {
  2713. const source = context.readFile(ref.filePath);
  2714. if (!source?.includes('=>')) return null;
  2715. let files = SELECTOR_NAMES.get(context);
  2716. if (!files) { files = new Map(); SELECTOR_NAMES.set(context, files); }
  2717. let names = files.get(ref.filePath);
  2718. if (!names) {
  2719. names = new Set([...source.matchAll(/\bconst\s+([\w$]+)\s*=\s*[\w$]+\s*\(\s*(?:\(\s*[\w$]+\s*\)|[\w$]+)\s*=>/g)].map(m => m[1]!));
  2720. files.set(ref.filePath, names);
  2721. }
  2722. if (!names.has(ref.referenceName)) return null;
  2723. const name = ref.referenceName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  2724. const lines = source.split('\n');
  2725. const before = lines.slice(0, ref.line - 1).concat(lines[ref.line - 1]!.slice(0, ref.column)).join('\n');
  2726. const code = blankStringContents(stripCommentsForRegex(before, 'typescript'));
  2727. const binding = new RegExp(`\\bconst\\s+${name}\\s*=\\s*([\\w$]+)\\s*\\(\\s*(?:\\(\\s*([\\w$]+)\\s*\\)|([\\w$]+))\\s*=>\\s*([\\w$]+)\\.([\\w$]+)\\s*\\)`, 'g');
  2728. const stackAt = (end: number): number[] => {
  2729. const stack: number[] = [];
  2730. for (let i = 0; i < end; i++) {
  2731. if (code[i] === '{') stack.push(i);
  2732. else if (code[i] === '}') stack.pop();
  2733. }
  2734. return stack;
  2735. };
  2736. const callScope = stackAt(code.length);
  2737. for (const m of [...code.matchAll(binding)].reverse()) {
  2738. if ((m[2] ?? m[3]) !== m[4]) continue;
  2739. if (!stackAt(m.index!).every((pos, i) => callScope[i] === pos)) continue;
  2740. const rest = code.slice(m.index! + m[0].length);
  2741. if (new RegExp(`\\b(?:const|let|var|function|class)\\s+(?:${name}\\b|\\{[^}]*\\b${name}\\b)`).test(rest) ||
  2742. hasParameterBinding(rest, name)) return null;
  2743. return resolveStoreAction(`${m[1]}.getState`, m[5]!, ref, context, true);
  2744. }
  2745. return null;
  2746. }
  2747. /** Import resolution names the module binding; a nearer parameter or block
  2748. * declaration can shadow that binding at this particular call site. */
  2749. function importShadowedAt(name: string, ref: UnresolvedRef, context: ResolutionContext): boolean {
  2750. const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  2751. for (const fn of context.getNodesInFile(ref.filePath)) {
  2752. if ((fn.kind === 'function' || fn.kind === 'method') && fn.startLine <= ref.line && fn.endLine >= ref.line &&
  2753. fn.signature && hasParameterBinding(`${fn.signature} {`, escaped)) return true;
  2754. }
  2755. const lines = (context.readFile(ref.filePath) ?? '').split('\n');
  2756. const before = lines.slice(0, ref.line - 1).concat(lines[ref.line - 1]?.slice(0, ref.column) ?? '').join('\n');
  2757. const code = blankStringContents(stripCommentsForRegex(before, 'typescript'));
  2758. const stackAt = (end: number): number[] => {
  2759. const stack: number[] = [];
  2760. for (let i = 0; i < end; i++) {
  2761. if (code[i] === '{') stack.push(i);
  2762. else if (code[i] === '}') stack.pop();
  2763. }
  2764. return stack;
  2765. };
  2766. const scope = stackAt(code.length);
  2767. const declarations = new RegExp(`\\b(?:const|let|var|function|class)\\s+(?:${escaped}\\b|\\{[^}]*\\b${escaped}\\b)`, 'g');
  2768. return [...code.matchAll(declarations)].some(m => stackAt(m.index!).every((p, i) => scope[i] === p));
  2769. }
  2770. /** Balanced parameter lists also cover function-typed parameters, whose own
  2771. * parentheses must not make the outer shadow invisible. Conservative when a
  2772. * parameter's type mentions the same name: leave that call unresolved. */
  2773. function hasParameterBinding(code: string, escapedName: string): boolean {
  2774. const name = new RegExp(`\\b${escapedName}\\b`);
  2775. if (new RegExp(`\\b${escapedName}\\s*=>`).test(code)) return true;
  2776. for (let i = 0; i < code.length; i++) {
  2777. if (code[i] !== '(' || /\b(?:if|while|for|switch|with)\s*$/.test(code.slice(0, i))) continue;
  2778. let depth = 1, j = i + 1;
  2779. for (; j < code.length && depth; j++) {
  2780. if (code[j] === '(') depth++;
  2781. else if (code[j] === ')') depth--;
  2782. }
  2783. if (depth === 0 && name.test(code.slice(i + 1, j - 1)) &&
  2784. /^\s*(?::[^=;{]*)?(?:=>|\{)/.test(code.slice(j))) return true;
  2785. }
  2786. return false;
  2787. }
  2788. /**
  2789. * Split a camelCase or PascalCase string into words.
  2790. */
  2791. function splitCamelCase(str: string): string[] {
  2792. return str.replace(/([a-z])([A-Z])/g, '$1 $2')
  2793. .replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2')
  2794. .split(/[\s._:\/\\]+/)
  2795. .filter(w => w.length > 1);
  2796. }
  2797. /**
  2798. * Compute directory proximity from a pre-split list of directory segments
  2799. * (`filePath1` minus its filename) and a second file path.
  2800. * Returns a score based on the number of shared leading directory segments.
  2801. * Higher score = closer in directory tree.
  2802. *
  2803. * Split into a pre-split variant because findBestMatch scores every candidate
  2804. * against the SAME `ref.filePath`; re-splitting it per candidate was a hot spot
  2805. * on large repos (#915), so the caller splits it once and passes the segments.
  2806. */
  2807. function pathProximityFromDirs(dir1: string[], filePath2: string): number {
  2808. const dir2 = filePath2.split('/');
  2809. dir2.pop(); // drop filename — matches the original slice(0, -1) on both paths
  2810. let shared = 0;
  2811. const limit = Math.min(dir1.length, dir2.length);
  2812. for (let i = 0; i < limit; i++) {
  2813. if (dir1[i] === dir2[i]) {
  2814. shared++;
  2815. } else {
  2816. break;
  2817. }
  2818. }
  2819. // Each shared directory segment contributes 15 points, capped at 80
  2820. return Math.min(shared * 15, 80);
  2821. }
  2822. /**
  2823. * Compute directory proximity between two file paths.
  2824. * Returns a score based on the number of shared directory segments.
  2825. */
  2826. function computePathProximity(filePath1: string, filePath2: string): number {
  2827. const dir1 = filePath1.split('/');
  2828. dir1.pop();
  2829. return pathProximityFromDirs(dir1, filePath2);
  2830. }
  2831. /**
  2832. * Find the best matching node when there are multiple candidates
  2833. */
  2834. function findBestMatch(
  2835. ref: UnresolvedRef,
  2836. candidates: Node[],
  2837. _context: ResolutionContext
  2838. ): Node | null {
  2839. // Prioritization rules:
  2840. // 1. Same file > different file
  2841. // 2. Directory proximity (same module/package > different module)
  2842. // 3. Same language > different language
  2843. // 4. Functions/methods > classes/types (for call references)
  2844. // 5. Exported > non-exported
  2845. let bestScore = -1;
  2846. let bestNode: Node | null = null;
  2847. // Split the ref's path once (it's the same across every candidate) instead of
  2848. // re-splitting it inside computePathProximity per candidate (#915 hot spot).
  2849. const refDirs = ref.filePath.split('/');
  2850. refDirs.pop();
  2851. // A same-language candidate ALWAYS outscores a cross-language one: same-language
  2852. // scores at least +50 (language bonus), while a cross-language candidate maxes
  2853. // out at +35 (−80 language, +80 proximity, +25 kind, +10 exported; it can never
  2854. // be in the same file). So when any same-language candidate exists, skip the
  2855. // cross-language ones — provably the same winner, without paying the per-candidate
  2856. // scoring. Cuts the candidate set to same-language size on mixed front-end +
  2857. // back-end repos (#915). When ALL candidates are cross-language (a legitimate
  2858. // cross-language `calls` bridge), none are skipped and behavior is unchanged.
  2859. const hasSameLanguage = candidates.some((c) => c.language === ref.language);
  2860. for (const candidate of candidates) {
  2861. if (hasSameLanguage && candidate.language !== ref.language) continue;
  2862. let score = 0;
  2863. // Same file bonus
  2864. if (candidate.filePath === ref.filePath) {
  2865. score += 100;
  2866. }
  2867. // Directory proximity bonus — strongly prefer same module/package
  2868. score += pathProximityFromDirs(refDirs, candidate.filePath);
  2869. // Language matching: strongly prefer same language, penalize cross-language
  2870. if (candidate.language === ref.language) {
  2871. score += 50;
  2872. } else {
  2873. score -= 80;
  2874. }
  2875. // For call references, prefer functions/methods
  2876. if (ref.referenceKind === 'calls') {
  2877. if (candidate.kind === 'function' || candidate.kind === 'method') {
  2878. score += 25;
  2879. }
  2880. }
  2881. // For instantiation references (`new Foo()`), prefer class-like
  2882. // targets — without this, a function named `Foo` in another module
  2883. // could outscore the actual class.
  2884. if (ref.referenceKind === 'instantiates') {
  2885. if (
  2886. candidate.kind === 'class' ||
  2887. candidate.kind === 'struct' ||
  2888. candidate.kind === 'union' ||
  2889. candidate.kind === 'interface'
  2890. ) {
  2891. score += 25;
  2892. }
  2893. }
  2894. // For decorator references (`@Foo`), prefer functions. Class
  2895. // decorators (Python `@SomeClass`, Java annotation interfaces)
  2896. // also resolve here, hence the smaller class bonus.
  2897. if (ref.referenceKind === 'decorates') {
  2898. if (candidate.kind === 'function' || candidate.kind === 'method') {
  2899. score += 25;
  2900. } else if (candidate.kind === 'class' || candidate.kind === 'interface') {
  2901. score += 15;
  2902. }
  2903. }
  2904. // Exported bonus
  2905. if (candidate.isExported) {
  2906. score += 10;
  2907. }
  2908. // Closer line number (within same file)
  2909. if (candidate.filePath === ref.filePath && candidate.startLine) {
  2910. const distance = Math.abs(candidate.startLine - ref.line);
  2911. score += Math.max(0, 20 - distance / 10);
  2912. }
  2913. if (score > bestScore) {
  2914. bestScore = score;
  2915. bestNode = candidate;
  2916. }
  2917. }
  2918. return bestNode;
  2919. }
  2920. /**
  2921. * Fuzzy match - last resort with lower confidence
  2922. */
  2923. export function matchFuzzy(
  2924. ref: UnresolvedRef,
  2925. context: ResolutionContext
  2926. ): ResolvedRef | null {
  2927. const lowerName = ref.referenceName.toLowerCase();
  2928. // Use pre-built lowercase index for O(1) lookup instead of scanning all nodes
  2929. const candidates = context.getNodesByLowerName(lowerName);
  2930. // Filter to callable kinds only (function, method, class)
  2931. const callableKinds = new Set(['function', 'method', 'class']);
  2932. const callableCandidates = applyLanguageGate(
  2933. candidates.filter((n) => callableKinds.has(n.kind)),
  2934. ref
  2935. );
  2936. // Prefer same-language matches
  2937. const sameLanguageCandidates = callableCandidates.filter(n => n.language === ref.language);
  2938. const finalCandidates = sameLanguageCandidates.length > 0 ? sameLanguageCandidates : callableCandidates;
  2939. // Both post-pipeline visibility guards (#1745 language-local + #1719 sealed
  2940. // module). The sealed-module test rejects the survivor and never filters the
  2941. // set that produced it: removing a sealed candidate from a crowd would leave
  2942. // a lone one and manufacture a 0.5 guess out of an ambiguity fuzzy declines.
  2943. // Also decline a bare JS/TS call whose only survivor is a method or a
  2944. // cross-file name the file already binds locally (#1714).
  2945. // A function nested inside another function is only callable from inside
  2946. // its container (#1230), so a builtin method call (`res.text()`) whose only
  2947. // same-named project symbol is some file's closure must decline (#1708).
  2948. // The check sits on the ONE candidate this strategy would commit to, not on
  2949. // the candidate set: filtering the unreachable ones out of a crowd would
  2950. // leave a single survivor and hand it every call of that name — on vite,
  2951. // `import { resolve } from 'node:path'` in a dozen playground configs onto
  2952. // the one reachable `resolve` method (#1709). Reachability may reject a
  2953. // unique guess; it must never manufacture one.
  2954. if (
  2955. finalCandidates.length === 1 &&
  2956. isVisibleAcrossFiles(finalCandidates[0]!, ref, context) &&
  2957. isCrossFileReachable(finalCandidates[0]!, ref, context) &&
  2958. !(isBareJsCall(ref, context) &&
  2959. (finalCandidates[0]!.kind === 'method' ||
  2960. (finalCandidates[0]!.filePath !== ref.filePath && isLocallyBoundJsName(ref.referenceName, ref.filePath, context)))) &&
  2961. isLexicallyReachable(finalCandidates[0]!, ref, context)
  2962. ) {
  2963. const isCrossLanguage = finalCandidates[0]!.language !== ref.language;
  2964. return {
  2965. original: ref,
  2966. targetNodeId: finalCandidates[0]!.id,
  2967. confidence: isCrossLanguage ? 0.3 : 0.5,
  2968. resolvedBy: 'fuzzy',
  2969. };
  2970. }
  2971. return null;
  2972. }
  2973. /**
  2974. * Match all strategies in order of confidence
  2975. */
  2976. /** ArkUI attribute-helper decorators a `.attr(...)` chain may resolve to. */
  2977. const ARKUI_ATTRIBUTE_DECORATORS = new Set(['Extend', 'Styles', 'AnimatableExtend', 'Builder']);
  2978. /**
  2979. * CODEGRAPH_RESOLVE_PROFILE=2 sub-stage attribution for matchReference's
  2980. * strategy pipeline (`nm:<stage>|<refKind>|hit/miss`). Module-global because
  2981. * the matcher is a free function; each thread (main + every pool worker) has
  2982. * its own module instance, and dumpNameMatcherProfile is invoked from
  2983. * ReferenceResolver.dumpResolveProfile so worker tables surface too.
  2984. */
  2985. const NM_PROFILE: Map<string, { n: number; ns: bigint }> | null =
  2986. process.env.CODEGRAPH_RESOLVE_PROFILE === '2' ? new Map() : null;
  2987. function nmTimedT<T>(stage: string, ref: UnresolvedRef, fn: () => T): T {
  2988. if (!NM_PROFILE) return fn();
  2989. const t0 = process.hrtime.bigint();
  2990. const r = fn();
  2991. const dt = process.hrtime.bigint() - t0;
  2992. const key = `nm:${stage}|${ref.referenceKind}|${r ? 'hit' : 'miss'}`;
  2993. const slot = NM_PROFILE.get(key);
  2994. if (slot) {
  2995. slot.n++;
  2996. slot.ns += dt;
  2997. } else {
  2998. NM_PROFILE.set(key, { n: 1, ns: dt });
  2999. }
  3000. return r;
  3001. }
  3002. function nmTimed(stage: string, ref: UnresolvedRef, fn: () => ResolvedRef | null): ResolvedRef | null {
  3003. return nmTimedT(stage, ref, fn);
  3004. }
  3005. /** Dump this thread's matchReference sub-stage table to stderr (no-op unless =2). */
  3006. export function dumpNameMatcherProfile(label: string): void {
  3007. if (!NM_PROFILE || NM_PROFILE.size === 0) return;
  3008. const rows = [...NM_PROFILE.entries()]
  3009. .map(([k, v]) => ({ k, n: v.n, ms: Number(v.ns / 1_000_000n) }))
  3010. .sort((a, b) => b.ms - a.ms);
  3011. for (const r of rows) {
  3012. console.error(
  3013. `[resolve-profile] ${label} ${r.k}: n=${r.n} total=${(r.ms / 1000).toFixed(1)}s avg=${((r.ms * 1000) / Math.max(1, r.n)).toFixed(0)}µs`
  3014. );
  3015. }
  3016. }
  3017. export function matchReference(
  3018. ref: UnresolvedRef,
  3019. context: ResolutionContext
  3020. ): ResolvedRef | null {
  3021. // Function-as-value refs (#756) resolve ONLY through the dedicated matcher —
  3022. // never the fuzzy/qualified fallthrough below (a wrong callback edge is
  3023. // worse than none).
  3024. if (ref.referenceKind === 'function_ref') {
  3025. return matchFunctionRef(ref, context);
  3026. }
  3027. // ArkTS chained UI attributes — emitted with a leading dot (`.titleStyle`,
  3028. // `.width`) by the extractor — resolve ONLY to decorator-marked attribute
  3029. // helpers: `@Extend`/`@Styles`/`@AnimatableExtend` functions (and global
  3030. // `@Builder`s used attribute-position). Framework attributes (`.width`,
  3031. // `.fontSize` — on nearly every UI line) match no such helper and stay
  3032. // unresolved, NEVER falling through to bare-name matching: on a samples
  3033. // monorepo that fallthrough manufactured 36k wrong edges, giving single
  3034. // same-named properties thousands of false callers. Ambiguity rule matches
  3035. // the rest of the file: several same-named helpers → prefer the call-site
  3036. // file, still ambiguous → drop the ref rather than guess.
  3037. if (ref.language === 'arkts' && ref.referenceName.startsWith('.')) {
  3038. const base = ref.referenceName.slice(1);
  3039. const candidates = context
  3040. .getNodesByName(base)
  3041. .filter(
  3042. (n) =>
  3043. n.language === 'arkts' &&
  3044. n.kind === 'function' &&
  3045. (n.decorators ?? []).some((d) => ARKUI_ATTRIBUTE_DECORATORS.has(d))
  3046. );
  3047. const chosen =
  3048. candidates.length > 1 ? preferCallSiteFile(candidates, ref.filePath) : candidates;
  3049. if (chosen.length !== 1) return null;
  3050. return {
  3051. original: ref,
  3052. targetNodeId: chosen[0]!.id,
  3053. confidence: 0.85,
  3054. resolvedBy: 'exact-match',
  3055. };
  3056. }
  3057. // Erlang `-behaviour(m)` refs target a MODULE. Letting them fall through to
  3058. // bare-name matching grabs any same-named symbol — on emqx,
  3059. // `-behaviour(supervisor)` resolved to a `-define(supervisor, …)` macro
  3060. // constant in an unrelated app. Resolve only to the behaviour module's
  3061. // namespace; an out-of-repo behaviour (OTP's gen_server/supervisor) stays
  3062. // unresolved rather than guessed. The same module-only rule applies to every
  3063. // ref an `.app`/`.app.src` resource file emits — its `{mod, …}` callback and
  3064. // `{applications, …}` dependency names can only mean modules, and on emqx
  3065. // the `ssl` OTP app otherwise resolved to a test helper FUNCTION named ssl.
  3066. if (
  3067. ref.language === 'erlang' &&
  3068. (ref.referenceKind === 'implements' || /\.app(?:\.src)?$/i.test(ref.filePath))
  3069. ) {
  3070. const modules = context
  3071. .getNodesByName(ref.referenceName)
  3072. .filter((n) => n.language === 'erlang' && n.kind === 'namespace');
  3073. const chosen = preferCallSiteFile(modules, ref.filePath)[0];
  3074. if (!chosen) return null;
  3075. return {
  3076. original: ref,
  3077. targetNodeId: chosen.id,
  3078. confidence: 0.9,
  3079. resolvedBy: 'exact-match',
  3080. };
  3081. }
  3082. // Erlang call/fun refs carry the call-site arity (`f/1` — #1610) because
  3083. // arity is part of the function's identity and every erlang function's
  3084. // qualifiedName carries it (`mod::f/1`). Resolve ONLY to a definition of
  3085. // that exact arity: the call site's own file first (a local call targets its
  3086. // own module by language semantics; `-import`ed functions ride the
  3087. // cross-file branch), and when no definition of that arity exists anywhere,
  3088. // resolve to NOTHING rather than a sibling arity — the real target may be
  3089. // macro-generated or out of repo, and a wrong-arity edge is worse than none.
  3090. if (
  3091. ref.language === 'erlang' &&
  3092. !ref.referenceName.includes('::') &&
  3093. (ref.referenceKind === 'calls' || ref.referenceKind === 'references')
  3094. ) {
  3095. const am = /^(.+)\/(\d{1,3})$/.exec(ref.referenceName);
  3096. if (am) {
  3097. // endsWith is length-anchored, so `/1` cannot match `…/11`.
  3098. const arityTail = `/${am[2]}`;
  3099. const candidates = context
  3100. .getNodesByName(am[1]!)
  3101. .filter(
  3102. (n) =>
  3103. n.language === 'erlang' && n.kind === 'function' && n.qualifiedName.endsWith(arityTail),
  3104. );
  3105. if (candidates.length > 0) {
  3106. const sameFile = candidates.find((n) => n.filePath === ref.filePath);
  3107. if (sameFile) {
  3108. return { original: ref, targetNodeId: sameFile.id, confidence: 0.95, resolvedBy: 'exact-match' };
  3109. }
  3110. if (candidates.length === 1) {
  3111. return { original: ref, targetNodeId: candidates[0]!.id, confidence: 0.8, resolvedBy: 'exact-match' };
  3112. }
  3113. const best = findBestMatch(ref, candidates, context);
  3114. if (best) {
  3115. const proximity = computePathProximity(ref.filePath, best.filePath);
  3116. return {
  3117. original: ref,
  3118. targetNodeId: best.id,
  3119. confidence: proximity >= 30 ? 0.7 : 0.4,
  3120. resolvedBy: 'exact-match',
  3121. };
  3122. }
  3123. }
  3124. return null;
  3125. }
  3126. }
  3127. if (isUnresolvedJsMemberCall(ref)) return null;
  3128. // Try strategies in order of confidence
  3129. let result: ResolvedRef | null;
  3130. // 0. File path match (e.g., "snippets/drawer-menu.liquid" → file node)
  3131. result = nmTimed('filePath', ref, () => matchByFilePath(ref, context));
  3132. if (result) return result;
  3133. // 1. Qualified name match (highest confidence)
  3134. result = nmTimed('qualifiedName', ref, () => matchByQualifiedName(ref, context));
  3135. if (result) return result;
  3136. // 1b. C++ chained call whose receiver is another call — `Foo::instance().bar()`
  3137. // encoded as `Foo::instance().bar` by the extractor (#645). Resolve the
  3138. // receiver's type from what the inner call returns, then the method on it.
  3139. if (ref.language === 'cpp' || ref.language === 'c') {
  3140. result = nmTimed('cppChain', ref, () => matchCppCallChain(ref, context));
  3141. if (result) return result;
  3142. }
  3143. // 1c. `::`-scoped factory chain — PHP `Cls::for($x)->method()` (#608) or Rust
  3144. // `Foo::new().bar()`, both encoded as `Cls::factory().method`. The receiver's
  3145. // type is the factory's `self` (PHP `: self`/`: static`, Rust `-> Self`) or
  3146. // concrete return type.
  3147. if (ref.language === 'php' || ref.language === 'rust') {
  3148. result = nmTimed('scopedChain', ref, () => matchScopedCallChain(ref, context));
  3149. if (result) return result;
  3150. }
  3151. // 1d. Dotted chained static-factory / fluent call (Java / Kotlin / C# / Swift /
  3152. // Go / Scala / Dart / Objective-C) — `Foo.getInstance().bar()` encoded as
  3153. // `Foo.getInstance().bar`, Go's bare-factory `New().Method()` as `New().Method`,
  3154. // Scala's companion factory, Dart's static factory / factory-constructor, or
  3155. // ObjC's chained message send `[[Foo create] doIt]` encoded as `Foo.create().doIt`
  3156. // (#645/#608 mechanism). Resolve the method's class from the inner call's
  3157. // declared return type, then validate it.
  3158. if (
  3159. ref.language === 'java' ||
  3160. ref.language === 'kotlin' ||
  3161. ref.language === 'csharp' ||
  3162. ref.language === 'swift' ||
  3163. ref.language === 'go' ||
  3164. ref.language === 'scala' ||
  3165. ref.language === 'dart' ||
  3166. ref.language === 'objc' ||
  3167. ref.language === 'pascal'
  3168. ) {
  3169. result = nmTimed('dottedChain', ref, () => matchDottedCallChain(ref, context));
  3170. if (result) return result;
  3171. }
  3172. // A call-receiver chain the extractor encoded as `<inner>().<method>` for a
  3173. // language with no chain resolver above (TS/JS, Python — #1683) is a
  3174. // receiver whose type is unknown. Nothing below may guess for it: the
  3175. // method-call pattern rejects the parens, exact name never matches, but the
  3176. // fuzzy strategy splits on `.` and would hand `make().run` to any `run` —
  3177. // the fabricated edge the encoding exists to prevent.
  3178. if (
  3179. ref.referenceName.includes('().') &&
  3180. (ref.language === 'typescript' || ref.language === 'javascript' || ref.language === 'tsx' || ref.language === 'jsx' || ref.language === 'python')
  3181. ) {
  3182. return nmTimed('storeAccessorChain', ref, () => matchStoreAccessorChain(ref, context));
  3183. }
  3184. // 2. Method call pattern
  3185. result = nmTimed('methodCall', ref, () => matchMethodCall(ref, context));
  3186. if (result) return result;
  3187. // 3. Exact name match
  3188. result = nmTimed('exactName', ref, () => matchByExactName(ref, context));
  3189. if (result) return result;
  3190. // 4. Fuzzy match (lowest confidence)
  3191. result = nmTimed('fuzzy', ref, () => matchFuzzy(ref, context));
  3192. if (result) return result;
  3193. return null;
  3194. }