Skip to content

fix(ruby): method-level call resolution, HAS_METHOD edges, and dispatch table#278

Merged
magyargergo merged 4 commits intomainfrom
fix-ruby-method-class-resolutions
Mar 14, 2026
Merged

fix(ruby): method-level call resolution, HAS_METHOD edges, and dispatch table#278
magyargergo merged 4 commits intomainfrom
fix-ruby-method-class-resolutions

Conversation

@magyargergo
Copy link
Collaborator

@magyargergo magyargergo commented Mar 14, 2026

Summary

Continuation of #111 — fixes Ruby-specific gaps left after the initial Ruby language support was merged.

  • Replace all if (language === Ruby) checks in processors with a callRouters dispatch table in call-routing.ts (renamed from ruby-call-routing.ts)
  • Fix Ruby method-level CALLS attribution — findEnclosingFunction now recognizes method and singleton_method AST nodes
  • Fix Ruby HAS_METHOD edges — CLASS_CONTAINER_TYPES now includes class and module
  • Add bare call capture via tree-sitter query for Ruby methods called without parentheses
  • Add Ruby member call detection (call node with receiver field) to inferCallForm and extractReceiverName
  • Wire resolveRubyImport into resolveLanguageImport (was dead code)
  • Add ruby.test.ts to CI integration workflow

Resolution gaps fixed (from code review)

  • Singleton method label mismatch (Bug): @definition.function@definition.method for singleton_method — CALLS edges from def self.foo bodies now resolve correctly
  • attr_ Properties missing ownerId/HAS_METHOD:* Property nodes from attr_accessor/attr_reader/attr_writer now get findEnclosingClassId + HAS_METHOD edges in both parse-worker and call-processor
  • extend vs include vs prepend distinction: heritageKind field added to RubyHeritageItem, propagated through heritage pipeline as IMPLEMENTS edge reason ('include', 'extend', 'prepend')
  • Bare call over-capture: Documented limitation of body_statement (identifier) query in tree-sitter comment
  • Bare require test coverage: Added non-relative require 'lib/...' import to fixture + test assertion
  • prepend test coverage: Added prepend Cacheable with distinct module fixture + test assertion

Deferred to #274

  • User.new constructor calls not linked to User class — requires constructor-dispatch type inference infrastructure
  • self.method() receiver not resolved to enclosing class — requires self → enclosing class type mapping in lookupTypeEnv

Test plan

  • 31 integration tests across 5 suites — all passing
    • Heritage & property resolution (21 tests): classes, modules, methods, singleton methods, imports, include/extend/prepend with distinct reasons, properties with HAS_METHOD, member calls, bare calls, bare require, graph integrity
    • Arity filtering (1 test): disambiguation by argument count
    • Member-call resolution (3 tests): obj.method(), HAS_METHOD edges
    • Ambiguous disambiguation (4 tests): same-name classes, import-based resolution
    • Local shadow (1 test): same-file priority over imports
  • Full test suite: no regressions across other language resolvers (Rust trait-impl reason preserved)
  • TypeScript compiles clean (tsc --noEmit)

Files changed

Area Files Change
Dispatch table call-routing.ts (CLI + web) Renamed from ruby-call-routing.ts, added callRouters Record, heritageKind on RubyHeritageItem
Processors call-processor.ts, parse-worker.ts, import-processor.ts (CLI + web) Replaced if (Ruby) with dispatch, properties get ownerId + HAS_METHOD
Heritage heritage-processor.ts Handles include/extend/prepend kinds alongside trait-impl
Ruby AST support utils.ts Added method/singleton_method to FUNCTION_NODE_TYPES, class/module to CLASS_CONTAINER_TYPES, Ruby member call detection
Bare calls tree-sitter-queries.ts singleton_method@definition.method, (body_statement (identifier)) query with over-capture docs
Import resolution import-processor.ts Wired resolveRubyImport into resolveLanguageImport
CI ci-integration.yml Added ruby.test.ts to pipeline and coverage jobs
Tests ruby.test.ts + 7 fixture files 31 integration tests

