mirror of
https://github.com/colbymchenry/codegraph.git
synced 2026-10-25 03:49:59 +08:00
fix(ruby): a call on a namespaced class links its caller to the class and its method (#2827)
A Ruby call on a constant written with its namespace, like Billing::ChargeCard.call(...) or ::Foo.call, recorded no edge. Both extractors (the kernel's ruby.rs and the wasm one) emitted the receiver's references ref only for a bare `constant` receiver, so a scope_resolution receiver got none. The call ref itself, Billing::ChargeCard.call, matched no resolver strategy either: the method-call patterns reject a `::` in the receiver. A class used only through such calls showed no callers and no dependents. Fix: - Both engines treat a constant path (a constant, or a scope_resolution of constants, a leading `::` included) as a constant receiver and emit the references ref under the receiver as written. A path through a value (obj::Thing, self.class::LIMIT) still emits none, and .new stays an instantiation. - A new resolver step, src/resolution/ruby-constant-paths.ts, resolves a Ruby references ref written as such a path, and a calls ref on one, the way Ruby looks the constant up: the class or module around the reference, then each namespace outside it, then the top level; a `::`-rooted path at the top level only. A call reaches the class's or module's own method, else the first one its ancestry declares (rubyAncestry, now exported). The step runs before the pre-filter, which dropped `::`-rooted names, and it is exclusive: a path the project doesn't declare (ActiveRecord::Base) or a class without the method links nothing, never a namesake by its last name, and production code never reaches a class only a test declares. Validation: - New __tests__/ruby-namespaced-receiver.test.ts (8 cases, each on the kernel and on wasm, plus a sync case) and a parity case in kernel-ruby-parity.test.ts. On main 16 of the 22 tests in the two files fail (the rest are guards and the old parity cases); all pass with the fix. Reverting only the resolver step, or only the wasm extractor, turns 13 and 10 of them red. - A/B against main (kernel path), every edge triaged: sinatra +23 edges, rubocop +168, mastodon's Ruby tree (4,363 files) +676; 0 removed, nodes identical. 866 of 867 are right: references by the path as written or under an enclosing namespace (each such case checked against the call site's own nesting), calls to a class method, a module_function method, or an inherited class method. 1 is wrong: Paperclip::Transcoder.make reaches the instance method `make`, since a singleton method and an instance method share one qualified name today; a bare Transcoder.make does the same on main. Controls express and flask are byte-identical. - kernel-parity.mjs --lang ruby: sinatra 147/147, rubocop 1799/1801 (the same 2 deferred on main), mastodon 3333/3333, 0 diffs. The fix arm's kernel and wasm graphs are byte-identical on sinatra and rubocop. - Index time, median of 3 interleaved runs: +0.4% to +1.7%, the same as the controls (+0.8%, +1.3%). Fixes #2503 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5b47bc4963
commit
dcb985f1d2
@@ -820,10 +820,12 @@ impl<'t> Walker<'t> {
|
||||
};
|
||||
self.push_ref(caller, &callee, calls_kind, line, col);
|
||||
|
||||
// Capitalized constant receiver (`Klass.static_call`, `RETRY_MAX.times`)
|
||||
// → an ADDITIONAL references ref at the RECEIVER's position.
|
||||
// (scope_resolution receivers get none — type ≠ constant.)
|
||||
if !skip && receiver.kind() == "constant" {
|
||||
// Capitalized constant receiver (`Klass.static_call`, `RETRY_MAX.times`),
|
||||
// or one written with its namespace (`Billing::ChargeCard.call`,
|
||||
// `::Foo.call` — #2503) → an ADDITIONAL references ref at the
|
||||
// RECEIVER's position, under the receiver as written. A path through
|
||||
// a value (`obj::Thing`, `self.class::LIMIT`) names no constant.
|
||||
if !skip && is_constant_path(receiver) {
|
||||
self.push_ref_at(
|
||||
caller,
|
||||
&receiver_name.to_string(),
|
||||
@@ -1121,6 +1123,19 @@ impl<'t> Walker<'t> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A constant, or a `scope_resolution` path of constants (`A::B::C`, a
|
||||
/// top-level `::A` / `::A::B`): what Ruby reads as a constant (isRubyConstantPath).
|
||||
fn is_constant_path(node: Node) -> bool {
|
||||
match node.kind() {
|
||||
"constant" => true,
|
||||
"scope_resolution" => {
|
||||
node.child_by_field_name("name").map_or(false, |n| n.kind() == "constant")
|
||||
&& node.child_by_field_name("scope").map_or(true, is_constant_path)
|
||||
}
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// The Ruby `call` node whose argument_list (or keyword pair) contains `node`
|
||||
/// — nearest `call` ancestor within 4 parent hops (function-ref.ts:837).
|
||||
fn ruby_enclosing_call(node: Node) -> Option<Node> {
|
||||
|
||||
Reference in New Issue
Block a user