GitNexus/gitnexus/test/integration/ruby-factory-block-owner.test.ts
Parafee41 233ca28492
fix(ruby): model block-taking class factories (#3376)
* fix(ruby): model block-taking class factories

* fix(ruby): cover braced factory blocks

* handle brace factory blocks

* fix nested Ruby factory ownership

* test(ruby): pin factory ownership by node id

* test(ruby): cover brace factory variants

---------

Co-authored-by: Gergő Magyar <gergomagyar@icloud.com>
2026-09-25 12:47:25 +01:00

158 lines
4.9 KiB
TypeScript

import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
import {
getRelationships,
runPipelineFromRepo,
writeFixtureRepo,
type PipelineResult,
} from './resolvers/helpers.js';
describe('Ruby block-taking factory ownership', () => {
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'gitnexus-ruby-factory-owner-'));
let result: PipelineResult;
beforeAll(async () => {
writeFixtureRepo(root, {
'app.rb': `
class Outer
StructType = Struct.new(:value) do
def struct_method = value
end
DataType = Data.define(:value) do
def data_method = value
end
ClassType = Class.new do
def class_method = 1
end
ModuleType = Module.new do
def module_method = 1
end
BraceStructType = Struct.new(:value) {
def brace_struct_method = value
}
BraceDataType = Data.define(:value) {
def brace_data_method = value
}
BraceClassType = Class.new {
def brace_class_method = 1
}
BraceModuleType = Module.new {
def brace_module_method = 1
}
ArbitraryType = Builder.make do
def arbitrary_method = 1
end
module Nested
ClassType = Class.new do
def nested_class_method = 1
end
end
First = Class.new do
Item = Class.new do
def first_item_method = 1
end
end
Second = Class.new do
Item = Class.new do
def second_item_method = 1
end
end
end
`,
});
result = await runPipelineFromRepo(root, () => {}, { skipGraphPhases: true });
}, 120_000);
afterAll(() => {
fs.rmSync(root, { recursive: true, force: true });
});
it.each([
['StructType', 'Struct', 'struct_method', 'Outer.StructType'],
['DataType', 'Class', 'data_method', 'Outer.DataType'],
['ClassType', 'Class', 'class_method', 'Outer.ClassType'],
['ModuleType', 'Trait', 'module_method', 'Outer.ModuleType'],
['BraceStructType', 'Struct', 'brace_struct_method', 'Outer.BraceStructType'],
['BraceDataType', 'Class', 'brace_data_method', 'Outer.BraceDataType'],
['BraceClassType', 'Class', 'brace_class_method', 'Outer.BraceClassType'],
['BraceModuleType', 'Trait', 'brace_module_method', 'Outer.BraceModuleType'],
])(
'materializes %s as a %s and attributes its factory-block method',
(owner, ownerLabel, method, qualifiedOwner) => {
const ownerNode = result.graph.nodes.find(
(node) =>
node.label === ownerLabel &&
node.properties.name === owner &&
node.id.includes(qualifiedOwner),
);
const ownership = getRelationships(result, 'HAS_METHOD').filter(
(edge) => edge.target === method,
);
expect(ownerNode).toBeDefined();
expect(ownership.map((edge) => edge.rel.sourceId)).toEqual([ownerNode?.id]);
},
);
it('does not treat an arbitrary block-taking call as a class factory', () => {
const syntheticOwner = result.graph.nodes.find(
(node) =>
['Class', 'Struct', 'Trait'].includes(node.label) &&
node.properties.name === 'ArbitraryType',
);
const ownership = getRelationships(result, 'HAS_METHOD').filter(
(edge) => edge.target === 'arbitrary_method',
);
expect(syntheticOwner).toBeUndefined();
expect(ownership.map((edge) => edge.source)).toEqual(['Outer']);
});
it('keeps same-tail factory constants distinct across nested lexical scopes', () => {
const owners = result.graph.nodes.filter(
(node) => node.label === 'Class' && node.properties.name === 'ClassType',
);
const ownership = getRelationships(result, 'HAS_METHOD').filter(
(edge) => edge.target === 'nested_class_method',
);
expect(owners).toHaveLength(2);
expect(new Set(owners.map((node) => node.id)).size).toBe(2);
const nestedOwner = owners.find((node) => node.id.includes('Outer.Nested.ClassType'));
expect(nestedOwner).toBeDefined();
expect(ownership.map((edge) => edge.rel.sourceId)).toEqual([nestedOwner?.id]);
});
it('keeps same-tail factories distinct when nested inside factory blocks', () => {
const owners = result.graph.nodes.filter(
(node) => node.label === 'Class' && node.properties.name === 'Item',
);
const firstOwner = owners.find((node) => node.id.includes('Outer.First.Item'));
const secondOwner = owners.find((node) => node.id.includes('Outer.Second.Item'));
const firstOwnership = getRelationships(result, 'HAS_METHOD').filter(
(edge) => edge.target === 'first_item_method',
);
const secondOwnership = getRelationships(result, 'HAS_METHOD').filter(
(edge) => edge.target === 'second_item_method',
);
expect(owners).toHaveLength(2);
expect(firstOwner).toBeDefined();
expect(secondOwner).toBeDefined();
expect(firstOwnership.map((edge) => edge.rel.sourceId)).toEqual([firstOwner?.id]);
expect(secondOwnership.map((edge) => edge.rel.sourceId)).toEqual([secondOwner?.id]);
});
});