Compound Engineered

…ch table refactoring

- Replace all `if (language === Ruby)` checks in processors with a
  `callRouters` dispatch table in call-routing.ts (renamed from
  ruby-call-routing.ts to preserve git history)
- Add Ruby `method` and `singleton_method` to FUNCTION_NODE_TYPES so
  findEnclosingFunction produces Method-level CALLS sources
- Add Ruby `class` and `module` to CLASS_CONTAINER_TYPES for HAS_METHOD
  edge generation
- Add bare call capture via tree-sitter query `(body_statement (identifier))`
  for Ruby methods called without parentheses
- Add Ruby member call detection (`call` node with `receiver` field) to
  inferCallForm and extractReceiverName
- Wire resolveRubyImport into resolveLanguageImport
- Add 24 integration tests across 5 suites: heritage/properties, arity
  filtering, member calls, ambiguous disambiguation, local shadow
- Add ruby.test.ts to CI integration workflow
@vercel
Copy link

vercel bot commented Mar 14, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
gitnexus Ready Ready Preview, Comment Mar 14, 2026 11:27am

Request Review

@magyargergo
Copy link
Collaborator Author

@claude review these changes and find gaps in ruby resolution strategies.

@claude
Copy link

claude bot commented Mar 14, 2026

Claude finished @magyargergo's task in 3m 56s —— View job


Ruby Resolution Strategy Review

  • Gather context and understand the request
  • Read changed files (call-routing, utils, processors, test fixtures)
  • Analyze Ruby resolution strategy gaps
  • Post detailed findings

Good structural work here — the dispatch table refactor is clean and the new test coverage is solid. That said, a review of the resolution pipeline surfaces several gaps worth addressing.


1. Singleton method label mismatch — CALLS attribution broken for def self.foo [Bug]

Files: gitnexus/src/core/ingestion/tree-sitter-queries.ts:519-521, gitnexus/src/core/ingestion/utils.ts:432-437

The tree-sitter query captures singleton methods as @definition.function:

(singleton_method name: (identifier) @name) @definition.function

getLabelFromCaptures therefore returns 'Function', storing the node with generateId('Function', 'file:name').

But extractFunctionName (used in findEnclosingFunctionId / findEnclosingFunction) returns label: 'Method' for singleton_method:

} else if (node.type === 'method' || node.type === 'singleton_method') {
  funcName = nameNode?.text;
  label = 'Method';  // ← wrong: stored as 'Function'
}

Impact: Any CALLS edge originating inside a def self.foo body computes sourceId = generateId('Method', 'file:foo'), but the actual node is generateId('Function', 'file:foo'). The sourceId points to a non-existent node. No test exercises calls inside singleton methods, so this is currently silent.

Fix: Either change extractFunctionName to return label: 'Function' for singleton_method, or change the query to use @definition.method. The query change is more consistent with the label semantics. Fix this →


2. User.new constructor calls not linked to User class [Medium gap]

File: gitnexus/src/core/ingestion/call-processor.ts:387-399, gitnexus/src/core/ingestion/utils.ts:759-762

inferCallForm correctly identifies User.new as a member call (receiver = User). But lookupTypeEnv is designed for local variables with explicit type annotations (user: User). Since User is a bare constant (a class name), not a local variable, receiverTypeName stays undefined.

resolveCallTarget then falls through to resolving new by name alone, which finds nothing (no def new is ever defined — constructors in Ruby are def initialize). The User.new call silently drops.

Consequence: Constructor calls don't contribute to the call graph at all for Ruby — no CALLS edge from create_user to User or User#initialize. The test fixture in ruby-app/lib/service.rb has User.new but no test asserts a CALLS edge from it.

Fix options:

  • Post-process callForm === 'member' && calledName === 'new' to reclassify as constructor with receiverTypeName = receiverName
  • Or in resolveCallTarget, when calledName === 'new', treat receiverName directly as the type name to look up

3. attr_* Property nodes missing ownerId / HAS_METHOD edges [Medium gap]

