mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-09 22:33:39 +00:00
* feat(ruby): migrate Ruby to scope-based resolution (RFC #909 Ring 3) Implement the full scope-resolution pipeline for Ruby following the PR #1639 (Rust migration) standard, targeting registration in MIGRATED_LANGUAGES with 100% scope parity. Scope resolver hooks (languages/ruby/): - query.ts: RUBY_SCOPE_QUERY covering scopes, declarations, imports, type-bindings (constructor inference via .new), and references - captures.ts: emitRubyScopeCaptures orchestrator with import decomposition, receiver-binding synthesis, method reclassification, and arity metadata for both declarations and calls - receiver-binding.ts: self type-binding synthesis for instance methods, singleton methods, and class << self blocks - interpret.ts: interpretRubyImport (wildcard semantics) and interpretRubyTypeBinding (YARD, constructor, alias sources) - import-target.ts: resolveRubyImportTarget adapting the existing suffix resolver for require/require_relative/load - merge-bindings.ts: tier-based shadowing (local > namespace > import) - arity.ts: Ruby arity check with *args/**kwargs/&block support - scope-resolver.ts: rubyScopeResolver with custom buildRubyMro (kind-aware IMPLEMENTS partitioning: prepend > direct > include; extend excluded from instance MRO per legacy semantics) - simple-hooks.ts: bindingScopeFor, importOwningScope, receiverBinding Wiring: - ruby.ts provider gains 7 scope-resolution hooks - Registered in SCOPE_RESOLVERS map and MIGRATED_LANGUAGES - 127 legacy tests wired with createResolverParityIt('ruby') - 27 new scope-specific tests in ruby-scope.test.ts Parity: 89/127 legacy tests pass under registry-primary; 38 are heritage/property/YARD gaps expected in V1. All 127 pass under legacy. Closes #931 * feat(ruby): add emitHeritageEdges hook, YARD parsing, bare calls, property emission Extend the scope-resolution pipeline with a new optional `emitHeritageEdges` hook (ScopeResolver contract + run.ts wiring) that runs between `preEmitInheritanceEdges` and `buildMro`. This lets languages whose heritage declarations are syntactic method calls (Ruby include/extend/prepend) emit IMPLEMENTS edges from the scope-resolver without touching the legacy pipeline. Ruby scope-resolution improvements: - Heritage: intercept include/extend/prepend in captures.ts, encode as special imports, emit IMPLEMENTS edges via emitHeritageEdges hook - Properties: intercept attr_accessor/attr_reader/attr_writer, emit Property nodes + HAS_PROPERTY edges via the same hook - Bare calls: add (body_statement (identifier)) capture to scope query, matching the legacy query pattern for zero-arity method calls - YARD parsing: second-pass comment scanner for @param/@return/@type annotations with findFollowingMethod that handles body_statement nesting - Query fixes: @declaration.trait for modules (was @declaration.module which normalizeNodeLabel didn't recognize), constant constructor bindings (SERVICE = UserService.new), call-return inference Parity: 114/127 legacy tests pass under registry-primary (up from 89). Remaining 13 are advanced type-inference chain resolution (compound receiver, cross-file return-type propagation, for-in element types). * feat(ruby): achieve 100% scope-resolution parity (127/127) Fix all 13 remaining type-inference failures: - Add expandsWildcardTo hook (expandRubyWildcardNames) so finalize can materialize individual bindings from require/require_relative wildcard imports, unblocking cross-file return-type propagation - Add member-call-return type binding synthesis in captures.ts for assignments like `x = obj.method()` — enables compound receiver chaining through member call return types - Add YARD @return support for attr_accessor/attr_reader/attr_writer calls, creating field-type bindings for chain resolution - Add @declaration.property captures alongside __property__ imports so properties register in localDefs → model.fields → write-access - Add constructor-return inference for methods ending with Foo.new() - Add for-loop variable type aliasing in scope query - Rebuild nodeLookup after emitHeritageEdges in run.ts so Property nodes created by the heritage hook are visible to downstream passes - Extend compound-receiver resolver to handle compound member-call rawNames with () and increase max depth from 4 to 8 - Extend receiver-bound-calls Case 3b for compound rawNames All 127 legacy Ruby tests pass under both REGISTRY_PRIMARY_RUBY=0 (legacy) and =1 (registry-primary). Ruby is now fully registered in MIGRATED_LANGUAGES with 100% scope parity. * test(ruby): add pipeline benchmark exercising heritage emission Synthetic Ruby codebases at 100/250/500 files with include + extend + prepend mixins, diamond mixin patterns (shared BaseMixin modules), attr_accessor properties, YARD annotations, and cross-file imports. Strict equality assertions verify exact IMPLEMENTS and HAS_PROPERTY edge counts: 4 IMPLEMENTS per class (include x2, extend, prepend) plus 1 per non-base mixin module, 3 HAS_PROPERTY per class. Dedup in emitRubyMixinEdges prevents double-counting when the worker path (repos >= 15 files) already created Property/IMPLEMENTS edges before scope-resolution runs. Scaling: 0.76x and 1.40x (both linear, well under 3x threshold). * ci: retrigger build * fix(ci): resolve format, registry-primary-flag, and sequential-mixin test failures - Run prettier on all changed files (captures.ts, run.ts, ruby-scope.test.ts, ruby.test.ts, ruby-pipeline-benchmark.test.ts) - Update registry-primary-flag.test.ts: use Swift (not in MIGRATED_LANGUAGES) instead of Ruby for the isolation and env-var mutation tests - Pin ruby-sequential-mixin.test.ts to REGISTRY_PRIMARY_RUBY=0 (legacy mode) since it tests inferImplicitReceiver + selectDispatch hooks that live in the legacy call-processor (gated off under registry-primary) --------- Co-authored-by: Test <test@example.com>
481 lines
13 KiB
TypeScript
481 lines
13 KiB
TypeScript
/**
|
|
* Ruby scope-resolution integration tests (U8).
|
|
*
|
|
* These tests run with REGISTRY_PRIMARY_RUBY=true to exercise the
|
|
* scope-based resolution path. They validate class methods, module mixins,
|
|
* singleton methods, require_relative imports, constructor inference,
|
|
* block scope, class inheritance, and super resolution.
|
|
*/
|
|
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
|
|
import path from 'path';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import {
|
|
getRelationships,
|
|
getNodesByLabel,
|
|
runPipelineFromRepo,
|
|
type PipelineResult,
|
|
} from './helpers.js';
|
|
|
|
function writeFixtureRepo(root: string, files: Record<string, string>): void {
|
|
for (const [rel, content] of Object.entries(files)) {
|
|
const abs = path.join(root, rel);
|
|
fs.mkdirSync(path.dirname(abs), { recursive: true });
|
|
fs.writeFileSync(abs, content, 'utf8');
|
|
}
|
|
}
|
|
|
|
let savedEnv: string | undefined;
|
|
|
|
beforeAll(() => {
|
|
savedEnv = process.env['REGISTRY_PRIMARY_RUBY'];
|
|
process.env['REGISTRY_PRIMARY_RUBY'] = 'true';
|
|
});
|
|
|
|
afterAll(() => {
|
|
if (savedEnv === undefined) delete process.env['REGISTRY_PRIMARY_RUBY'];
|
|
else process.env['REGISTRY_PRIMARY_RUBY'] = savedEnv;
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 1. Basic class method resolution
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Ruby scope: basic class method resolution', () => {
|
|
let result: PipelineResult;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ruby-scope-basic-'));
|
|
writeFixtureRepo(tmpDir, {
|
|
'models/user.rb': `
|
|
class User
|
|
def save
|
|
true
|
|
end
|
|
|
|
def greet
|
|
"hello"
|
|
end
|
|
end
|
|
`,
|
|
'app.rb': `
|
|
require_relative 'models/user'
|
|
|
|
def main
|
|
u = User.new
|
|
u.save
|
|
u.greet
|
|
end
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(tmpDir, () => {});
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('detects User class', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
|
});
|
|
|
|
it('detects save and greet as Method nodes', () => {
|
|
const methods = getNodesByLabel(result, 'Method');
|
|
expect(methods).toContain('save');
|
|
expect(methods).toContain('greet');
|
|
});
|
|
|
|
it('emits HAS_METHOD edges from User to save and greet', () => {
|
|
const edges = getRelationships(result, 'HAS_METHOD');
|
|
const userSave = edges.find((e) => e.source === 'User' && e.target === 'save');
|
|
const userGreet = edges.find((e) => e.source === 'User' && e.target === 'greet');
|
|
expect(userSave).toBeDefined();
|
|
expect(userGreet).toBeDefined();
|
|
});
|
|
|
|
it('resolves main → u.save() as CALLS edge', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const saveCall = calls.find(
|
|
(c) => c.target === 'save' && c.source === 'main' && c.targetFilePath?.includes('user.rb'),
|
|
);
|
|
expect(saveCall).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 2. Module mixin with include
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Ruby scope: module mixin with include', () => {
|
|
let result: PipelineResult;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ruby-scope-mixin-'));
|
|
writeFixtureRepo(tmpDir, {
|
|
'serializable.rb': `
|
|
module Serializable
|
|
def serialize
|
|
to_json
|
|
end
|
|
end
|
|
`,
|
|
'user.rb': `
|
|
require_relative 'serializable'
|
|
|
|
class User
|
|
include Serializable
|
|
|
|
def save
|
|
serialize
|
|
end
|
|
end
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(tmpDir, () => {});
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('detects User as Class and Serializable as Trait', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toContain('User');
|
|
expect(getNodesByLabel(result, 'Trait')).toContain('Serializable');
|
|
});
|
|
|
|
it('emits IMPLEMENTS edge from User to Serializable', () => {
|
|
const impls = getRelationships(result, 'IMPLEMENTS');
|
|
const edge = impls.find((e) => e.source === 'User' && e.target === 'Serializable');
|
|
expect(edge).toBeDefined();
|
|
});
|
|
|
|
it('resolves save → serialize as CALLS edge', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const serializeCall = calls.find((c) => c.target === 'serialize' && c.source === 'save');
|
|
expect(serializeCall).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 3. Singleton method (def self.foo)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Ruby scope: singleton method (def self.foo)', () => {
|
|
let result: PipelineResult;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ruby-scope-singleton-'));
|
|
writeFixtureRepo(tmpDir, {
|
|
'config.rb': `
|
|
class Config
|
|
def self.load
|
|
new
|
|
end
|
|
|
|
def validate
|
|
true
|
|
end
|
|
end
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(tmpDir, () => {});
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('detects Config class', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toContain('Config');
|
|
});
|
|
|
|
it('detects load (singleton) and validate (instance) as Method nodes', () => {
|
|
const methods = getNodesByLabel(result, 'Method');
|
|
expect(methods).toContain('load');
|
|
expect(methods).toContain('validate');
|
|
});
|
|
|
|
it('emits HAS_METHOD edges from Config to both methods', () => {
|
|
const edges = getRelationships(result, 'HAS_METHOD');
|
|
const configLoad = edges.find((e) => e.source === 'Config' && e.target === 'load');
|
|
const configValidate = edges.find((e) => e.source === 'Config' && e.target === 'validate');
|
|
expect(configLoad).toBeDefined();
|
|
expect(configValidate).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 4. Require/require_relative import resolution
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Ruby scope: require_relative import resolution', () => {
|
|
let result: PipelineResult;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ruby-scope-imports-'));
|
|
writeFixtureRepo(tmpDir, {
|
|
'lib/utils.rb': `
|
|
class Utils
|
|
def format(text)
|
|
text.strip
|
|
end
|
|
end
|
|
`,
|
|
'app.rb': `
|
|
require_relative 'lib/utils'
|
|
|
|
def run
|
|
u = Utils.new
|
|
u.format("hello")
|
|
end
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(tmpDir, () => {});
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('emits IMPORTS edge from app.rb to lib/utils.rb', () => {
|
|
const imports = getRelationships(result, 'IMPORTS');
|
|
const imp = imports.find(
|
|
(e) => e.sourceFilePath?.includes('app.rb') && e.targetFilePath?.includes('utils.rb'),
|
|
);
|
|
expect(imp).toBeDefined();
|
|
});
|
|
|
|
it('resolves run → u.format() as CALLS edge to utils.rb', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const formatCall = calls.find(
|
|
(c) => c.target === 'format' && c.source === 'run' && c.targetFilePath?.includes('utils.rb'),
|
|
);
|
|
expect(formatCall).toBeDefined();
|
|
});
|
|
|
|
it('detects Utils class and format method', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toContain('Utils');
|
|
expect(getNodesByLabel(result, 'Method')).toContain('format');
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 5. Constructor inference (User.new)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Ruby scope: constructor inference via .new', () => {
|
|
let result: PipelineResult;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ruby-scope-ctor-'));
|
|
writeFixtureRepo(tmpDir, {
|
|
'formatter.rb': `
|
|
class Formatter
|
|
def format(text)
|
|
text.upcase
|
|
end
|
|
end
|
|
|
|
def main
|
|
f = Formatter.new
|
|
f.format("hello")
|
|
end
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(tmpDir, () => {});
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('detects Formatter class and format method', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toContain('Formatter');
|
|
expect(getNodesByLabel(result, 'Method')).toContain('format');
|
|
});
|
|
|
|
it('emits HAS_METHOD edge from Formatter to format', () => {
|
|
const edges = getRelationships(result, 'HAS_METHOD');
|
|
const fmtEdge = edges.find((e) => e.source === 'Formatter' && e.target === 'format');
|
|
expect(fmtEdge).toBeDefined();
|
|
});
|
|
|
|
it('resolves main → f.format() to Formatter#format via constructor inference', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const formatCall = calls.find(
|
|
(c) =>
|
|
c.target === 'format' && c.source === 'main' && c.targetFilePath?.includes('formatter.rb'),
|
|
);
|
|
expect(formatCall).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 6. Block scope (do...end with params)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Ruby scope: block scope with do...end', () => {
|
|
let result: PipelineResult;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ruby-scope-block-'));
|
|
writeFixtureRepo(tmpDir, {
|
|
'processor.rb': `
|
|
class Processor
|
|
def run
|
|
items = [1, 2, 3]
|
|
items.each do |item|
|
|
process(item)
|
|
end
|
|
end
|
|
|
|
def process(x)
|
|
x * 2
|
|
end
|
|
end
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(tmpDir, () => {});
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('detects Processor class', () => {
|
|
expect(getNodesByLabel(result, 'Class')).toContain('Processor');
|
|
});
|
|
|
|
it('detects run and process as Method nodes on Processor', () => {
|
|
const methods = getNodesByLabel(result, 'Method');
|
|
expect(methods).toContain('run');
|
|
expect(methods).toContain('process');
|
|
});
|
|
|
|
it('emits HAS_METHOD edges from Processor to run and process', () => {
|
|
const edges = getRelationships(result, 'HAS_METHOD');
|
|
const procRun = edges.find((e) => e.source === 'Processor' && e.target === 'run');
|
|
const procProcess = edges.find((e) => e.source === 'Processor' && e.target === 'process');
|
|
expect(procRun).toBeDefined();
|
|
expect(procProcess).toBeDefined();
|
|
});
|
|
|
|
it('resolves run → process() as CALLS edge inside block scope', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const processCall = calls.find((c) => c.target === 'process' && c.source === 'run');
|
|
expect(processCall).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 7. Class inheritance (EXTENDS)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Ruby scope: class inheritance via <', () => {
|
|
let result: PipelineResult;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ruby-scope-inherit-'));
|
|
writeFixtureRepo(tmpDir, {
|
|
'animals.rb': `
|
|
class Animal
|
|
def speak
|
|
"..."
|
|
end
|
|
end
|
|
|
|
class Dog < Animal
|
|
def bark
|
|
speak
|
|
end
|
|
end
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(tmpDir, () => {});
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('detects Animal and Dog as Class nodes', () => {
|
|
const classes = getNodesByLabel(result, 'Class');
|
|
expect(classes).toContain('Animal');
|
|
expect(classes).toContain('Dog');
|
|
});
|
|
|
|
it('emits EXTENDS edge from Dog to Animal', () => {
|
|
const extends_ = getRelationships(result, 'EXTENDS');
|
|
const edge = extends_.find((e) => e.source === 'Dog' && e.target === 'Animal');
|
|
expect(edge).toBeDefined();
|
|
});
|
|
|
|
it('resolves bark → speak as CALLS edge', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const speakCall = calls.find((c) => c.target === 'speak' && c.source === 'bark');
|
|
expect(speakCall).toBeDefined();
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 8. Super resolution
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe('Ruby scope: super resolution in subclass', () => {
|
|
let result: PipelineResult;
|
|
let tmpDir: string;
|
|
|
|
beforeAll(async () => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ruby-scope-super-'));
|
|
writeFixtureRepo(tmpDir, {
|
|
'hierarchy.rb': `
|
|
class Base
|
|
def greet
|
|
"hello"
|
|
end
|
|
end
|
|
|
|
class Child < Base
|
|
def greet
|
|
super
|
|
end
|
|
end
|
|
|
|
def main
|
|
c = Child.new
|
|
c.greet
|
|
end
|
|
`,
|
|
});
|
|
result = await runPipelineFromRepo(tmpDir, () => {});
|
|
}, 60000);
|
|
|
|
afterAll(() => {
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('detects Base and Child as Class nodes', () => {
|
|
const classes = getNodesByLabel(result, 'Class');
|
|
expect(classes).toContain('Base');
|
|
expect(classes).toContain('Child');
|
|
});
|
|
|
|
it('emits EXTENDS edge from Child to Base', () => {
|
|
const extends_ = getRelationships(result, 'EXTENDS');
|
|
const edge = extends_.find((e) => e.source === 'Child' && e.target === 'Base');
|
|
expect(edge).toBeDefined();
|
|
});
|
|
|
|
it('resolves main → c.greet() as CALLS edge', () => {
|
|
const calls = getRelationships(result, 'CALLS');
|
|
const greetCall = calls.find((c) => c.target === 'greet' && c.source === 'main');
|
|
expect(greetCall).toBeDefined();
|
|
});
|
|
});
|