function-ref.ts 36 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862
  1. /**
  2. * Function-as-value capture (#756) — registration-linking for callbacks.
  3. *
  4. * A function name used as a VALUE — passed as a call argument
  5. * (`register_handler(target_cb)`, `signal(SIGINT, handler)`), assigned to a
  6. * field or function pointer (`o->cb = target_cb`, `OnFire := TargetCb`),
  7. * placed in a struct/object initializer (`{ .recv_cb = my_cb }`,
  8. * `{ recv: targetCb }`, `Ops{Cb: targetCb}`), or listed in a function table
  9. * (`static cb_t table[] = { cb_a, cb_b }`) — is a real dependency that static
  10. * call extraction misses entirely: `callers(target_cb)` showed nothing but
  11. * direct calls, so every callback looked dead and its registration sites were
  12. * invisible to impact analysis.
  13. *
  14. * This module captures those value positions during the AST walk as
  15. * `function_ref` candidates. Capture is table-driven per language (the value
  16. * positions and wrapper forms differ per grammar — `&fn` in C, `Main::fn` in
  17. * Java, `::fn` in Kotlin, `#selector(fn)` in Swift, `@TargetCb` in Pascal,
  18. * `method(:fn)` in Ruby). Candidates are GATED at end-of-file extraction
  19. * (see `TreeSitterExtractor.flushFnRefCandidates`): only names matching a
  20. * same-file function/method or an imported binding survive, which bounds
  21. * volume and keeps precision high. Resolution then matches survivors against
  22. * function/method nodes ONLY (`matchFunctionRef` in
  23. * `src/resolution/name-matcher.ts`) and persists them as `references` edges,
  24. * which `callers`/`impact` already traverse.
  25. *
  26. * Deliberately NOT covered (resolving the *dispatch* — `o->cb(x)` → the
  27. * registered function — needs data-flow through struct fields; a wrong edge
  28. * is worse than none): indirect-call resolution and `obj.method` member
  29. * values where `obj` isn't `this`/`self` (the receiver's type is statically
  30. * unknowable without local data-flow).
  31. */
  32. import type { Node as SyntaxNode } from 'web-tree-sitter';
  33. import { getNodeText, getChildByField } from './tree-sitter-helpers';
  34. export interface FnRefCandidate {
  35. name: string;
  36. line: number;
  37. column: number;
  38. /** Which capture position produced this candidate (gate policy keys on it). */
  39. mode: CaptureMode;
  40. /**
  41. * True when the value was an explicit reference form (`&fn`, `&Cls::m`,
  42. * `::fn`, `#selector`, `method(:sym)`) rather than a bare identifier —
  43. * C++'s flush policy keys on it.
  44. */
  45. explicitRef: boolean;
  46. /**
  47. * Skip the same-file/import name gate for this candidate. Set for PHP
  48. * string callables in known HOF positions: PHP global functions are
  49. * referenced cross-file WITHOUT imports (global namespace), so the gate
  50. * can't see them — the strong positional prior (a string argument to
  51. * `usort`/`array_map`/…) plus resolution's unique-or-drop rule carry the
  52. * precision instead.
  53. */
  54. skipGate?: boolean;
  55. }
  56. /** How to pull candidate value nodes out of a dispatched container node. */
  57. type CaptureMode =
  58. | 'args' // every named child is a potential value (call argument lists)
  59. | 'rhs' // the assignment right-hand side (named field, else last named child)
  60. | 'value' // the `value` field of a keyed pair (object/struct/table initializers)
  61. | 'list' // every named child (array / initializer-list / table positional elements)
  62. | 'varinit'; // a variable declarator's initializer value
  63. interface CaptureRule {
  64. mode: CaptureMode;
  65. /** Field holding the value for rhs/value/varinit (defaults per mode). */
  66. field?: string;
  67. }
  68. export interface FnRefSpec {
  69. /** Bare identifier node types that can act as a function value. */
  70. idTypes: Set<string>;
  71. /** Container node type → how to extract candidate values from it. */
  72. dispatch: Map<string, CaptureRule>;
  73. /**
  74. * Transparent wrapper layers between a container and its values
  75. * (`argument`, `value_argument`, `literal_element`, `expression_list`…).
  76. * Value: the field to descend into, or null for "named children".
  77. * `expression_list` fans out to ALL named children (Go multi-assign).
  78. */
  79. layers?: Map<string, string | null>;
  80. /**
  81. * Unary wrappers whose operand is the function value — C/C++ `&fn`
  82. * (pointer_expression), Pascal `@Fn` (exprUnary), Scala eta `fn _`
  83. * (postfix_expression). Value: operand field, or null for first named child.
  84. */
  85. unwrap?: Map<string, string | null>;
  86. /**
  87. * Whole-node reference forms needing bespoke name extraction —
  88. * `method_reference` (Java), `callable_reference` / `navigation_expression`
  89. * (Kotlin), `selector_expression` (Swift `#selector` / ObjC `@selector`),
  90. * Ruby `method(:sym)` calls, and `this.method` member forms.
  91. */
  92. special?: Set<string>;
  93. /**
  94. * Capture modes whose candidates skip the same-file/import gate and rely on
  95. * resolution's unique-or-drop rule instead. C-family only: an initializer
  96. * value, function-pointer assignment RHS, or table element is a
  97. * function-pointer position by construction, and C has no symbol imports —
  98. * the dominant repo-scale pattern (`server.c`'s command table naming
  99. * handlers defined across files) would otherwise be invisible. Call
  100. * arguments stay gated everywhere (locals passed as args dwarf callbacks).
  101. */
  102. ungatedModes?: Set<CaptureMode>;
  103. /**
  104. * C++ only: in args/rhs/varinit positions, accept ONLY explicit reference
  105. * forms (`&fn`, `&Cls::method`) — never bare identifiers. C++ codebases are
  106. * dense with generic free-function/accessor names (`begin`, `end`, `out`,
  107. * `size`, `data`) that collide with parameters and locals, and out-of-line
  108. * member definitions extract as function-kind nodes — bare-id matching on
  109. * fmt was mostly wrong edges. File-scope initializer tables (value/list)
  110. * still accept bare identifiers, same as C.
  111. */
  112. addressOfOnly?: boolean;
  113. }
  114. /** Names that are never function references even when grammars call them identifiers. */
  115. const NAME_STOPLIST = new Set([
  116. 'this',
  117. 'self',
  118. 'super',
  119. 'null',
  120. 'nil',
  121. 'true',
  122. 'false',
  123. 'undefined',
  124. 'new',
  125. 'NULL',
  126. 'nullptr',
  127. 'None',
  128. ]);
  129. // ---------------------------------------------------------------------------
  130. // Per-language specs. Node types verified against each grammar (probe fixtures
  131. // in the #756 investigation; see docs/design/function-ref-capture.md).
  132. // ---------------------------------------------------------------------------
  133. /** C / C++ / Objective-C share the C-family initializer & assignment shapes. */
  134. function cFamilySpec(extra?: { special?: string[]; addressOfOnly?: boolean }): FnRefSpec {
  135. return {
  136. idTypes: new Set(['identifier']),
  137. dispatch: new Map<string, CaptureRule>([
  138. ['argument_list', { mode: 'args' }],
  139. ['assignment_expression', { mode: 'rhs', field: 'right' }],
  140. ['init_declarator', { mode: 'varinit', field: 'value' }],
  141. ['initializer_list', { mode: 'list' }],
  142. ['initializer_pair', { mode: 'value', field: 'value' }],
  143. ]),
  144. unwrap: new Map([['pointer_expression', 'argument']]),
  145. special: new Set(extra?.special ?? []),
  146. // C has no symbol imports, and callbacks are registered cross-file at repo
  147. // scale (redis: server.c's command table names handlers from t_*.c) — so
  148. // initializer positions bypass the gate and lean on resolution's
  149. // unique-or-drop rule. ONLY 'value'/'list' (struct/array initializers),
  150. // and the flush additionally requires FILE scope: a C file-scope
  151. // initializer is a constant-expression context, so a bare identifier
  152. // there can only be a function address (or enum/macro, which the
  153. // function-kind filter drops) — never a variable. 'rhs'/'varinit' were
  154. // tried and produced false edges (`prev = next`, `*str = field` — data
  155. // assignments matching a unique same-named function elsewhere), so
  156. // assignments stay gated to same-file/import.
  157. ungatedModes: new Set<CaptureMode>(['value', 'list']),
  158. addressOfOnly: extra?.addressOfOnly,
  159. };
  160. }
  161. // `this.handleClick` capture (member_expression) emits a `this.`-PREFIXED
  162. // candidate name: resolution scopes it to the enclosing symbol's class
  163. // (qualified-name prefix), so `this.fonts` (a property, post-#808) and
  164. // inherited/unknown members yield no edge, while same-class methods —
  165. // `btn.on('click', this.handleClick)`, the observer-registration idiom —
  166. // resolve precisely. Bare identifiers stay function-kind-only (a bare id can
  167. // never be a method value in JS).
  168. const TS_JS_SPEC: FnRefSpec = {
  169. // `shorthand_property_identifier`: `{ handleSubmit }` — the object a hook
  170. // returns its handlers in, and a namespace object's members.
  171. idTypes: new Set(['identifier', 'shorthand_property_identifier']),
  172. dispatch: new Map<string, CaptureRule>([
  173. ['arguments', { mode: 'args' }],
  174. ['assignment_expression', { mode: 'rhs', field: 'right' }],
  175. ['variable_declarator', { mode: 'varinit', field: 'value' }],
  176. ['pair', { mode: 'value', field: 'value' }],
  177. ['array', { mode: 'list' }],
  178. // A JSX attribute value or child: `onPress={handleSubmit}`, `renderItem={renderRow}`,
  179. // `<Route component={Home}/>`. The expression's one named child is the value; a
  180. // spread or a call normalizes to nothing. This is THE handler-binding idiom of
  181. // React, and without it a tap's handler had no edge from the component that
  182. // renders it — the Screens and Steps views could not see what a tap does.
  183. ['jsx_expression', { mode: 'list' }],
  184. // An object literal's shorthand members — `return { handleApprove,
  185. // handleRetake }` from a hook, `const Api = { upload, createFolder }`.
  186. // Every named child is offered; only a shorthand identifier normalizes
  187. // (a `pair` is its own container above, a spread or a method is nothing).
  188. ['object', { mode: 'list' }],
  189. ]),
  190. special: new Set(['member_expression']),
  191. };
  192. const PYTHON_SPEC: FnRefSpec = {
  193. idTypes: new Set(['identifier']),
  194. dispatch: new Map<string, CaptureRule>([
  195. ['argument_list', { mode: 'args' }],
  196. ['assignment', { mode: 'rhs', field: 'right' }],
  197. ['keyword_argument', { mode: 'value', field: 'value' }], // Thread(target=worker)
  198. ['pair', { mode: 'value', field: 'value' }],
  199. ['list', { mode: 'list' }],
  200. // `return SomeClass` / `return handler` — factory returns are how DRF
  201. // wires views to serializers (get_serializer_class) and how Python
  202. // factories hand back callables (#1478). A single returned expression is
  203. // a direct named child, so 'list' covers it; tuple returns (`return A, B`)
  204. // sit under an expression_list child and are deliberately not descended.
  205. ['return_statement', { mode: 'list' }],
  206. ]),
  207. special: new Set(['attribute']),
  208. };
  209. const GO_SPEC: FnRefSpec = {
  210. idTypes: new Set(['identifier']),
  211. dispatch: new Map<string, CaptureRule>([
  212. ['argument_list', { mode: 'args' }],
  213. ['assignment_statement', { mode: 'rhs', field: 'right' }],
  214. ['short_var_declaration', { mode: 'rhs', field: 'right' }],
  215. ['var_spec', { mode: 'varinit', field: 'value' }],
  216. ['keyed_element', { mode: 'value' }], // value = last literal_element child
  217. ['literal_value', { mode: 'list' }], // positional composite literals
  218. ]),
  219. layers: new Map<string, string | null>([
  220. ['literal_element', null],
  221. ['expression_list', null],
  222. ]),
  223. };
  224. const RUST_SPEC: FnRefSpec = {
  225. idTypes: new Set(['identifier']),
  226. dispatch: new Map<string, CaptureRule>([
  227. ['arguments', { mode: 'args' }],
  228. ['assignment_expression', { mode: 'rhs', field: 'right' }],
  229. ['field_initializer', { mode: 'value', field: 'value' }],
  230. ['array_expression', { mode: 'list' }],
  231. ['static_item', { mode: 'varinit', field: 'value' }],
  232. ['let_declaration', { mode: 'varinit', field: 'value' }],
  233. ]),
  234. };
  235. const JAVA_SPEC: FnRefSpec = {
  236. // No bare-identifier function values in Java — only method references.
  237. idTypes: new Set<string>(),
  238. dispatch: new Map<string, CaptureRule>([
  239. ['argument_list', { mode: 'args' }],
  240. ['assignment_expression', { mode: 'rhs', field: 'right' }],
  241. ['variable_declarator', { mode: 'varinit', field: 'value' }],
  242. ]),
  243. special: new Set(['method_reference']),
  244. };
  245. const KOTLIN_SPEC: FnRefSpec = {
  246. idTypes: new Set<string>(),
  247. dispatch: new Map<string, CaptureRule>([
  248. ['value_arguments', { mode: 'args' }],
  249. ['assignment', { mode: 'rhs' }], // RHS = last named child (no field in grammar)
  250. ]),
  251. layers: new Map<string, string | null>([['value_argument', null]]),
  252. special: new Set(['callable_reference', 'navigation_expression']),
  253. };
  254. const CSHARP_SPEC: FnRefSpec = {
  255. idTypes: new Set(['identifier']),
  256. dispatch: new Map<string, CaptureRule>([
  257. ['argument_list', { mode: 'args' }],
  258. ['assignment_expression', { mode: 'rhs', field: 'right' }], // covers `+=` event subscription
  259. ['initializer_expression', { mode: 'list' }],
  260. ['variable_declarator', { mode: 'varinit' }],
  261. ]),
  262. layers: new Map<string, string | null>([['argument', null]]),
  263. special: new Set(['member_access_expression']),
  264. };
  265. const RUBY_SPEC: FnRefSpec = {
  266. // Bare identifiers in Ruby args are method CALLS or locals, never function
  267. // values — only the `method(:name)` idiom (and `&method(:name)`) plus
  268. // hook-DSL symbols (`before_action :authenticate`) qualify.
  269. idTypes: new Set<string>(),
  270. dispatch: new Map<string, CaptureRule>([
  271. ['argument_list', { mode: 'args' }],
  272. ['pair', { mode: 'value', field: 'value' }],
  273. ]),
  274. layers: new Map<string, string | null>([['block_argument', null]]),
  275. special: new Set(['call', 'simple_symbol']),
  276. };
  277. /**
  278. * Rails/ActiveSupport-style hook DSLs whose symbol arguments name a method of
  279. * the enclosing class: lifecycle callbacks (`before_action`, `after_save`,
  280. * `around_create`, `skip_before_action`…), `validate :method`, `set_callback`,
  281. * `helper_method`, and `rescue_from(..., with: :handler)`. NOT `validates`
  282. * (plural) — its symbols name ATTRIBUTES, not methods.
  283. */
  284. const RUBY_HOOK_RE = /^(skip_)?(before|after|around)_[a-z_]+$/;
  285. const RUBY_HOOK_NAMES = new Set(['validate', 'set_callback', 'helper_method', 'rescue_from']);
  286. function isRubyHookCall(name: string): boolean {
  287. return RUBY_HOOK_RE.test(name) || RUBY_HOOK_NAMES.has(name);
  288. }
  289. const SWIFT_SPEC: FnRefSpec = {
  290. idTypes: new Set(['simple_identifier']),
  291. dispatch: new Map<string, CaptureRule>([
  292. ['value_arguments', { mode: 'args' }],
  293. ['assignment', { mode: 'rhs', field: 'result' }],
  294. ['array_literal', { mode: 'list' }],
  295. ['property_declaration', { mode: 'varinit', field: 'value' }],
  296. ]),
  297. layers: new Map<string, string | null>([['value_argument', 'value']]),
  298. special: new Set(['selector_expression']),
  299. };
  300. const SCALA_SPEC: FnRefSpec = {
  301. idTypes: new Set(['identifier']),
  302. dispatch: new Map<string, CaptureRule>([
  303. ['arguments', { mode: 'args' }],
  304. ['assignment_expression', { mode: 'rhs', field: 'right' }],
  305. ['val_definition', { mode: 'varinit', field: 'value' }],
  306. ]),
  307. unwrap: new Map<string, string | null>([['postfix_expression', null]]), // eta-expansion `fn _`
  308. };
  309. const DART_SPEC: FnRefSpec = {
  310. idTypes: new Set(['identifier']),
  311. dispatch: new Map<string, CaptureRule>([
  312. ['arguments', { mode: 'args' }],
  313. ['assignment_expression', { mode: 'rhs', field: 'right' }],
  314. ['pair', { mode: 'value', field: 'value' }],
  315. ['list_literal', { mode: 'list' }],
  316. ['static_final_declaration', { mode: 'varinit' }],
  317. ]),
  318. layers: new Map<string, string | null>([['argument', null]]),
  319. };
  320. const LUA_SPEC: FnRefSpec = {
  321. idTypes: new Set(['identifier']),
  322. dispatch: new Map<string, CaptureRule>([
  323. ['arguments', { mode: 'args' }],
  324. ['assignment_statement', { mode: 'rhs' }], // RHS expression_list children carry `value` fields
  325. ['field', { mode: 'value', field: 'value' }], // table fields, keyed AND positional
  326. ]),
  327. layers: new Map<string, string | null>([['expression_list', null]]),
  328. };
  329. const PASCAL_SPEC: FnRefSpec = {
  330. idTypes: new Set(['identifier']),
  331. dispatch: new Map<string, CaptureRule>([
  332. ['exprArgs', { mode: 'args' }],
  333. ['assignment', { mode: 'rhs', field: 'rhs' }], // OnClick := Handler
  334. ]),
  335. unwrap: new Map<string, string | null>([['exprUnary', 'operand']]), // @Handler
  336. };
  337. /**
  338. * PHP core functions whose string arguments are CALLABLES — the positional
  339. * prior that makes a bare string trustworthy as a function reference.
  340. * Deliberately core-PHP only; framework registries (WordPress `add_action`)
  341. * belong in a frameworks/ resolver if ever added.
  342. */
  343. const PHP_CALLABLE_HOFS = new Set([
  344. 'array_map', 'array_filter', 'array_walk', 'array_walk_recursive', 'array_reduce',
  345. 'usort', 'uasort', 'uksort',
  346. 'array_udiff', 'array_udiff_assoc', 'array_uintersect', 'array_uintersect_assoc',
  347. 'call_user_func', 'call_user_func_array',
  348. 'forward_static_call', 'forward_static_call_array',
  349. 'preg_replace_callback', 'preg_replace_callback_array',
  350. 'register_shutdown_function', 'register_tick_function',
  351. 'set_error_handler', 'set_exception_handler', 'spl_autoload_register',
  352. 'ob_start', 'iterator_apply', 'header_register_callback',
  353. 'is_callable',
  354. ]);
  355. const PHP_SPEC: FnRefSpec = {
  356. // PHP has no bare-identifier function values (the first-class callable
  357. // `fn(...)` already extracts as a `calls` edge). What qualifies:
  358. // - a string argument to a known callable-taking core function
  359. // (`usort($a, 'cmp_items')`) — see PHP_CALLABLE_HOFS
  360. // - array callables: `[$this, 'method']` (class-scoped) and
  361. // `[Foo::class, 'method']` (qualified), in any call's arguments
  362. idTypes: new Set<string>(),
  363. dispatch: new Map<string, CaptureRule>([['arguments', { mode: 'args' }]]),
  364. layers: new Map<string, string | null>([['argument', null]]),
  365. special: new Set(['encapsed_string', 'string', 'array_creation_expression']),
  366. };
  367. /**
  368. * Capture specs by language.
  369. */
  370. export const FN_REF_SPECS: Record<string, FnRefSpec | undefined> = {
  371. c: cFamilySpec(),
  372. cpp: cFamilySpec({ addressOfOnly: true }),
  373. objc: cFamilySpec({ special: ['selector_expression'] }),
  374. typescript: TS_JS_SPEC,
  375. tsx: TS_JS_SPEC,
  376. javascript: TS_JS_SPEC,
  377. jsx: TS_JS_SPEC,
  378. python: PYTHON_SPEC,
  379. go: GO_SPEC,
  380. rust: RUST_SPEC,
  381. java: JAVA_SPEC,
  382. kotlin: KOTLIN_SPEC,
  383. csharp: CSHARP_SPEC,
  384. php: PHP_SPEC,
  385. ruby: RUBY_SPEC,
  386. swift: SWIFT_SPEC,
  387. scala: SCALA_SPEC,
  388. dart: DART_SPEC,
  389. lua: LUA_SPEC,
  390. luau: LUA_SPEC,
  391. pascal: PASCAL_SPEC,
  392. };
  393. // ---------------------------------------------------------------------------
  394. // Capture
  395. // ---------------------------------------------------------------------------
  396. /**
  397. * Extract candidate names from a dispatched container node. Returns the
  398. * (name, position) pairs of every function-value-shaped expression found.
  399. */
  400. export function captureFnRefCandidates(
  401. container: SyntaxNode,
  402. rule: CaptureRule,
  403. spec: FnRefSpec,
  404. source: string
  405. ): FnRefCandidate[] {
  406. const valueNodes: SyntaxNode[] = [];
  407. switch (rule.mode) {
  408. case 'args':
  409. case 'list': {
  410. for (let i = 0; i < container.namedChildCount; i++) {
  411. const child = container.namedChild(i);
  412. if (child) valueNodes.push(child);
  413. }
  414. break;
  415. }
  416. case 'rhs': {
  417. const rhs = rule.field
  418. ? getChildByField(container, rule.field)
  419. : container.namedChild(container.namedChildCount - 1);
  420. if (rhs) {
  421. // Param-storage skip: `this.status = status` / `o->cb = cb` — when
  422. // the assigned member's name EQUALS the RHS identifier, the RHS is a
  423. // local/parameter being stored, and the function it holds (if any)
  424. // is unknowable statically. A same-named function elsewhere would
  425. // resolve to the WRONG target (excalidraw A/B finding), so skip.
  426. const lhs =
  427. getChildByField(container, 'left') ??
  428. getChildByField(container, 'lhs') ??
  429. getChildByField(container, 'target') ??
  430. (container.namedChildCount >= 2 ? container.namedChild(0) : null);
  431. const lhsText = lhs ? getNodeText(lhs, source) : '';
  432. const lhsLastName = lhsText.match(/([A-Za-z_$][A-Za-z0-9_$]*)\s*$/)?.[1];
  433. const rhsText = getNodeText(rhs, source).trim();
  434. if (lhsLastName && lhsLastName === rhsText) break;
  435. valueNodes.push(rhs);
  436. }
  437. break;
  438. }
  439. case 'value': {
  440. let value = rule.field ? getChildByField(container, rule.field) : null;
  441. // Keyed containers without a value field (Go keyed_element): the value
  442. // is the LAST named child (the first is the key).
  443. if (!value && container.namedChildCount > 0) {
  444. value = container.namedChild(container.namedChildCount - 1);
  445. }
  446. if (value) valueNodes.push(value);
  447. break;
  448. }
  449. case 'varinit': {
  450. // Destructuring (`const { center } = ellipse`) extracts DATA from the
  451. // RHS — never a function alias. Without this skip, a parameter that
  452. // shadows a same-named imported function produced a wrong edge.
  453. const nameNode =
  454. getChildByField(container, 'name') ?? getChildByField(container, 'pattern');
  455. if (nameNode && (nameNode.type === 'object_pattern' || nameNode.type === 'array_pattern' ||
  456. nameNode.type === 'tuple_pattern' || nameNode.type === 'struct_pattern')) {
  457. break;
  458. }
  459. if (rule.field) {
  460. const value = getChildByField(container, rule.field);
  461. if (value) valueNodes.push(value);
  462. } else {
  463. // No value field in this grammar (C# variable_declarator, Dart
  464. // static_final_declaration): the initializer is the last named child —
  465. // but a declarator WITHOUT an initializer has its NAME there instead.
  466. // Require ≥2 named children and never pick the name/pattern child.
  467. const value = container.namedChild(container.namedChildCount - 1);
  468. const nameChild =
  469. getChildByField(container, 'name') ?? getChildByField(container, 'pattern');
  470. if (
  471. value &&
  472. container.namedChildCount >= 2 &&
  473. (!nameChild || value.id !== nameChild.id)
  474. ) {
  475. valueNodes.push(value);
  476. }
  477. }
  478. break;
  479. }
  480. }
  481. const out: FnRefCandidate[] = [];
  482. for (const v of valueNodes) {
  483. // A bare identifier is one that normalizes without passing through an
  484. // unwrap/special reference form. C++'s addressOfOnly policy (applied at
  485. // flush, where file scope is known) drops bare ids outside file-scope
  486. // initializer tables.
  487. const explicitRef = !spec.idTypes.has(v.type);
  488. for (const { name, node, skipGate } of normalizeValue(v, spec, source, 0)) {
  489. if (!name || NAME_STOPLIST.has(name)) continue;
  490. out.push({
  491. name,
  492. line: node.startPosition.row + 1,
  493. column: node.startPosition.column,
  494. mode: rule.mode,
  495. explicitRef,
  496. skipGate,
  497. });
  498. }
  499. }
  500. return out;
  501. }
  502. /** One normalized function-value: its name, source node, and gate policy. */
  503. interface NormalizedRef {
  504. name: string;
  505. node: SyntaxNode;
  506. skipGate?: boolean;
  507. }
  508. /**
  509. * Normalize one value expression to zero or more function names. Recursion is
  510. * bounded (wrapper layers only); anything that isn't a recognized
  511. * function-value shape yields [].
  512. */
  513. function normalizeValue(
  514. node: SyntaxNode,
  515. spec: FnRefSpec,
  516. source: string,
  517. depth: number
  518. ): NormalizedRef[] {
  519. if (depth > 4) return [];
  520. const type = node.type;
  521. // Bare identifier
  522. if (spec.idTypes.has(type)) {
  523. return [{ name: getNodeText(node, source), node }];
  524. }
  525. // Transparent layers (argument, value_argument, literal_element,
  526. // expression_list, block_argument). expression_list fans out (Go `a, b = f, g`).
  527. const layerField = spec.layers?.get(type);
  528. if (spec.layers?.has(type)) {
  529. // Labeled-argument param-forward skip (Swift/Kotlin): `value: value` /
  530. // `delay: delay` — when the label EQUALS the value identifier, the value
  531. // is a forwarded local/parameter, not a function reference (Alamofire
  532. // A/B finding; same rationale as the `this.x = x` assignment skip).
  533. if (type === 'value_argument') {
  534. const label = getChildByField(node, 'name');
  535. const value = getChildByField(node, 'value') ?? node.namedChild(node.namedChildCount - 1);
  536. if (
  537. label &&
  538. value &&
  539. getNodeText(label, source).trim() === getNodeText(value, source).trim()
  540. ) {
  541. return [];
  542. }
  543. }
  544. if (layerField) {
  545. const inner = getChildByField(node, layerField);
  546. return inner ? normalizeValue(inner, spec, source, depth + 1) : [];
  547. }
  548. const results: NormalizedRef[] = [];
  549. for (let i = 0; i < node.namedChildCount; i++) {
  550. const child = node.namedChild(i);
  551. if (child) results.push(...normalizeValue(child, spec, source, depth + 1));
  552. }
  553. return results;
  554. }
  555. // Unary wrappers: &fn / @Fn / `fn _`
  556. const unwrapField = spec.unwrap?.get(type);
  557. if (spec.unwrap?.has(type)) {
  558. // C-family `pointer_expression` covers BOTH `&x` (address-of — a function
  559. // value) and `*x` (dereference — a data read, never a function value).
  560. // Only `&` qualifies; without this, fmt's `*begin` reads resolved to its
  561. // free `begin()` functions.
  562. if (type === 'pointer_expression' && node.child(0)?.type !== '&') return [];
  563. const inner = unwrapField ? getChildByField(node, unwrapField) : node.namedChild(0);
  564. if (!inner) return [];
  565. // C++ `&Widget::on_click` — keep the QUALIFIED name. Resolution scopes the
  566. // method to that class (more precise than a bare-name match, and exempt
  567. // from the cpp bare-ids-are-free-functions rule since `&Cls::m` is an
  568. // explicit member-pointer).
  569. if (inner.type === 'qualified_identifier') {
  570. const text = getNodeText(inner, source).trim();
  571. return /^[A-Za-z_][\w:]*$/.test(text) ? [{ name: text, node: inner }] : [];
  572. }
  573. return normalizeValue(inner, spec, source, depth + 1);
  574. }
  575. // Special whole-node reference forms
  576. if (spec.special?.has(type)) {
  577. return normalizeSpecial(node, type, source);
  578. }
  579. return [];
  580. }
  581. /** Rightmost descendant-or-self named child of one of the given types. */
  582. function lastNamedOfType(node: SyntaxNode, types: Set<string>): SyntaxNode | null {
  583. let found: SyntaxNode | null = null;
  584. for (let i = 0; i < node.namedChildCount; i++) {
  585. const child = node.namedChild(i);
  586. if (!child) continue;
  587. if (types.has(child.type)) found = child;
  588. const deeper = lastNamedOfType(child, types);
  589. if (deeper) found = deeper;
  590. }
  591. return found;
  592. }
  593. function normalizeSpecial(
  594. node: SyntaxNode,
  595. type: string,
  596. source: string
  597. ): NormalizedRef[] {
  598. switch (type) {
  599. // Java method references. Receiver decides the resolution route (#808):
  600. // `this::run0` / `super::close` → `this.<m>` (class-scoped resolver;
  601. // super rides the inherited-member supertype pass)
  602. // `Type::method` (capitalized) → qualified `Type::method` (suffix-
  603. // matched against that type's members, cross-file capable)
  604. // `variable::method` → nothing (receiver type unknown statically —
  605. // the deferred obj.method class)
  606. case 'method_reference': {
  607. let last: SyntaxNode | null = null;
  608. for (let i = 0; i < node.namedChildCount; i++) {
  609. const child = node.namedChild(i);
  610. if (child && child.type === 'identifier') last = child;
  611. }
  612. if (!last) return [];
  613. const m = getNodeText(last, source);
  614. const text = getNodeText(node, source);
  615. if (text.startsWith('this::') || text.startsWith('super::')) {
  616. return [{ name: `this.${m}`, node: last }];
  617. }
  618. const recv = text.match(/^([A-Z][A-Za-z0-9_]*)\s*::/);
  619. if (recv) {
  620. // `Type::method` — but `Type::new` (constructor ref) has no method
  621. // node to land on; let the stoplist drop it via the bare name.
  622. return m === 'new' ? [] : [{ name: `${recv[1]}::${m}`, node: last }];
  623. }
  624. return [];
  625. }
  626. // Kotlin `::targetCb` (one part) / `OtherClass::handle` (two parts —
  627. // receiver is a type_identifier; lowercase receivers are variables, the
  628. // deferred obj.method class).
  629. case 'callable_reference': {
  630. let receiver: SyntaxNode | null = null;
  631. let member: SyntaxNode | null = null;
  632. for (let i = 0; i < node.namedChildCount; i++) {
  633. const child = node.namedChild(i);
  634. if (!child) continue;
  635. if (child.type === 'type_identifier') receiver = child;
  636. if (child.type === 'simple_identifier') member = child;
  637. }
  638. if (!member) return [];
  639. const m = getNodeText(member, source);
  640. if (!receiver) return [{ name: m, node: member }]; // ::topLevelFn
  641. const recvText = getNodeText(receiver, source);
  642. return /^[A-Z]/.test(recvText)
  643. ? [{ name: `${recvText}::${m}`, node: member }]
  644. : []; // variable::method — unknown receiver type
  645. }
  646. // Kotlin `this::fire` parses as navigation_expression with a `::fire`
  647. // navigation_suffix — route through the class-scoped `this.` resolver.
  648. // Ordinary `a.b` navigation (and any non-`this` receiver) MUST yield
  649. // nothing.
  650. case 'navigation_expression': {
  651. if (!getNodeText(node, source).startsWith('this::')) return [];
  652. for (let i = 0; i < node.namedChildCount; i++) {
  653. const child = node.namedChild(i);
  654. if (child && child.type === 'navigation_suffix' && getNodeText(child, source).startsWith('::')) {
  655. const id = child.namedChild(child.namedChildCount - 1);
  656. if (id) return [{ name: `this.${getNodeText(id, source)}`, node: id }];
  657. }
  658. }
  659. return [];
  660. }
  661. // Swift `#selector(Holder.fire)` → fire. ObjC `@selector(storeImage:)` →
  662. // `storeImage:` verbatim (ObjC method nodes keep their selector colons).
  663. case 'selector_expression': {
  664. const inner = node.namedChild(0);
  665. if (!inner) return [];
  666. if (inner.type === 'identifier' || inner.type === 'simple_identifier') {
  667. return [{ name: getNodeText(inner, source), node: inner }];
  668. }
  669. // Swift dotted form: rightmost simple_identifier. ObjC keyword selector:
  670. // text as-is.
  671. const last = lastNamedOfType(node, new Set(['simple_identifier']));
  672. if (last) return [{ name: getNodeText(last, source), node: last }];
  673. return [{ name: getNodeText(inner, source).trim(), node: inner }];
  674. }
  675. // Ruby `method(:target_cb)` — a `call` whose method is literally `method`
  676. // with a single symbol argument.
  677. case 'call': {
  678. const method = getChildByField(node, 'method');
  679. if (!method || getNodeText(method, source) !== 'method') return [];
  680. const args = getChildByField(node, 'arguments');
  681. if (!args || args.namedChildCount !== 1) return [];
  682. const sym = args.namedChild(0);
  683. if (!sym || sym.type !== 'simple_symbol') return [];
  684. const name = getNodeText(sym, source).replace(/^:/, '');
  685. return name ? [{ name, node: sym }] : [];
  686. }
  687. // `this.handleClick` (TS/JS) — object must be EXACTLY `this`. The name
  688. // keeps the `this.` prefix so resolution can scope it to the enclosing
  689. // class (see resolveThisMemberFnRef) instead of bare name-matching.
  690. case 'member_expression': {
  691. const obj = getChildByField(node, 'object');
  692. const prop = getChildByField(node, 'property');
  693. if (obj && prop && obj.type === 'this' && prop.type === 'property_identifier') {
  694. return [{ name: `this.${getNodeText(prop, source)}`, node: prop }];
  695. }
  696. return [];
  697. }
  698. // `self.handle_click` (Python) — object must be EXACTLY `self`.
  699. case 'attribute': {
  700. const obj = getChildByField(node, 'object');
  701. const attr = getChildByField(node, 'attribute');
  702. if (obj && attr && obj.type === 'identifier' && getNodeText(obj, source) === 'self') {
  703. return [{ name: getNodeText(attr, source), node: attr }];
  704. }
  705. return [];
  706. }
  707. // `this.Run0` (C#) — receiver must be EXACTLY `this`. Two grammar shapes:
  708. // newer tree-sitter-c-sharp exposes an `expression` field holding a
  709. // `this_expression`; the vendored grammar keeps `this` as an anonymous
  710. // token (only the `name` field is a named child), so fall back to the
  711. // node text.
  712. case 'member_access_expression': {
  713. const name = getChildByField(node, 'name');
  714. if (!name) return [];
  715. const expr = getChildByField(node, 'expression');
  716. const isThisReceiver = expr
  717. ? expr.type === 'this_expression' || expr.type === 'this'
  718. : getNodeText(node, source).startsWith('this.');
  719. return isThisReceiver ? [{ name: getNodeText(name, source), node: name }] : [];
  720. }
  721. // PHP string callable — trustworthy ONLY as an argument to a known
  722. // callable-taking core function (`usort($a, 'cmp_items')`). PHP global
  723. // functions are referenced cross-file without imports, so these skip the
  724. // name gate and rely on resolution's unique-or-drop rule. A
  725. // `'Cls::method'` string becomes a qualified candidate.
  726. case 'encapsed_string':
  727. case 'string': {
  728. const callee = phpEnclosingCallName(node);
  729. if (!callee || !PHP_CALLABLE_HOFS.has(callee)) return [];
  730. const content = phpStringContent(node, source);
  731. if (!content) return [];
  732. if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(content)) {
  733. return [{ name: content, node, skipGate: true }];
  734. }
  735. if (/^[A-Za-z_][A-Za-z0-9_]*::[A-Za-z_][A-Za-z0-9_]*$/.test(content)) {
  736. return [{ name: content, node, skipGate: true }];
  737. }
  738. return [];
  739. }
  740. // PHP array callables, valid in ANY call's arguments (the shape itself is
  741. // unambiguous): `[$this, 'method']` → class-scoped `this.method`;
  742. // `[Foo::class, 'method']` → qualified `Foo::method`.
  743. case 'array_creation_expression': {
  744. if (node.namedChildCount !== 2) return [];
  745. const recv = node.namedChild(0)?.namedChild(0);
  746. const strEl = node.namedChild(1)?.namedChild(0);
  747. if (!recv || !strEl) return [];
  748. if (strEl.type !== 'encapsed_string' && strEl.type !== 'string') return [];
  749. const member = phpStringContent(strEl, source);
  750. if (!member || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(member)) return [];
  751. if (recv.type === 'variable_name' && getNodeText(recv, source) === '$this') {
  752. return [{ name: `this.${member}`, node: strEl }];
  753. }
  754. if (recv.type === 'class_constant_access_expression') {
  755. const cls = recv.namedChild(0);
  756. const kw = recv.namedChild(1);
  757. if (cls && kw && getNodeText(kw, source) === 'class') {
  758. return [{ name: `${getNodeText(cls, source)}::${member}`, node: strEl }];
  759. }
  760. }
  761. return [];
  762. }
  763. // Ruby hook-DSL symbols (`before_action :authenticate`,
  764. // `rescue_from E, with: :render_404`): the symbol names a method of the
  765. // ENCLOSING class — route through the class-scoped `this.` resolver
  766. // (which also walks superclasses, covering ApplicationController-style
  767. // inheritance). Symbols under any other call yield nothing.
  768. case 'simple_symbol': {
  769. const call = rubyEnclosingCall(node);
  770. if (!call) return [];
  771. const method = getChildByField(call, 'method');
  772. if (!method || !isRubyHookCall(getNodeText(method, source))) return [];
  773. const sym = getNodeText(node, source).replace(/^:/, '');
  774. if (!/^[A-Za-z_][A-Za-z0-9_?!]*$/.test(sym)) return [];
  775. return [{ name: `this.${sym}`, node }];
  776. }
  777. default:
  778. return [];
  779. }
  780. }
  781. /** Content of a PHP string literal node (single- or double-quoted). */
  782. function phpStringContent(node: SyntaxNode, source: string): string | null {
  783. for (let i = 0; i < node.namedChildCount; i++) {
  784. const child = node.namedChild(i);
  785. if (child?.type === 'string_content') return getNodeText(child, source).trim();
  786. }
  787. return null;
  788. }
  789. /** The function name of the PHP call whose arguments contain `node`, if any. */
  790. function phpEnclosingCallName(node: SyntaxNode): string | null {
  791. let cur: SyntaxNode | null = node.parent;
  792. for (let hops = 0; cur && hops < 4; hops++, cur = cur.parent) {
  793. if (cur.type === 'function_call_expression') {
  794. const fn = getChildByField(cur, 'function');
  795. return fn ? fn.text : null;
  796. }
  797. if (cur.type === 'member_call_expression' || cur.type === 'scoped_call_expression') {
  798. return null; // method calls aren't core HOFs
  799. }
  800. }
  801. return null;
  802. }
  803. /** The Ruby `call` node whose argument_list (or keyword pair) contains `node`. */
  804. function rubyEnclosingCall(node: SyntaxNode): SyntaxNode | null {
  805. let cur: SyntaxNode | null = node.parent;
  806. for (let hops = 0; cur && hops < 4; hops++, cur = cur.parent) {
  807. if (cur.type === 'call') return cur;
  808. }
  809. return null;
  810. }