Files: gitnexus/src/core/ingestion/workers/parse-worker.ts:892-924, gitnexus/src/core/ingestion/call-processor.ts:158-179

Both parse-worker (worker path) and call-processor (direct path) route attr_accessor to the 'properties' branch which creates Property nodes without walking up the AST to find the enclosing class:

// parse-worker.ts ~line 912
result.symbols.push({
  filePath: file.path,
  name: item.propName,
  nodeId,
  type: 'Property',
  // ← no ownerId!
});
// no HAS_METHOD relationship emitted

The regular node-creation path (for method/function nodes) does call findEnclosingClassId and emit HAS_METHOD. The properties routing branch skips all of that.

Impact: user.id, user.name etc. can't be resolved as member calls via the ownerId filter because the properties aren't linked to User. The test asserts Property nodes exist but doesn't assert HAS_METHOD to the enclosing class.


4. Bare call query over-capture [Low/Medium noise]

File: gitnexus/src/core/ingestion/tree-sitter-queries.ts:527-529

(body_statement
  (identifier) @call.name @call)

body_statement (identifier) matches any identifier as a direct child of a body statement — including variable reads (x), constants used as values, true/false/nil, etc. For example:

def foo
  result = compute()
  result  # ← bare identifier, captured as @call
end

result here is a variable reference, not a method call. While isBuiltInOrNoise and the absence of a matching symbol definition suppress most of these, they add noise and risk false CALLS edges for any local variable name that coincidentally matches a method name elsewhere.

A tighter query would restrict to identifiers not preceded by an assignment:

(body_statement (identifier) @call.name @call)  ; already exists

vs. checking whether the identifier has a parent assignment target, but that requires post-processing. At minimum, documenting this limitation would help. Fix this →


5. extend vs. include semantic distinction lost [Low]

File: gitnexus/src/core/ingestion/call-routing.ts:106-127

Both include and extend are routed to { kind: 'heritage' } with kind: 'trait-impl', producing identical IMPLEMENTS edges. In Ruby:

  • include Mod → instance methods from Mod become instance methods
  • extend Mod → instance methods from Mod become class methods

