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
@@ -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` | — |
|
||||
| Lua / Luau | `arguments` | `assignment_statement` (`expression_list.value`) | `field.value` (keyed + positional) | (same) | — |
|
||||
| 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)
|
||||
|
||||
@@ -199,8 +200,18 @@ Index cost on redis: +6% time, +5% db size.
|
||||
imports, so cross-file bare callbacks only resolve when repo-unique
|
||||
(functions; methods are enclosing-type-only). Cross-TYPE `#selector`
|
||||
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**
|
||||
(`resolveDeferredThisMemberRefs`, depth-capped BFS over implements/extends,
|
||||
runs after edges persist — same lifecycle as the #750 conformance pass).
|
||||
|
||||
Reference in New Issue
Block a user