feat(extraction): PHP string/array callables + Ruby lifecycle-hook symbols (#811)
The last two deferred callback-registration shapes from #756, each scoped to positions where the reference is trustworthy: PHP — a string is a callable ONLY in a known callable position: - string args of core HOFs (usort, array_map, array_filter, call_user_func*, preg_replace_callback, spl_autoload_register, set_error_handler, … — PHP_CALLABLE_HOFS): ungated (PHP globals are referenced cross-file without imports) + resolution unique-or-drop, function-kind only ('Cls::m' strings resolve qualified) - array callables anywhere in call args: [$this, 'method'] routes through the class-scoped this. resolver (parents included); [Foo::class, 'method'] resolves qualified - strings to arbitrary functions: deliberately nothing Ruby — hook-DSL symbols name a method of the enclosing class: (skip_)?(before|after|around)_* / validate / set_callback / helper_method / rescue_from(with:) symbols → class-scoped this.<sym>, riding the supertype pass so `before_action :authenticate` in a controller resolves to ApplicationController's method. `validates` (plural) excluded — its symbols name ATTRIBUTES. Class-body-level hooks attribute to the CLASS node (the scoped resolvers now accept class-like from-nodes). Also hardened while validating: the this.X supertype pass is now NODE-anchored — file-anchored class node → implements/extends edge targets → contains-anchored member lookup — replacing the name-keyed getSupertypes walk, which unioned every same-named class's parents (rails has a dozen `Engine`s) and produced a cross-class wrong edge. A/B vs main: WordPress +556 (14/14 sampled genuine — [$this,'m'] wiring, array_map('absint',…), sodium polyfill call_user_func_array dispatch); rails/rails +385 after the node-anchored fix (16/16 sampled genuine, incl. inherited hooks across real extends edges); controls byte-stable (excalidraw 0-delta, redis identical, typeorm keeps its +4 inherited getters). The only calls-edge deltas anywhere are pre-existing minified-bundle resolution jitter (wp-tinymce.js single-letter symbols). Full suite 1391 passed. EXTRACTION_VERSION 21 → 22 (re-index to benefit). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
38095aa95b
commit
1f15f93feb
@@ -19,6 +19,8 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|||||||
- TypeScript and JavaScript **class fields are now reported as properties instead of methods**. A plain field like `public fonts: Fonts;` previously extracted as a method, misrepresenting class shape and letting calls to same-named functions resolve to data fields (a boolean field named `isArray` was soaking up `Array.isArray(...)` call edges). Fields holding arrow functions or function expressions (`onClick = () => {…}`, including wrapped ones like `onScroll = throttle(() => {…})`) correctly remain methods and their bodies are still analyzed. Field initializers are analyzed too, so `history = createHistory()` records its call — and JavaScript class fields, which previously produced no symbol at all, now appear in the graph. Re-index a project to benefit. (#808) (TypeScript, JavaScript)
|
- TypeScript and JavaScript **class fields are now reported as properties instead of methods**. A plain field like `public fonts: Fonts;` previously extracted as a method, misrepresenting class shape and letting calls to same-named functions resolve to data fields (a boolean field named `isArray` was soaking up `Array.isArray(...)` call edges). Fields holding arrow functions or function expressions (`onClick = () => {…}`, including wrapped ones like `onScroll = throttle(() => {…})`) correctly remain methods and their bodies are still analyzed. Field initializers are analyzed too, so `history = createHistory()` records its call — and JavaScript class fields, which previously produced no symbol at all, now appear in the graph. Re-index a project to benefit. (#808) (TypeScript, JavaScript)
|
||||||
- Callback registration through `this` now resolves precisely in TypeScript and JavaScript: `window.addEventListener("online", this.onOfflineStatusToggle)` or an API object like `{ mutateElement: this.mutateElement }` produces a reference edge to the **enclosing class's own method** — never a same-named method on an unrelated class, and never a data field. Builds on the callback-registration support below. (#808) (TypeScript, JavaScript)
|
- Callback registration through `this` now resolves precisely in TypeScript and JavaScript: `window.addEventListener("online", this.onOfflineStatusToggle)` or an API object like `{ mutateElement: this.mutateElement }` produces a reference edge to the **enclosing class's own method** — never a same-named method on an unrelated class, and never a data field. Builds on the callback-registration support below. (#808) (TypeScript, JavaScript)
|
||||||
- Callback-registration coverage deepened across four more shapes: a `this.<member>` registration whose method lives on a **base class** now resolves through the inheritance chain (`bus.on("submit", this.handleSubmit)` in a subclass links to the parent's `handleSubmit`); Java and Kotlin **method references to other classes** (`Handlers::onMessage`, `OtherClass::handle`) resolve across files, with `this::` and `super::` scoped to the defining class and references through a variable deliberately left out; and Swift bare callback names now match only the **enclosing type's** methods (implicit `self`), eliminating a class of wrong edges where a parameter like `request` linked to a same-named method on an unrelated type. (Java, Kotlin, Swift, TypeScript, JavaScript)
|
- Callback-registration coverage deepened across four more shapes: a `this.<member>` registration whose method lives on a **base class** now resolves through the inheritance chain (`bus.on("submit", this.handleSubmit)` in a subclass links to the parent's `handleSubmit`); Java and Kotlin **method references to other classes** (`Handlers::onMessage`, `OtherClass::handle`) resolve across files, with `this::` and `super::` scoped to the defining class and references through a variable deliberately left out; and Swift bare callback names now match only the **enclosing type's** methods (implicit `self`), eliminating a class of wrong edges where a parameter like `request` linked to a same-named method on an unrelated type. (Java, Kotlin, Swift, TypeScript, JavaScript)
|
||||||
|
- PHP **string and array callables** now register: a string passed to a callable-taking core function (`usort($items, 'cmp_items')`, `array_map('absint', …)`, `call_user_func`, `spl_autoload_register`, …) links to that function — including across files — and the array forms `[$this, 'method']` and `[Foo::class, 'method']` link to the named method (the `$this` form resolves through the class and its parents). Strings passed to arbitrary functions are deliberately ignored: only known callable positions are trusted. Validated on WordPress core (+556 edges, every sampled edge a genuine registration). (PHP)
|
||||||
|
- Ruby **lifecycle-hook symbols** now register: `before_action :authenticate`, `after_save :reindex`, `around_create`, `validate :check`, `rescue_from(…, with: :handler)` and friends link the symbol to the method it names — on the class itself or **inherited from a parent** (`before_action :authenticate` in a controller resolves to `ApplicationController`'s method). `validates` (plural) is excluded since its symbols name attributes, not methods. Validated on rails/rails (+385 edges, every sampled edge genuine). (Ruby)
|
||||||
- CodeGraph now sees where a function is **registered as a callback**, not just where it's called. A function name passed as an argument (`signal(SIGINT, handler)`, `qsort(…, compare)`, `addEventListener(…, onBlur)`), assigned to a function pointer or field (`ops->recv_cb = my_cb`, `OnClick := Handler`), or placed in a struct initializer or handler table (`{ .recv_cb = my_cb }`, `{ "get", getCommand }`) now produces a reference edge from the registration site to the function — so `codegraph_callers` and `codegraph_impact` surface callback wiring that previously looked like dead code. Works across all supported languages, including the language-specific forms: C/C++ `&fn`, Java `Class::method`, Kotlin `::fn`, Swift `#selector`, Objective-C `@selector`, Ruby `method(:fn)`, Scala eta-expansion, and Delphi/Pascal `@Handler` and `OnClick := Handler` event wiring. Callers output labels these "via callback registration". Resolution is deliberately conservative: an ambiguous name produces no edge rather than a wrong one. Re-index a project to benefit. Thanks @zmcrazy. (#756)
|
- CodeGraph now sees where a function is **registered as a callback**, not just where it's called. A function name passed as an argument (`signal(SIGINT, handler)`, `qsort(…, compare)`, `addEventListener(…, onBlur)`), assigned to a function pointer or field (`ops->recv_cb = my_cb`, `OnClick := Handler`), or placed in a struct initializer or handler table (`{ .recv_cb = my_cb }`, `{ "get", getCommand }`) now produces a reference edge from the registration site to the function — so `codegraph_callers` and `codegraph_impact` surface callback wiring that previously looked like dead code. Works across all supported languages, including the language-specific forms: C/C++ `&fn`, Java `Class::method`, Kotlin `::fn`, Swift `#selector`, Objective-C `@selector`, Ruby `method(:fn)`, Scala eta-expansion, and Delphi/Pascal `@Handler` and `OnClick := Handler` event wiring. Callers output labels these "via callback registration". Resolution is deliberately conservative: an ambiguous name produces no edge rather than a wrong one. Re-index a project to benefit. Thanks @zmcrazy. (#756)
|
||||||
- The `codegraph_node` MCP tool can now **read a whole source file like the built-in Read tool — only faster, served from the index**. Pass a file path with no symbol and it returns that file's current source with line numbers (the same `<n>⇥<line>` shape Read produces, so an assistant can edit straight from it), narrowable with `offset`/`limit` exactly like Read, plus a one-line note of which files depend on it (the file's blast radius). Use it anywhere you'd reach for Read on an indexed source file. Pass `symbolsOnly: true` for just the file's structure. Configuration/data files (`.yml` / `.properties`) are summarized by key only, never dumped, so secrets in them are never surfaced. The agent-facing guidance was also retuned so assistants reach for codegraph while *implementing* a change (not only when answering questions), since one codegraph call returns the same bytes plus the blast radius, faster than re-reading the file.
|
- The `codegraph_node` MCP tool can now **read a whole source file like the built-in Read tool — only faster, served from the index**. Pass a file path with no symbol and it returns that file's current source with line numbers (the same `<n>⇥<line>` shape Read produces, so an assistant can edit straight from it), narrowable with `offset`/`limit` exactly like Read, plus a one-line note of which files depend on it (the file's blast radius). Use it anywhere you'd reach for Read on an indexed source file. Pass `symbolsOnly: true` for just the file's structure. Configuration/data files (`.yml` / `.properties`) are summarized by key only, never dumped, so secrets in them are never surfaced. The agent-facing guidance was also retuned so assistants reach for codegraph while *implementing* a change (not only when answering questions), since one codegraph call returns the same bytes plus the blast radius, faster than re-reading the file.
|
||||||
- New `codegraph upgrade` command updates CodeGraph to the latest release in place — it detects how you installed (the standalone `install.sh` / `install.ps1` bundle, npm, or npx) and does the right thing for each, on macOS, Linux, and Windows. Use `codegraph upgrade --check` to see whether an update is available without installing, or `codegraph upgrade <version>` to move to a specific version. After upgrading it reminds you to re-index your projects so they pick up the newer engine's improvements. (#679)
|
- New `codegraph upgrade` command updates CodeGraph to the latest release in place — it detects how you installed (the standalone `install.sh` / `install.ps1` bundle, npm, or npx) and does the right thing for each, on macOS, Linux, and Windows. Use `codegraph upgrade --check` to see whether an update is available without installing, or `codegraph upgrade <version>` to move to a specific version. After upgrading it reminds you to re-index your projects so they pick up the newer engine's improvements. (#679)
|
||||||
|
|||||||
@@ -617,6 +617,88 @@ describe('Function-as-value capture (#756)', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('PHP: HOF string callables, [$this,…] and [Cls::class,…] arrays; non-HOF strings ignored', async () => {
|
||||||
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-php-'));
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tmpDir, 'handlers.php'),
|
||||||
|
"<?php\nfunction cmp_items($a, $b) { return $a <=> $b; }\n"
|
||||||
|
);
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tmpDir, 'main.php'),
|
||||||
|
[
|
||||||
|
'<?php',
|
||||||
|
'class Saver {',
|
||||||
|
' public function onSave($x) {}',
|
||||||
|
' public function wire() {',
|
||||||
|
" register_shutdown_function([$this, 'onSave']);",
|
||||||
|
' }',
|
||||||
|
'}',
|
||||||
|
'class Loader {',
|
||||||
|
' public static function load($cls) {}',
|
||||||
|
'}',
|
||||||
|
'function sorter($items) {',
|
||||||
|
" usort($items, 'cmp_items');", // known HOF, cross-file string → edge
|
||||||
|
" spl_autoload_register([Loader::class, 'load']);",
|
||||||
|
" some_random_fn('cmp_items');", // NOT a known HOF → no edge
|
||||||
|
' return $items;',
|
||||||
|
'}',
|
||||||
|
].join('\n')
|
||||||
|
);
|
||||||
|
|
||||||
|
const cg = CodeGraph.initSync(tmpDir);
|
||||||
|
try {
|
||||||
|
await cg.indexAll();
|
||||||
|
// Exactly ONE source for cmp_items: the usort site, not some_random_fn.
|
||||||
|
expect(sourceNames(cg, fnRefEdgesInto(cg, 'cmp_items'))).toEqual(['sorter']);
|
||||||
|
expect(sourceNames(cg, fnRefEdgesInto(cg, 'onSave'))).toEqual(['wire']);
|
||||||
|
expect(sourceNames(cg, fnRefEdgesInto(cg, 'load'))).toEqual(['sorter']);
|
||||||
|
} finally {
|
||||||
|
cg.destroy();
|
||||||
|
tmpDir = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('RUBY HOOKS: before_action/rescue_from symbols resolve class-scoped incl. inherited; validates is excluded', async () => {
|
||||||
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-rubyhooks-'));
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(tmpDir, 'posts_controller.rb'),
|
||||||
|
[
|
||||||
|
'class ApplicationController',
|
||||||
|
' def authenticate; end',
|
||||||
|
'end',
|
||||||
|
'',
|
||||||
|
'class PostsController < ApplicationController',
|
||||||
|
' before_action :authenticate', // inherited → ApplicationController
|
||||||
|
' after_save :reindex',
|
||||||
|
' validates :title, presence: true', // attributes, NOT methods → no edge
|
||||||
|
' rescue_from StandardError, with: :render_500',
|
||||||
|
'',
|
||||||
|
' def reindex; end',
|
||||||
|
' def render_500; end',
|
||||||
|
' def title; end',
|
||||||
|
'end',
|
||||||
|
].join('\n')
|
||||||
|
);
|
||||||
|
|
||||||
|
const cg = CodeGraph.initSync(tmpDir);
|
||||||
|
try {
|
||||||
|
await cg.indexAll();
|
||||||
|
|
||||||
|
const auth = fnRefEdgesInto(cg, 'authenticate');
|
||||||
|
expect(auth).toHaveLength(1);
|
||||||
|
expect(cg.getNode(auth[0]!.target)?.qualifiedName).toContain('ApplicationController');
|
||||||
|
|
||||||
|
expect(fnRefEdgesInto(cg, 'reindex')).toHaveLength(1);
|
||||||
|
expect(fnRefEdgesInto(cg, 'render_500')).toHaveLength(1);
|
||||||
|
// `validates :title` names an attribute — the same-named METHOD must
|
||||||
|
// get no registration edge.
|
||||||
|
expect(fnRefEdgesInto(cg, 'title')).toHaveLength(0);
|
||||||
|
} finally {
|
||||||
|
cg.destroy();
|
||||||
|
tmpDir = undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it('DRAIN: resolvable function_ref rows leave unresolved_refs; re-index is stable', async () => {
|
it('DRAIN: resolvable function_ref rows leave unresolved_refs; re-index is stable', async () => {
|
||||||
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-drain-'));
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-fnref-drain-'));
|
||||||
fs.writeFileSync(
|
fs.writeFileSync(
|
||||||
|
|||||||
@@ -57,7 +57,8 @@ custom `visitNode` hooks like Scala's val/var handler) get a candidates-only
|
|||||||
| Dart | `arguments` (`argument`) | `assignment_expression.right` | `pair.value` | `list_literal`, `static_final_declaration` | — |
|
| Dart | `arguments` (`argument`) | `assignment_expression.right` | `pair.value` | `list_literal`, `static_final_declaration` | — |
|
||||||
| Lua / Luau | `arguments` | `assignment_statement` (`expression_list.value`) | `field.value` (keyed + positional) | (same) | — |
|
| Lua / Luau | `arguments` | `assignment_statement` (`expression_list.value`) | `field.value` (keyed + positional) | (same) | — |
|
||||||
| Pascal | `exprArgs` (via `visitPascalBlock`) | `assignment.rhs` (`OnFire := Handler`) | — | — | `@Handler` (`exprUnary.operand`) |
|
| Pascal | `exprArgs` (via `visitPascalBlock`) | `assignment.rhs` (`OnFire := Handler`) | — | — | `@Handler` (`exprUnary.operand`) |
|
||||||
| PHP | **skipped** | — | — | — | first-class callable `fn(...)` already extracts as a `calls` edge; string callables are a precision risk, deferred |
|
| PHP | string callables ONLY as args of known core HOFs (`usort`, `array_map`, `call_user_func*`… — `PHP_CALLABLE_HOFS`), ungated + unique-or-drop (PHP globals aren't imported) | — | — | — | `[$this, 'm']` → class-scoped `this.m`; `[Foo::class, 'm']` → qualified; `'Cls::m'` → qualified; first-class callable `fn(...)` already extracts as `calls` |
|
||||||
|
| Ruby hooks | `(skip_)?(before\|after\|around)_*` + `validate`/`set_callback`/`helper_method`/`rescue_from(with:)` symbols → class-scoped `this.<sym>` (rides the supertype pass: `before_action :authenticate` → ApplicationController). `validates` (plural) excluded — its symbols are ATTRIBUTES | — | — | — | symbols under any other call yield nothing |
|
||||||
|
|
||||||
## Precision rules (each one bought by a real-repo false positive)
|
## Precision rules (each one bought by a real-repo false positive)
|
||||||
|
|
||||||
@@ -199,8 +200,18 @@ Index cost on redis: +6% time, +5% db size.
|
|||||||
imports, so cross-file bare callbacks only resolve when repo-unique
|
imports, so cross-file bare callbacks only resolve when repo-unique
|
||||||
(functions; methods are enclosing-type-only). Cross-TYPE `#selector`
|
(functions; methods are enclosing-type-only). Cross-TYPE `#selector`
|
||||||
targets (rare — target-action is normally self) are scoped away too.
|
targets (rare — target-action is normally self) are scoped away too.
|
||||||
- **PHP string callables**, **Ruby bare symbols** outside `method(:sym)`,
|
- **`obj.method` member values** where `obj` isn't `this`/`self`: deferred —
|
||||||
**`obj.method` member values** where `obj` isn't `this`/`self`: deferred.
|
the receiver's type is statically unknowable without local data-flow.
|
||||||
|
- **PHP strings outside known-HOF positions** (a bare `'handler'` to an
|
||||||
|
arbitrary function; framework registries like WordPress `add_action`):
|
||||||
|
deliberately uncaptured — a string is only trustworthy as a callable in a
|
||||||
|
known callable position. Framework registries belong in a `frameworks/`
|
||||||
|
resolver if ever added. **Ruby symbols outside the hook DSLs** likewise.
|
||||||
|
- **The supertype pass is NODE-anchored** (file-anchored class node →
|
||||||
|
implements/extends edge targets → `contains`-anchored member lookup): a
|
||||||
|
name-keyed `getSupertypes('Engine')` unioned every rails `Engine`'s parents
|
||||||
|
and produced a cross-class wrong edge; the node walk eliminated it
|
||||||
|
(rails +440 → +385, all sampled edges genuine).
|
||||||
- **`this.X` inherited members resolve through the supertype pass**
|
- **`this.X` inherited members resolve through the supertype pass**
|
||||||
(`resolveDeferredThisMemberRefs`, depth-capped BFS over implements/extends,
|
(`resolveDeferredThisMemberRefs`, depth-capped BFS over implements/extends,
|
||||||
runs after edges persist — same lifecycle as the #750 conformance pass).
|
runs after edges persist — same lifecycle as the #750 conformance pass).
|
||||||
|
|||||||
@@ -21,4 +21,4 @@
|
|||||||
* turns the re-index hint into noise — keep it honest (see CLAUDE.md, "Honesty
|
* turns the re-index hint into noise — keep it honest (see CLAUDE.md, "Honesty
|
||||||
* in the product is load-bearing").
|
* in the product is load-bearing").
|
||||||
*/
|
*/
|
||||||
export const EXTRACTION_VERSION = 21;
|
export const EXTRACTION_VERSION = 22;
|
||||||
|
|||||||
+166
-12
@@ -25,9 +25,9 @@
|
|||||||
*
|
*
|
||||||
* Deliberately NOT covered (resolving the *dispatch* — `o->cb(x)` → the
|
* Deliberately NOT covered (resolving the *dispatch* — `o->cb(x)` → the
|
||||||
* registered function — needs data-flow through struct fields; a wrong edge
|
* registered function — needs data-flow through struct fields; a wrong edge
|
||||||
* is worse than none): indirect-call resolution, PHP string callables,
|
* is worse than none): indirect-call resolution and `obj.method` member
|
||||||
* Ruby bare symbols outside `method(:sym)`, and `obj.method` member values
|
* values where `obj` isn't `this`/`self` (the receiver's type is statically
|
||||||
* where `obj` isn't `this`/`self`.
|
* unknowable without local data-flow).
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import type { Node as SyntaxNode } from 'web-tree-sitter';
|
import type { Node as SyntaxNode } from 'web-tree-sitter';
|
||||||
@@ -45,6 +45,15 @@ export interface FnRefCandidate {
|
|||||||
* C++'s flush policy keys on it.
|
* C++'s flush policy keys on it.
|
||||||
*/
|
*/
|
||||||
explicitRef: boolean;
|
explicitRef: boolean;
|
||||||
|
/**
|
||||||
|
* Skip the same-file/import name gate for this candidate. Set for PHP
|
||||||
|
* string callables in known HOF positions: PHP global functions are
|
||||||
|
* referenced cross-file WITHOUT imports (global namespace), so the gate
|
||||||
|
* can't see them — the strong positional prior (a string argument to
|
||||||
|
* `usort`/`array_map`/…) plus resolution's unique-or-drop rule carry the
|
||||||
|
* precision instead.
|
||||||
|
*/
|
||||||
|
skipGate?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** How to pull candidate value nodes out of a dispatched container node. */
|
/** How to pull candidate value nodes out of a dispatched container node. */
|
||||||
@@ -252,16 +261,30 @@ const CSHARP_SPEC: FnRefSpec = {
|
|||||||
|
|
||||||
const RUBY_SPEC: FnRefSpec = {
|
const RUBY_SPEC: FnRefSpec = {
|
||||||
// Bare identifiers in Ruby args are method CALLS or locals, never function
|
// Bare identifiers in Ruby args are method CALLS or locals, never function
|
||||||
// values — only the `method(:name)` idiom (and `&method(:name)`) qualifies.
|
// values — only the `method(:name)` idiom (and `&method(:name)`) plus
|
||||||
|
// hook-DSL symbols (`before_action :authenticate`) qualify.
|
||||||
idTypes: new Set<string>(),
|
idTypes: new Set<string>(),
|
||||||
dispatch: new Map<string, CaptureRule>([
|
dispatch: new Map<string, CaptureRule>([
|
||||||
['argument_list', { mode: 'args' }],
|
['argument_list', { mode: 'args' }],
|
||||||
['pair', { mode: 'value', field: 'value' }],
|
['pair', { mode: 'value', field: 'value' }],
|
||||||
]),
|
]),
|
||||||
layers: new Map<string, string | null>([['block_argument', null]]),
|
layers: new Map<string, string | null>([['block_argument', null]]),
|
||||||
special: new Set(['call']),
|
special: new Set(['call', 'simple_symbol']),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Rails/ActiveSupport-style hook DSLs whose symbol arguments name a method of
|
||||||
|
* the enclosing class: lifecycle callbacks (`before_action`, `after_save`,
|
||||||
|
* `around_create`, `skip_before_action`…), `validate :method`, `set_callback`,
|
||||||
|
* `helper_method`, and `rescue_from(..., with: :handler)`. NOT `validates`
|
||||||
|
* (plural) — its symbols name ATTRIBUTES, not methods.
|
||||||
|
*/
|
||||||
|
const RUBY_HOOK_RE = /^(skip_)?(before|after|around)_[a-z_]+$/;
|
||||||
|
const RUBY_HOOK_NAMES = new Set(['validate', 'set_callback', 'helper_method', 'rescue_from']);
|
||||||
|
function isRubyHookCall(name: string): boolean {
|
||||||
|
return RUBY_HOOK_RE.test(name) || RUBY_HOOK_NAMES.has(name);
|
||||||
|
}
|
||||||
|
|
||||||
const SWIFT_SPEC: FnRefSpec = {
|
const SWIFT_SPEC: FnRefSpec = {
|
||||||
idTypes: new Set(['simple_identifier']),
|
idTypes: new Set(['simple_identifier']),
|
||||||
dispatch: new Map<string, CaptureRule>([
|
dispatch: new Map<string, CaptureRule>([
|
||||||
@@ -316,9 +339,39 @@ const PASCAL_SPEC: FnRefSpec = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Capture specs by language. PHP is deliberately absent: its first-class
|
* PHP core functions whose string arguments are CALLABLES — the positional
|
||||||
* callable `fn(...)` already extracts as a `calls` edge, and string callables
|
* prior that makes a bare string trustworthy as a function reference.
|
||||||
* (`'fn_name'`) are a precision risk left for a follow-up.
|
* Deliberately core-PHP only; framework registries (WordPress `add_action`)
|
||||||
|
* belong in a frameworks/ resolver if ever added.
|
||||||
|
*/
|
||||||
|
const PHP_CALLABLE_HOFS = new Set([
|
||||||
|
'array_map', 'array_filter', 'array_walk', 'array_walk_recursive', 'array_reduce',
|
||||||
|
'usort', 'uasort', 'uksort',
|
||||||
|
'array_udiff', 'array_udiff_assoc', 'array_uintersect', 'array_uintersect_assoc',
|
||||||
|
'call_user_func', 'call_user_func_array',
|
||||||
|
'forward_static_call', 'forward_static_call_array',
|
||||||
|
'preg_replace_callback', 'preg_replace_callback_array',
|
||||||
|
'register_shutdown_function', 'register_tick_function',
|
||||||
|
'set_error_handler', 'set_exception_handler', 'spl_autoload_register',
|
||||||
|
'ob_start', 'iterator_apply', 'header_register_callback',
|
||||||
|
'is_callable',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const PHP_SPEC: FnRefSpec = {
|
||||||
|
// PHP has no bare-identifier function values (the first-class callable
|
||||||
|
// `fn(...)` already extracts as a `calls` edge). What qualifies:
|
||||||
|
// - a string argument to a known callable-taking core function
|
||||||
|
// (`usort($a, 'cmp_items')`) — see PHP_CALLABLE_HOFS
|
||||||
|
// - array callables: `[$this, 'method']` (class-scoped) and
|
||||||
|
// `[Foo::class, 'method']` (qualified), in any call's arguments
|
||||||
|
idTypes: new Set<string>(),
|
||||||
|
dispatch: new Map<string, CaptureRule>([['arguments', { mode: 'args' }]]),
|
||||||
|
layers: new Map<string, string | null>([['argument', null]]),
|
||||||
|
special: new Set(['encapsed_string', 'string', 'array_creation_expression']),
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Capture specs by language.
|
||||||
*/
|
*/
|
||||||
export const FN_REF_SPECS: Record<string, FnRefSpec | undefined> = {
|
export const FN_REF_SPECS: Record<string, FnRefSpec | undefined> = {
|
||||||
c: cFamilySpec(),
|
c: cFamilySpec(),
|
||||||
@@ -334,6 +387,7 @@ export const FN_REF_SPECS: Record<string, FnRefSpec | undefined> = {
|
|||||||
java: JAVA_SPEC,
|
java: JAVA_SPEC,
|
||||||
kotlin: KOTLIN_SPEC,
|
kotlin: KOTLIN_SPEC,
|
||||||
csharp: CSHARP_SPEC,
|
csharp: CSHARP_SPEC,
|
||||||
|
php: PHP_SPEC,
|
||||||
ruby: RUBY_SPEC,
|
ruby: RUBY_SPEC,
|
||||||
swift: SWIFT_SPEC,
|
swift: SWIFT_SPEC,
|
||||||
scala: SCALA_SPEC,
|
scala: SCALA_SPEC,
|
||||||
@@ -441,7 +495,7 @@ export function captureFnRefCandidates(
|
|||||||
// flush, where file scope is known) drops bare ids outside file-scope
|
// flush, where file scope is known) drops bare ids outside file-scope
|
||||||
// initializer tables.
|
// initializer tables.
|
||||||
const explicitRef = !spec.idTypes.has(v.type);
|
const explicitRef = !spec.idTypes.has(v.type);
|
||||||
for (const { name, node } of normalizeValue(v, spec, source, 0)) {
|
for (const { name, node, skipGate } of normalizeValue(v, spec, source, 0)) {
|
||||||
if (!name || NAME_STOPLIST.has(name)) continue;
|
if (!name || NAME_STOPLIST.has(name)) continue;
|
||||||
out.push({
|
out.push({
|
||||||
name,
|
name,
|
||||||
@@ -449,12 +503,20 @@ export function captureFnRefCandidates(
|
|||||||
column: node.startPosition.column,
|
column: node.startPosition.column,
|
||||||
mode: rule.mode,
|
mode: rule.mode,
|
||||||
explicitRef,
|
explicitRef,
|
||||||
|
skipGate,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** One normalized function-value: its name, source node, and gate policy. */
|
||||||
|
interface NormalizedRef {
|
||||||
|
name: string;
|
||||||
|
node: SyntaxNode;
|
||||||
|
skipGate?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize one value expression to zero or more function names. Recursion is
|
* Normalize one value expression to zero or more function names. Recursion is
|
||||||
* bounded (wrapper layers only); anything that isn't a recognized
|
* bounded (wrapper layers only); anything that isn't a recognized
|
||||||
@@ -465,7 +527,7 @@ function normalizeValue(
|
|||||||
spec: FnRefSpec,
|
spec: FnRefSpec,
|
||||||
source: string,
|
source: string,
|
||||||
depth: number
|
depth: number
|
||||||
): Array<{ name: string; node: SyntaxNode }> {
|
): NormalizedRef[] {
|
||||||
if (depth > 4) return [];
|
if (depth > 4) return [];
|
||||||
const type = node.type;
|
const type = node.type;
|
||||||
|
|
||||||
@@ -497,7 +559,7 @@ function normalizeValue(
|
|||||||
const inner = getChildByField(node, layerField);
|
const inner = getChildByField(node, layerField);
|
||||||
return inner ? normalizeValue(inner, spec, source, depth + 1) : [];
|
return inner ? normalizeValue(inner, spec, source, depth + 1) : [];
|
||||||
}
|
}
|
||||||
const results: Array<{ name: string; node: SyntaxNode }> = [];
|
const results: NormalizedRef[] = [];
|
||||||
for (let i = 0; i < node.namedChildCount; i++) {
|
for (let i = 0; i < node.namedChildCount; i++) {
|
||||||
const child = node.namedChild(i);
|
const child = node.namedChild(i);
|
||||||
if (child) results.push(...normalizeValue(child, spec, source, depth + 1));
|
if (child) results.push(...normalizeValue(child, spec, source, depth + 1));
|
||||||
@@ -551,7 +613,7 @@ function normalizeSpecial(
|
|||||||
node: SyntaxNode,
|
node: SyntaxNode,
|
||||||
type: string,
|
type: string,
|
||||||
source: string
|
source: string
|
||||||
): Array<{ name: string; node: SyntaxNode }> {
|
): NormalizedRef[] {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
// Java method references. Receiver decides the resolution route (#808):
|
// Java method references. Receiver decides the resolution route (#808):
|
||||||
// `this::run0` / `super::close` → `this.<m>` (class-scoped resolver;
|
// `this::run0` / `super::close` → `this.<m>` (class-scoped resolver;
|
||||||
@@ -683,7 +745,99 @@ function normalizeSpecial(
|
|||||||
return isThisReceiver ? [{ name: getNodeText(name, source), node: name }] : [];
|
return isThisReceiver ? [{ name: getNodeText(name, source), node: name }] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PHP string callable — trustworthy ONLY as an argument to a known
|
||||||
|
// callable-taking core function (`usort($a, 'cmp_items')`). PHP global
|
||||||
|
// functions are referenced cross-file without imports, so these skip the
|
||||||
|
// name gate and rely on resolution's unique-or-drop rule. A
|
||||||
|
// `'Cls::method'` string becomes a qualified candidate.
|
||||||
|
case 'encapsed_string':
|
||||||
|
case 'string': {
|
||||||
|
const callee = phpEnclosingCallName(node);
|
||||||
|
if (!callee || !PHP_CALLABLE_HOFS.has(callee)) return [];
|
||||||
|
const content = phpStringContent(node, source);
|
||||||
|
if (!content) return [];
|
||||||
|
if (/^[A-Za-z_][A-Za-z0-9_]*$/.test(content)) {
|
||||||
|
return [{ name: content, node, skipGate: true }];
|
||||||
|
}
|
||||||
|
if (/^[A-Za-z_][A-Za-z0-9_]*::[A-Za-z_][A-Za-z0-9_]*$/.test(content)) {
|
||||||
|
return [{ name: content, node, skipGate: true }];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// PHP array callables, valid in ANY call's arguments (the shape itself is
|
||||||
|
// unambiguous): `[$this, 'method']` → class-scoped `this.method`;
|
||||||
|
// `[Foo::class, 'method']` → qualified `Foo::method`.
|
||||||
|
case 'array_creation_expression': {
|
||||||
|
if (node.namedChildCount !== 2) return [];
|
||||||
|
const recv = node.namedChild(0)?.namedChild(0);
|
||||||
|
const strEl = node.namedChild(1)?.namedChild(0);
|
||||||
|
if (!recv || !strEl) return [];
|
||||||
|
if (strEl.type !== 'encapsed_string' && strEl.type !== 'string') return [];
|
||||||
|
const member = phpStringContent(strEl, source);
|
||||||
|
if (!member || !/^[A-Za-z_][A-Za-z0-9_]*$/.test(member)) return [];
|
||||||
|
if (recv.type === 'variable_name' && getNodeText(recv, source) === '$this') {
|
||||||
|
return [{ name: `this.${member}`, node: strEl }];
|
||||||
|
}
|
||||||
|
if (recv.type === 'class_constant_access_expression') {
|
||||||
|
const cls = recv.namedChild(0);
|
||||||
|
const kw = recv.namedChild(1);
|
||||||
|
if (cls && kw && getNodeText(kw, source) === 'class') {
|
||||||
|
return [{ name: `${getNodeText(cls, source)}::${member}`, node: strEl }];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ruby hook-DSL symbols (`before_action :authenticate`,
|
||||||
|
// `rescue_from E, with: :render_404`): the symbol names a method of the
|
||||||
|
// ENCLOSING class — route through the class-scoped `this.` resolver
|
||||||
|
// (which also walks superclasses, covering ApplicationController-style
|
||||||
|
// inheritance). Symbols under any other call yield nothing.
|
||||||
|
case 'simple_symbol': {
|
||||||
|
const call = rubyEnclosingCall(node);
|
||||||
|
if (!call) return [];
|
||||||
|
const method = getChildByField(call, 'method');
|
||||||
|
if (!method || !isRubyHookCall(getNodeText(method, source))) return [];
|
||||||
|
const sym = getNodeText(node, source).replace(/^:/, '');
|
||||||
|
if (!/^[A-Za-z_][A-Za-z0-9_?!]*$/.test(sym)) return [];
|
||||||
|
return [{ name: `this.${sym}`, node }];
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Content of a PHP string literal node (single- or double-quoted). */
|
||||||
|
function phpStringContent(node: SyntaxNode, source: string): string | null {
|
||||||
|
for (let i = 0; i < node.namedChildCount; i++) {
|
||||||
|
const child = node.namedChild(i);
|
||||||
|
if (child?.type === 'string_content') return getNodeText(child, source).trim();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The function name of the PHP call whose arguments contain `node`, if any. */
|
||||||
|
function phpEnclosingCallName(node: SyntaxNode): string | null {
|
||||||
|
let cur: SyntaxNode | null = node.parent;
|
||||||
|
for (let hops = 0; cur && hops < 4; hops++, cur = cur.parent) {
|
||||||
|
if (cur.type === 'function_call_expression') {
|
||||||
|
const fn = getChildByField(cur, 'function');
|
||||||
|
return fn ? fn.text : null;
|
||||||
|
}
|
||||||
|
if (cur.type === 'member_call_expression' || cur.type === 'scoped_call_expression') {
|
||||||
|
return null; // method calls aren't core HOFs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The Ruby `call` node whose argument_list (or keyword pair) contains `node`. */
|
||||||
|
function rubyEnclosingCall(node: SyntaxNode): SyntaxNode | null {
|
||||||
|
let cur: SyntaxNode | null = node.parent;
|
||||||
|
for (let hops = 0; cur && hops < 4; hops++, cur = cur.parent) {
|
||||||
|
if (cur.type === 'call') return cur;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|||||||
@@ -454,15 +454,18 @@ export class TreeSitterExtractor {
|
|||||||
// segment — the simple name Java/Kotlin code uses in `OtherClass::method`
|
// segment — the simple name Java/Kotlin code uses in `OtherClass::method`
|
||||||
// references.
|
// references.
|
||||||
const SIMPLE_NAME = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
const SIMPLE_NAME = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
||||||
const DOTTED_NAME = /^[A-Za-z_$][A-Za-z0-9_$.]*\.([A-Za-z_$][A-Za-z0-9_$]*)$/;
|
// JVM imports are dotted (`com.example.OtherClass`); PHP `use` imports
|
||||||
|
// are backslashed (`App\Services\Mailer`). Both contribute their last
|
||||||
|
// segment — the simple name code uses to reference them.
|
||||||
|
const QUALIFIED_IMPORT = /^[A-Za-z_$][A-Za-z0-9_$.\\]*[.\\]([A-Za-z_$][A-Za-z0-9_$]*)$/;
|
||||||
const importedNames = new Set<string>();
|
const importedNames = new Set<string>();
|
||||||
for (const r of this.unresolvedReferences) {
|
for (const r of this.unresolvedReferences) {
|
||||||
if (r.referenceKind !== 'imports') continue;
|
if (r.referenceKind !== 'imports') continue;
|
||||||
if (SIMPLE_NAME.test(r.referenceName)) {
|
if (SIMPLE_NAME.test(r.referenceName)) {
|
||||||
importedNames.add(r.referenceName);
|
importedNames.add(r.referenceName);
|
||||||
} else {
|
} else {
|
||||||
const dotted = r.referenceName.match(DOTTED_NAME);
|
const qualified = r.referenceName.match(QUALIFIED_IMPORT);
|
||||||
if (dotted) importedNames.add(dotted[1]!);
|
if (qualified) importedNames.add(qualified[1]!);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -497,7 +500,9 @@ export class TreeSitterExtractor {
|
|||||||
// (constant-expression context — see FnRefSpec.ungatedModes).
|
// (constant-expression context — see FnRefSpec.ungatedModes).
|
||||||
// - everything else: name ∈ same-file functions/methods ∪ imports.
|
// - everything else: name ∈ same-file functions/methods ∪ imports.
|
||||||
if (!c.name.startsWith('this.')) {
|
if (!c.name.startsWith('this.')) {
|
||||||
const skipGate = ungated?.has(c.mode) === true && atFileScope;
|
const skipGate =
|
||||||
|
(ungated?.has(c.mode) === true && atFileScope) ||
|
||||||
|
c.skipGate === true; // PHP HOF-position string callables (see FnRefCandidate.skipGate)
|
||||||
if (!skipGate) {
|
if (!skipGate) {
|
||||||
if (c.name.includes('::')) {
|
if (c.name.includes('::')) {
|
||||||
const scopeName = c.name.slice(0, c.name.indexOf('::'));
|
const scopeName = c.name.slice(0, c.name.indexOf('::'));
|
||||||
|
|||||||
+73
-31
@@ -1212,9 +1212,17 @@ export class ReferenceResolver {
|
|||||||
if (!member) return null;
|
if (!member) return null;
|
||||||
const fromNode = this.queries.getNodeById(ref.fromNodeId);
|
const fromNode = this.queries.getNodeById(ref.fromNodeId);
|
||||||
if (!fromNode) return null;
|
if (!fromNode) return null;
|
||||||
const sep = fromNode.qualifiedName.lastIndexOf('::');
|
// A hook declared at class-body level (Ruby `before_action :authenticate`)
|
||||||
if (sep <= 0) return null; // not inside a class scope
|
// attributes to the CLASS node itself — its qualified name IS the scope.
|
||||||
const classPrefix = fromNode.qualifiedName.slice(0, sep);
|
// For members, strip the member segment.
|
||||||
|
let classPrefix: string;
|
||||||
|
if (SUPERTYPE_BEARING_KINDS.has(fromNode.kind) || fromNode.kind === 'module') {
|
||||||
|
classPrefix = fromNode.qualifiedName;
|
||||||
|
} else {
|
||||||
|
const sep = fromNode.qualifiedName.lastIndexOf('::');
|
||||||
|
if (sep <= 0) return null; // not inside a class scope
|
||||||
|
classPrefix = fromNode.qualifiedName.slice(0, sep);
|
||||||
|
}
|
||||||
const candidates = this.context
|
const candidates = this.context
|
||||||
.getNodesByQualifiedName(`${classPrefix}::${member}`)
|
.getNodesByQualifiedName(`${classPrefix}::${member}`)
|
||||||
.filter(
|
.filter(
|
||||||
@@ -1260,38 +1268,72 @@ export class ReferenceResolver {
|
|||||||
const member = ref.referenceName.slice('this.'.length);
|
const member = ref.referenceName.slice('this.'.length);
|
||||||
const fromNode = this.queries.getNodeById(ref.fromNodeId);
|
const fromNode = this.queries.getNodeById(ref.fromNodeId);
|
||||||
if (!fromNode || !member) continue;
|
if (!fromNode || !member) continue;
|
||||||
const sep = fromNode.qualifiedName.lastIndexOf('::');
|
// Class-body-level hooks (Ruby) attribute to the CLASS node itself.
|
||||||
if (sep <= 0) continue;
|
let className: string;
|
||||||
const classPrefix = fromNode.qualifiedName.slice(0, sep);
|
if (SUPERTYPE_BEARING_KINDS.has(fromNode.kind) || fromNode.kind === 'module') {
|
||||||
const className = classPrefix.includes('::')
|
className = fromNode.name;
|
||||||
? classPrefix.slice(classPrefix.lastIndexOf('::') + 2)
|
} else {
|
||||||
: classPrefix;
|
const sep = fromNode.qualifiedName.lastIndexOf('::');
|
||||||
|
if (sep <= 0) continue;
|
||||||
|
const classPrefix = fromNode.qualifiedName.slice(0, sep);
|
||||||
|
className = classPrefix.includes('::')
|
||||||
|
? classPrefix.slice(classPrefix.lastIndexOf('::') + 2)
|
||||||
|
: classPrefix;
|
||||||
|
}
|
||||||
|
|
||||||
// BFS up the supertype graph by simple name.
|
// NODE-anchored BFS up the supertype graph: start from the class node
|
||||||
const seen = new Set<string>([className]);
|
// in the ref's own file (never a same-named class elsewhere — rails has
|
||||||
let frontier = this.context.getSupertypes?.(className, ref.language) ?? [];
|
// a dozen `Engine`s), follow implements/extends EDGES to supertype
|
||||||
|
// NODES, and look members up through `contains` edges. No name-based
|
||||||
|
// unions anywhere — a name-keyed getSupertypes('Engine') merged every
|
||||||
|
// Engine's parents and produced a cross-class wrong edge on rails.
|
||||||
|
let frontierNodes = this.context
|
||||||
|
.getNodesByName(className)
|
||||||
|
.filter(
|
||||||
|
(n) =>
|
||||||
|
SUPERTYPE_BEARING_KINDS.has(n.kind) &&
|
||||||
|
n.filePath === ref.filePath
|
||||||
|
);
|
||||||
|
if (frontierNodes.length === 0) {
|
||||||
|
// The class itself may be declared in another file (partial/reopened
|
||||||
|
// classes); fall back to same-family nodes of that name.
|
||||||
|
frontierNodes = this.context
|
||||||
|
.getNodesByName(className)
|
||||||
|
.filter(
|
||||||
|
(n) =>
|
||||||
|
SUPERTYPE_BEARING_KINDS.has(n.kind) &&
|
||||||
|
sameLanguageFamily(n.language, ref.language)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const seenNodes = new Set<string>(frontierNodes.map((n) => n.id));
|
||||||
let target: Node | null = null;
|
let target: Node | null = null;
|
||||||
for (let depth = 0; depth < 5 && frontier.length > 0 && !target; depth++) {
|
for (let depth = 0; depth < 5 && frontierNodes.length > 0 && !target; depth++) {
|
||||||
const next: string[] = [];
|
const next: Node[] = [];
|
||||||
for (const superName of frontier) {
|
for (const typeNode of frontierNodes) {
|
||||||
if (seen.has(superName)) continue;
|
for (const edge of this.queries.getOutgoingEdges(typeNode.id, ['implements', 'extends'])) {
|
||||||
seen.add(superName);
|
const superNode = this.queries.getNodeById(edge.target);
|
||||||
const members = this.context
|
if (!superNode || seenNodes.has(superNode.id)) continue;
|
||||||
.getNodesByName(member)
|
seenNodes.add(superNode.id);
|
||||||
.filter(
|
if (!SUPERTYPE_BEARING_KINDS.has(superNode.kind)) continue;
|
||||||
(n) =>
|
// Member lookup anchored on the supertype's contains edges.
|
||||||
(n.kind === 'function' || n.kind === 'method') &&
|
for (const c of this.queries.getOutgoingEdges(superNode.id, ['contains'])) {
|
||||||
sameLanguageFamily(n.language, ref.language) &&
|
const m = this.queries.getNodeById(c.target);
|
||||||
(n.qualifiedName === `${superName}::${member}` ||
|
if (
|
||||||
n.qualifiedName.endsWith(`::${superName}::${member}`))
|
m &&
|
||||||
);
|
m.name === member &&
|
||||||
if (members.length > 0) {
|
(m.kind === 'function' || m.kind === 'method') &&
|
||||||
target = members.reduce((a, b) => (a.startLine <= b.startLine ? a : b));
|
sameLanguageFamily(m.language, ref.language)
|
||||||
break;
|
) {
|
||||||
|
target = m;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (target) break;
|
||||||
|
next.push(superNode);
|
||||||
}
|
}
|
||||||
next.push(...(this.context.getSupertypes?.(superName, ref.language) ?? []));
|
if (target) break;
|
||||||
}
|
}
|
||||||
frontier = next;
|
frontierNodes = next;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (target) {
|
if (target) {
|
||||||
|
|||||||
@@ -192,12 +192,15 @@ export function matchFunctionRef(
|
|||||||
// A/B finding; same pattern in vendored docopt.py). Python's `self.m`
|
// A/B finding; same pattern in vendored docopt.py). Python's `self.m`
|
||||||
// form keeps method targets via its own capture shape. C++ likewise: a
|
// form keeps method targets via its own capture shape. C++ likewise: a
|
||||||
// bare identifier can only be a FREE function (member values need
|
// bare identifier can only be a FREE function (member values need
|
||||||
// `&Cls::method`). Other languages keep method targets: C# method groups,
|
// `&Cls::method`). PHP string callables name global FUNCTIONS (methods
|
||||||
// Swift/Dart implicit-self, Java/Kotlin method references.
|
// need the `[$obj, 'm']` array form, which carries its own shape). Other
|
||||||
|
// languages keep method targets: C# method groups, Swift/Dart
|
||||||
|
// implicit-self, Java/Kotlin method references.
|
||||||
const bareFnOnly =
|
const bareFnOnly =
|
||||||
ref.language === 'typescript' || ref.language === 'tsx' ||
|
ref.language === 'typescript' || ref.language === 'tsx' ||
|
||||||
ref.language === 'javascript' || ref.language === 'jsx' ||
|
ref.language === 'javascript' || ref.language === 'jsx' ||
|
||||||
ref.language === 'cpp' || ref.language === 'python';
|
ref.language === 'cpp' || ref.language === 'python' ||
|
||||||
|
ref.language === 'php';
|
||||||
|
|
||||||
// Qualified member-pointer (`&Widget::on_click` → "Widget::on_click"):
|
// Qualified member-pointer (`&Widget::on_click` → "Widget::on_click"):
|
||||||
// resolve the member ON THAT SCOPE — exempt from bareFnOnly (the `&Cls::m`
|
// resolve the member ON THAT SCOPE — exempt from bareFnOnly (the `&Cls::m`
|
||||||
|
|||||||
Reference in New Issue
Block a user