These are semantically different mixins. If the graph ever uses IMPLEMENTS for member-call resolution filtering (e.g., resolving user.serialize_data through User's included modules), conflating extend with include could produce incorrect results for class-level method calls.


6. self.method() calls not resolved to enclosing class [Low/Medium]

File: gitnexus/src/core/ingestion/utils.ts:823-825

extractReceiverName correctly returns 'self' (since 'self' is in SIMPLE_RECEIVER_TYPES). But lookupTypeEnv treats 'self' as a local variable name and won't find it mapped to the enclosing class. receiverTypeName stays undefined, so the method lookup proceeds without class-owner filtering.

For def foo; self.bar; end, bar would resolve the same as a bare bar() call — no special handling of self as the owning class. No test covers self.method() member calls.


7. Missing coverage: bare require of local files [Low]

File: gitnexus/test/integration/resolvers/ruby.test.ts

resolveRubyImport handles both require 'lib/utils' and require_relative './utils'. The test suite only exercises require_relative. A fixture with require 'lib/user' (no relative prefix) would confirm the suffix-based resolution works for non-relative requires too.


8. prepend routing not tested [Low]

File: gitnexus/src/core/ingestion/call-routing.ts:106

prepend is listed alongside include/extend in the routing logic but has no integration test. While the routing path is the same, an explicit test would prevent regressions if prepend semantics are ever handled differently.


Summary

Gap Severity File
Singleton method label mismatch (def self.foo → CALLS broken) Bug tree-sitter-queries.ts, utils.ts:432
User.new not resolved to User class Medium call-processor.ts:387
attr_* Properties missing ownerId / HAS_METHOD Medium parse-worker.ts:912, call-processor.ts:161
Bare call over-capture (body_statement identifier) Low/Medium tree-sitter-queries.ts:527
extend vs include semantic distinction Low call-routing.ts:106
self.method() receiver not resolved to class Low/Medium utils.ts:823
Bare require of local files untested Low ruby.test.ts
prepend untested Low ruby.test.ts

The singleton method bug (#1) and missing ownerId for properties (#3) are the highest priority. The User.new gap (#2) is architecturally interesting since Ruby's constructor dispatch doesn't map cleanly to the constructor detection used by other languages.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 14, 2026

CI Report

All checks passed

Pipeline Status

Stage Status Details
✅ Typecheck success tsc --noEmit
✅ Unit Tests success 3 platforms
✅ Integration success 3 OS x 4 groups = 12 jobs

Test Results

1672 passed
· 514 suites · 1672 total
· ⏱️ 37s
· 📊 1151 unit + 521 integration

Code Coverage

Combined (Unit + Integration)

Metric Coverage Covered Threshold Status
Statements 48.4% 3800/7851 26% 🟢 █████████░░░░░░░░░░░
Branches 40.64% 2224/5472 23% 🟢 ████████░░░░░░░░░░░░
Functions 50.6% 418/826 28% 🟢 ██████████░░░░░░░░░░
Lines 49.8% 3521/7070 27% 🟢 █████████░░░░░░░░░░░
Coverage breakdown by test suite

Unit Tests

Metric Coverage Covered Threshold Status
Statements 37.29% 2928/7851 26% 🟢 ███████░░░░░░░░░░░░░
Branches 32.1% 1757/5472 23% 🟢 ██████░░░░░░░░░░░░░░
Functions 38.74% 320/826 28% 🟢 ███████░░░░░░░░░░░░░
Lines 38.64% 2732/7070 27% 🟢 ███████░░░░░░░░░░░░░

Integration Tests

Metric Coverage Covered Threshold Status
Statements 24.5% 1924/7851 26% 🔴 ████░░░░░░░░░░░░░░░░
Branches 19.27% 1055/5472 23% 🔴 ███░░░░░░░░░░░░░░░░░
Functions 26.63% 220/826 28% 🔴 █████░░░░░░░░░░░░░░░
Lines 25.17% 1780/7070 27% 🔴 █████░░░░░░░░░░░░░░░
Coverage thresholds are auto-ratcheted — they only go up

Vitest thresholds.autoUpdate bumps the floor whenever local coverage exceeds it.
CI enforces the current thresholds; developers commit the ratcheted values.


📋 View full run · Generated by CI

- Fix singleton_method label mismatch: @definition.function → @definition.method
  so CALLS edges from `def self.foo` bodies get correct sourceId
- Add ownerId and HAS_METHOD edges to attr_* Property nodes by calling
  findEnclosingClassId in both parse-worker and call-processor property branches
- Distinguish include/extend/prepend heritage: add heritageKind to
  RubyHeritageItem, propagate through heritage pipeline as IMPLEMENTS reason
- Document bare call over-capture limitation in tree-sitter query comment
- Add bare `require` (non-relative) import test coverage
- Add prepend/extend test coverage with distinct Loggable/Cacheable modules

31 Ruby integration tests passing, no regressions in other language resolvers.
@magyargergo
Copy link
Collaborator Author

@claude review my changes since last time and take into account that some items have been defferred and mentioned in the PRs message

@claude
Copy link

claude bot commented Mar 14, 2026

Claude finished @magyargergo's task in 5m 55s —— View job


Ruby Resolution Strategy — Follow-up Review

  • Gather context and read changed files
  • Review fixes for previously-identified gaps (accounting for deferred items)
  • Check for any new issues introduced
  • Post findings

All six non-deferred gaps from the previous review are addressed. Two items (User.new constructor dispatch, self.method() receiver resolution) are properly deferred to #274 and clearly documented. No regressions against the previous state. Three new observations below.


Previously-identified gaps — status

Gap Status
Singleton method label mismatch (def self.foo CALLS broken) ✅ Fixed — @definition.method in query + extractFunctionName handles singleton_method with label: 'Method'. New test exercises CALLS from factory → run_validations.
attr_* Properties missing ownerId / HAS_METHOD ✅ Fixed — CLI call-processor.ts and parse-worker.ts both now call findEnclosingClassId and emit HAS_METHOD. Tests assert edges for User → name and BaseModel → id.
extend / include / prepend semantic distinction ✅ Fixed — heritageKind field on RubyHeritageItem, propagated through heritage pipeline as IMPLEMENTS reason. Tests assert reason: 'include', reason: 'extend', reason: 'prepend' independently.
Bare call over-capture ✅ Addressed — clear comment added above the body_statement (identifier) query explaining the limitation and suppression strategy.
Bare require test coverage ✅ Added — require 'lib/concerns/serializable' in base_model.rb with assertion that checks both source and target file paths.
prepend test coverage ✅ Added — prepend Cacheable with a distinct Cacheable module fixture; assertion checks reason: 'prepend'.
User.new → constructor link ⏩ Deferred to #274
self.method() → enclosing class ⏩ Deferred to #274

New observations

1. Web call-processor.ts heritage reason still hardcoded to 'trait-impl' [Bug]

File: gitnexus-web/src/core/ingestion/call-processor.ts:221-224

The CLI call-processor.ts correctly propagates item.heritageKind as the IMPLEMENTS reason. The web package did not receive the same fix:

// gitnexus-web — still hardcoded
const relId = generateId('IMPLEMENTS', `${childId}->${parentId}`);
graph.addRelationship({
  id: relId, sourceId: childId, targetId: parentId,
  type: 'IMPLEMENTS', confidence: 1.0, reason: 'trait-impl',  // ← wrong for include/extend/prepend
});

The web package will emit reason: 'trait-impl' for all Ruby include/extend/prepend relationships, and will also use the old edge ID format (missing the :${kind} suffix). Fix this →


2. Web call-processor.ts properties still missing ownerId and HAS_METHOD [Parity gap]

File: gitnexus-web/src/core/ingestion/call-processor.ts:244

The fix for attr_* properties (ownerId, HAS_METHOD edges) landed in the CLI packages but was not applied to the web package:

// gitnexus-web — no ownerId, no HAS_METHOD
symbolTable.add(file.path, item.propName, nodeId, 'Property');
// no HAS_METHOD relationship emitted

vs. the CLI fix which calls findEnclosingClassId and emits HAS_METHOD. Property member-call resolution (e.g. user.name) will not work correctly in the web build. Fix this →


3. ExtractedHeritage.kind JSDoc comment is stale [Minor]

File: gitnexus/src/core/ingestion/workers/parse-worker.ts:108

/** 'extends' | 'implements' | 'trait-impl' */
kind: string;

Should now include 'include' | 'extend' | 'prepend'. Not a runtime issue since the type is string, but stale documentation will confuse future readers. Fix this →


Notes

  • Double-processing of require: Parse-worker and import-processor both route Ruby require calls independently. No duplicate edges result because addImportEdge uses Map<string, Set<string>> which deduplicates by path pair. Safe as-is.
  • extractReceiverName Ruby block: Checking parent.type === 'call' before parent.childForFieldName('receiver') is correct and safe. For Ruby call nodes, nameNode.parent is always the call node, so the guard is slightly redundant but harmless.
  • callRouter with bare-call identifier nodes: When the (body_statement (identifier)) query fires, captureMap['call'] is the bare identifier. routeRubyCall passes through all if branches (none match require/include/attr_*) and returns CALL_RESULT immediately — no callNode.parent access for non-matching call names. ✅

Summary: The structural work is solid and all non-deferred previous review items are correctly fixed. Items 1 and 2 above are the only actionable gaps: the web package needs its call-processor.ts updated to match the CLI fixes for heritage reasons and property ownerId/HAS_METHOD.

…, singleton_method label

- Web call-processor: use item.heritageKind as IMPLEMENTS reason instead of
  hardcoded 'trait-impl', add :${kind} suffix to edge ID for uniqueness
- Web call-processor: port findEnclosingClassId, add HAS_METHOD edges for
  attr_* Property nodes to match CLI fix
- Web call-processor: singleton_method label 'Function' → 'Method' to match
  CLI tree-sitter query fix
- CLI parse-worker: update stale ExtractedHeritage.kind JSDoc to include
  'include' | 'extend' | 'prepend'
Web package was missing HAS_METHOD in the RelationshipType union,
causing a type mismatch with the HAS_METHOD edges emitted by the
attr_* property fix in call-processor.ts.
@magyargergo
Copy link
Collaborator Author

@claude review my changes since last time and take into account that some items have been defferred and mentioned in the PRs message

@claude
Copy link

claude bot commented Mar 14, 2026

Claude finished @magyargergo's task in 4m 3s —— View job


Ruby Resolution Strategy — Third Review

  • Gather context and read changed files
  • Check status of 3 items from previous review
  • Scan for new gaps
  • Post findings

All three items from the second review are addressed in commits fcfa665 and 789484e. No actionable new gaps. Two deferred items (User.new constructor, self.method() receiver) remain correctly parked in #274.


Items from second review — status

Gap Status
Web heritage reason hardcoded to 'trait-impl' ✅ Fixed — item.heritageKind used as reason; edge ID gets :${kind} suffix (fcfa665)
Web attr_* properties missing ownerId + HAS_METHOD ✅ Fixed — findEnclosingClassId added inline to web package, HAS_METHOD edges emitted (fcfa665)
ExtractedHeritage.kind JSDoc stale ✅ Fixed — comment now includes 'include' | 'extend' | 'prepend' (fcfa665)
HAS_METHOD missing from web RelationshipType union ✅ Fixed — required follow-up from property fix (789484e)
User.new constructor dispatch ⏩ Deferred to #274
self.method() receiver resolution ⏩ Deferred to #274

New observations

1. Heritage fallback generates wrong label for Ruby classes [Low / pre-existing]

File: gitnexus/src/core/ingestion/heritage-processor.ts:302-304

} else if (h.kind === 'trait-impl' || h.kind === 'include' || h.kind === 'extend' || h.kind === 'prepend') {
  const structId = symbolTable.lookupExact(h.filePath, h.className) ||
                   resolveSymbol(...)?.nodeId ||
                   generateId('Struct', `${h.filePath}:${h.className}`);  // ← 'Struct', not 'Class'

If a Ruby class (or module) is somehow missing from the symbol table, the fallback ID is generateId('Struct', ...) rather than generateId('Class', ...), which won't match the actual node. In practice, Ruby classes are registered during parse, so the lookupExact arm fires first and this fallback is never reached. The same pattern exists for trait-impl and predates this PR — not a regression, just worth noting.

2. Web symbolTable.add doesn't receive ownerId for properties [Negligible]

File: gitnexus-web/src/core/ingestion/call-processor.ts:278

symbolTable.add(file.path, item.propName, nodeId, 'Property');
// CLI version passes: { ownerId: propEnclosingClassId }

The web resolver doesn't implement receiver-type filtering (it uses a simpler three-tier lookup with no ownerId step), so the missing ownerId has no effect on resolution quality. The HAS_METHOD edge is emitted correctly for graph completeness. Acceptable parity gap given the architectural difference.

3. callRouters dispatch missing Kotlin in web package [Informational]

File: gitnexus-web/src/core/ingestion/call-routing.ts:30-43

The CLI callRouters includes [SupportedLanguages.Kotlin]: noRouting while the web version doesn't. TypeScript's exhaustive Record<SupportedLanguages, CallRouter> check would catch this if the web SupportedLanguages enum includes Kotlin — CI passes clean, so the web package intentionally omits Kotlin support. No action needed.


Summary

The patch is solid. All second-review items are correctly resolved with no regressions introduced. The singleton_method label fix now correctly propagates through both CLI and web findEnclosingFunction. The heritageKind field flows cleanly from call-routing.tscall-processor.tsheritage-processor.ts with correct reason values in all three execution paths (CLI worker, CLI direct, web). Ready to merge pending the deferred #274 work.

@magyargergo magyargergo merged commit 6e38db8 into main Mar 14, 2026
22 checks passed
@magyargergo magyargergo deleted the fix-ruby-method-class-resolutions branch March 14, 2026 11:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant