From 9046175902a35e220446729d94f44b4bc60e2210 Mon Sep 17 00:00:00 2001 From: kigland Date: Fri, 25 Sep 2026 09:14:38 +0800 Subject: [PATCH] fix nested Ruby factory ownership --- .../class-extractors/configs/ruby.ts | 6 +++- gitnexus/src/core/ingestion/languages/ruby.ts | 2 +- .../ruby-factory-block-owner.test.ts | 32 +++++++++++++++++++ 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/gitnexus/src/core/ingestion/class-extractors/configs/ruby.ts b/gitnexus/src/core/ingestion/class-extractors/configs/ruby.ts index e1a351eea..bc57d8926 100644 --- a/gitnexus/src/core/ingestion/class-extractors/configs/ruby.ts +++ b/gitnexus/src/core/ingestion/class-extractors/configs/ruby.ts @@ -46,10 +46,14 @@ export const rubyFactoryBindingName = (node: SyntaxNode): string | undefined => export const rubyClassConfig: ClassExtractionConfig = { language: SupportedLanguages.Ruby, typeDeclarationNodes: ['class', 'do_block', 'block'], - ancestorScopeNodeTypes: ['module', 'class'], + ancestorScopeNodeTypes: ['module', 'class', 'do_block', 'block'], // #1978: key nested-type nodes by their fully-qualified path (Outer.Inner) so // same-tail classes nested under different modules stay distinct. qualifiedNodeId: true, + extractScopeSegments: (node) => { + const factoryName = rubyFactoryBindingName(node); + return factoryName ? [factoryName] : undefined; + }, extractName: (node) => rubyFactoryBindingName(node), extractType: (node) => { const type = rubyFactoryType(node); diff --git a/gitnexus/src/core/ingestion/languages/ruby.ts b/gitnexus/src/core/ingestion/languages/ruby.ts index dca92268e..63bd9b3a6 100644 --- a/gitnexus/src/core/ingestion/languages/ruby.ts +++ b/gitnexus/src/core/ingestion/languages/ruby.ts @@ -146,7 +146,7 @@ const BUILT_INS: ReadonlySet = new Set([ * All other container types are returned as-is. */ const rubyResolveEnclosingOwner = (node: SyntaxNode): SyntaxNode | null => { - if (node.type === 'do_block') { + if (node.type === 'do_block' || node.type === 'block') { return rubyFactoryType(node) === undefined ? null : node; } if (node.type === 'singleton_class') { diff --git a/gitnexus/test/integration/ruby-factory-block-owner.test.ts b/gitnexus/test/integration/ruby-factory-block-owner.test.ts index da33ad1b4..8668e4c0b 100644 --- a/gitnexus/test/integration/ruby-factory-block-owner.test.ts +++ b/gitnexus/test/integration/ruby-factory-block-owner.test.ts @@ -46,6 +46,18 @@ class Outer 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 `, }); @@ -105,4 +117,24 @@ end 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]); + }); });