From dfe83f333ef88d6043b21b26deb3a93995737748 Mon Sep 17 00:00:00 2001 From: marxo126 Date: Sat, 21 Mar 2026 10:25:33 +0100 Subject: [PATCH] fix: deduplicate Swift extension class nodes in call resolution MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When Swift extensions create multiple Class nodes with the same name (e.g. Product.swift + ProductMatchableConformance.swift), the call resolver gets multiple candidates and refuses to emit a CALLS edge. Add dedup: when all candidates share the same type (Class/Struct) and differ only by file, prefer the primary definition (shortest filepath). Note: This fix is partial — some constructor calls inside function bodies may still be consumed by the type-env constructor binding scanner before reaching resolveCallTarget. Filed as known limitation. Co-Authored-By: Claude Opus 4.6 (1M context) --- gitnexus/src/core/ingestion/call-processor.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/gitnexus/src/core/ingestion/call-processor.ts b/gitnexus/src/core/ingestion/call-processor.ts index 2c323ad4b..53b8a9be8 100644 --- a/gitnexus/src/core/ingestion/call-processor.ts +++ b/gitnexus/src/core/ingestion/call-processor.ts @@ -893,7 +893,20 @@ const resolveCallTarget = ( if (disambiguated) return toResolveResult(disambiguated, tiered.tier); } - if (filteredCandidates.length !== 1) return null; + if (filteredCandidates.length !== 1) { + // Deduplicate: Swift extensions create multiple Class nodes with the same name. + // When all candidates share the same type and differ only by file (extension vs + // primary definition), they represent the same symbol. Prefer the primary + // definition (shortest file path: Product.swift over ProductExtension.swift). + if (filteredCandidates.length > 1) { + const allSameType = filteredCandidates.every(c => c.type === filteredCandidates[0].type); + if (allSameType && (filteredCandidates[0].type === 'Class' || filteredCandidates[0].type === 'Struct')) { + const sorted = [...filteredCandidates].sort((a, b) => a.filePath.length - b.filePath.length); + return toResolveResult(sorted[0], tiered.tier); + } + } + return null; + } return toResolveResult(filteredCandidates[0], tiered.tier); };