diff --git a/gitnexus/src/core/ingestion/languages/ruby/query.ts b/gitnexus/src/core/ingestion/languages/ruby/query.ts index 4e9494c41..f371448b8 100644 --- a/gitnexus/src/core/ingestion/languages/ruby/query.ts +++ b/gitnexus/src/core/ingestion/languages/ruby/query.ts @@ -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 diff --git a/gitnexus/src/core/ingestion/languages/ruby/simple-hooks.ts b/gitnexus/src/core/ingestion/languages/ruby/simple-hooks.ts index b09bd2616..5f6935587 100644 --- a/gitnexus/src/core/ingestion/languages/ruby/simple-hooks.ts +++ b/gitnexus/src/core/ingestion/languages/ruby/simple-hooks.ts @@ -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; }