mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-10 03:27:59 +00:00
fix(ruby): infer an instance variable's type from the constructor it calls
`@service = UserService.new` in `initialize` bound nothing, so `@service.inner` had no receiver type and the fold declined the whole chain — the Ruby half of #2807. An instance variable is the ONLY way a Ruby object gets a field, and Ruby has no annotations, so this was the single shape that could have worked and did not: the existing constructor-inferred patterns bind a local (`x = Foo.new`) and a constant (`SERVICE = Foo.new`), never an ivar. Adds the plain and `Foo::Bar` qualified ivar forms. `@type-binding.name` is captured on the `instance_variable` node so the bound name keeps its `@` sigil and matches the receiver text at the call site verbatim — the resolver compares spellings, and `service` would never have matched `@service`. `rubyBindingScopeFor` gains a Class hoist gated on a narrow `@type-binding.ivar-field` marker riding the same node: an ivar declares a field of the enclosing class, so the binding must live on the Class scope or no other method can see it. Gated on the dedicated marker, never on `@type-binding.constructor` at large, which also fires for `x = Foo.new` locals that must stay in their own method. Measured: `AssignedField.run` now emits BOTH chain links, exact parity with the local-const control. Refs #2807 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
6c99f5488c
commit
55d370793d
2 changed files with 39 additions and 1 deletions
|
|
@ -178,6 +178,32 @@ const RUBY_SCOPE_QUERY = `
|
|||
method: (identifier) @_new_method2
|
||||
(#eq? @_new_method2 "new"))) @type-binding.constructor
|
||||
|
||||
;; Instance-variable constructor: \`@service = UserService.new\` (#2807).
|
||||
;; The patterns above bind locals and constants; an instance variable — the
|
||||
;; only way a Ruby object gets a field at all — bound nothing, so \`@service.run\`
|
||||
;; had no receiver type and the fold declined the whole chain.
|
||||
;;
|
||||
;; \`@type-binding.name\` is captured on the \`instance_variable\` node, so the
|
||||
;; bound name keeps its \`@\` sigil and matches the receiver text at the call
|
||||
;; site verbatim. The narrow \`@type-binding.ivar-field\` marker rides the same
|
||||
;; node for \`rubyBindingScopeFor\` to hoist on; anchorCaptureFor takes the
|
||||
;; broadest range, so the assignment stays the anchor and the source stays
|
||||
;; \`constructor-inferred\`.
|
||||
|
||||
(assignment
|
||||
left: (instance_variable) @type-binding.name @type-binding.ivar-field
|
||||
right: (call
|
||||
receiver: (constant) @type-binding.type
|
||||
method: (identifier) @_new_ivar
|
||||
(#eq? @_new_ivar "new"))) @type-binding.constructor
|
||||
|
||||
(assignment
|
||||
left: (instance_variable) @type-binding.name @type-binding.ivar-field
|
||||
right: (call
|
||||
receiver: (scope_resolution) @type-binding.type
|
||||
method: (identifier) @_new_ivar_q
|
||||
(#eq? @_new_ivar_q "new"))) @type-binding.constructor
|
||||
|
||||
;; Constant constructor: SERVICE = UserService.new (left is constant, not identifier)
|
||||
|
||||
(assignment
|
||||
|
|
|
|||
|
|
@ -8,17 +8,29 @@ import type {
|
|||
NodeLabel,
|
||||
} from 'gitnexus-shared';
|
||||
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
||||
import { walkToScope } from '../typescript/simple-hooks.js';
|
||||
|
||||
export function rubyBindingScopeFor(
|
||||
decl: CaptureMatch,
|
||||
innermost: Scope,
|
||||
_tree: ScopeTree,
|
||||
tree: ScopeTree,
|
||||
): ScopeId | null {
|
||||
// Keep self typeBindings in the method's Function scope so
|
||||
// populateClassOwnedMembers can match Method defs to their receiver types.
|
||||
if (decl['@type-binding.self'] !== undefined) {
|
||||
return innermost.id;
|
||||
}
|
||||
// `@ivar = Foo.new` in `initialize` (or any method) declares a FIELD of the
|
||||
// enclosing class, so its type binding belongs on the Class scope — the only
|
||||
// place `typeOfMemberOnClass` reads it. Left on the method's own Function
|
||||
// scope it would be invisible to every other method (#2807).
|
||||
//
|
||||
// Gated on the marker that pattern emits, never on `@type-binding.constructor`
|
||||
// at large: that capture also fires for `x = Foo.new` locals, and hoisting
|
||||
// those to the class would leak a method local into every sibling method.
|
||||
if (decl['@type-binding.ivar-field'] !== undefined) {
|
||||
return walkToScope(innermost, tree, 'Class');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue