From b4a2a4b91eaa744357f0d5ad4ae9265617d7f805 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 20 May 2026 08:00:03 +0100 Subject: [PATCH] fix(ingestion): Prioritize same-module Java type resolution for duplicate FQNs across modules (#1712) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial plan * Fix Java same-name type resolution with same-module priority Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/df0843e3-e244-4e0f-a94a-311df3899bd0 * Refine Java ambiguity fallback safety check Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/df0843e3-e244-4e0f-a94a-311df3899bd0 * Remove Java-specific fallback from shared scope walkers Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/e882906a-2c96-411e-94a5-123a345421a9 * Harden Java module key and ambiguous owner fallback Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/e882906a-2c96-411e-94a5-123a345421a9 * Add negative assertions for duplicate-FQN module edges Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/e882906a-2c96-411e-94a5-123a345421a9 * Make Java same-module ordering path-agnostic Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/b317a759-f6bc-4590-bd2a-628f0ee9c477 * Refine generic Java path-affinity ordering safeguards Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/b317a759-f6bc-4590-bd2a-628f0ee9c477 * Polish Java path-affinity ordering clarity Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/b317a759-f6bc-4590-bd2a-628f0ee9c477 * Simplify Java path-affinity ordering logic Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/b317a759-f6bc-4590-bd2a-628f0ee9c477 * Revert legacy DAG Java ambiguity ordering changes Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/94e50cf2-9733-4e69-a0eb-9fd38cbdb589 * Skip duplicate-FQN Java assertions in legacy parity mode Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/1b560efa-1b3b-4697-b590-c6ef447f431e * Tighten duplicate-FQN Java CALLS edge cardinality assertions Agent-Logs-Url: https://github.com/abhigyanpatwari/GitNexus/sessions/67c18f93-5e56-4b15-8404-cdf1be9b4485 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Gergő Magyar --- gitnexus/src/core/ingestion/languages/java.ts | 44 +++++++++++++ .../src/main/java/com/example/Module1App.java | 8 +++ .../main/java/com/example/UserService.java | 6 ++ .../src/main/java/com/example/Module2App.java | 8 +++ .../main/java/com/example/UserService.java | 6 ++ .../test/integration/resolvers/helpers.ts | 7 ++ .../test/integration/resolvers/java.test.ts | 66 +++++++++++++++++++ 7 files changed, 145 insertions(+) create mode 100644 gitnexus/test/fixtures/lang-resolution/java-duplicate-fqn-modules/module1/src/main/java/com/example/Module1App.java create mode 100644 gitnexus/test/fixtures/lang-resolution/java-duplicate-fqn-modules/module1/src/main/java/com/example/UserService.java create mode 100644 gitnexus/test/fixtures/lang-resolution/java-duplicate-fqn-modules/module2/src/main/java/com/example/Module2App.java create mode 100644 gitnexus/test/fixtures/lang-resolution/java-duplicate-fqn-modules/module2/src/main/java/com/example/UserService.java diff --git a/gitnexus/src/core/ingestion/languages/java.ts b/gitnexus/src/core/ingestion/languages/java.ts index c70eacb10..20755d7cd 100644 --- a/gitnexus/src/core/ingestion/languages/java.ts +++ b/gitnexus/src/core/ingestion/languages/java.ts @@ -27,6 +27,7 @@ import { javaMethodConfig } from '../method-extractors/configs/jvm.js'; import { createVariableExtractor } from '../variable-extractors/generic.js'; import { javaVariableConfig } from '../variable-extractors/configs/jvm.js'; import { createHeritageExtractor } from '../heritage-extractors/generic.js'; +import type { SymbolDefinition } from 'gitnexus-shared'; import { emitJavaScopeCaptures, interpretJavaImport, @@ -39,6 +40,48 @@ import { resolveJavaImportTarget, } from './java/index.js'; +const orderJavaSameNameTypeCandidates = ({ + callSiteFilePath, + candidates, +}: { + readonly typeName: string; + readonly callSiteFilePath: string; + readonly candidates: readonly SymbolDefinition[]; +}): readonly SymbolDefinition[] | null => { + if (!callSiteFilePath.endsWith('.java')) return null; + if (candidates.length <= 1) return null; + const callerDir = splitDirectorySegments(callSiteFilePath); + + const scored = candidates.map((candidate, index) => ({ + candidate, + index, + score: sharedPrefixLength(callerDir, splitDirectorySegments(candidate.filePath)), + })); + const bestScore = Math.max(...scored.map((entry) => entry.score)); + // When all candidates tie, we have no structural signal to prefer one path. + // Returning null keeps downstream ambiguity handling conservative. + if (scored.every((entry) => entry.score === bestScore)) return null; + + const ordered = [...scored] + .sort((a, b) => b.score - a.score || a.index - b.index) + .map((entry) => entry.candidate); + return ordered; +}; + +const splitDirectorySegments = (filePath: string): string[] => { + const normalized = filePath.replace(/\\/g, '/'); + // Remove empty segments from leading/trailing/multiple slashes, then drop filename. + const segments = normalized.split('/').filter(Boolean); + return segments.slice(0, -1); +}; + +const sharedPrefixLength = (left: readonly string[], right: readonly string[]): number => { + const max = Math.min(left.length, right.length); + let idx = 0; + while (idx < max && left[idx] === right[idx]) idx += 1; + return idx; +}; + export const javaProvider = defineLanguage({ id: SupportedLanguages.Java, extensions: ['.java'], @@ -87,4 +130,5 @@ export const javaProvider = defineLanguage({ receiverBinding: javaReceiverBinding, arityCompatibility: javaArityCompatibility, resolveImportTarget: resolveJavaImportTarget, + orderSameNameTypeCandidates: orderJavaSameNameTypeCandidates, }); diff --git a/gitnexus/test/fixtures/lang-resolution/java-duplicate-fqn-modules/module1/src/main/java/com/example/Module1App.java b/gitnexus/test/fixtures/lang-resolution/java-duplicate-fqn-modules/module1/src/main/java/com/example/Module1App.java new file mode 100644 index 000000000..242956497 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-duplicate-fqn-modules/module1/src/main/java/com/example/Module1App.java @@ -0,0 +1,8 @@ +package com.example; + +public class Module1App { + public void run() { + UserService service = new UserService(); + service.ping(); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/java-duplicate-fqn-modules/module1/src/main/java/com/example/UserService.java b/gitnexus/test/fixtures/lang-resolution/java-duplicate-fqn-modules/module1/src/main/java/com/example/UserService.java new file mode 100644 index 000000000..9daea3d45 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-duplicate-fqn-modules/module1/src/main/java/com/example/UserService.java @@ -0,0 +1,6 @@ +package com.example; + +public class UserService { + public void ping() { + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/java-duplicate-fqn-modules/module2/src/main/java/com/example/Module2App.java b/gitnexus/test/fixtures/lang-resolution/java-duplicate-fqn-modules/module2/src/main/java/com/example/Module2App.java new file mode 100644 index 000000000..7d24142b8 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-duplicate-fqn-modules/module2/src/main/java/com/example/Module2App.java @@ -0,0 +1,8 @@ +package com.example; + +public class Module2App { + public void run() { + UserService service = new UserService(); + service.ping(); + } +} diff --git a/gitnexus/test/fixtures/lang-resolution/java-duplicate-fqn-modules/module2/src/main/java/com/example/UserService.java b/gitnexus/test/fixtures/lang-resolution/java-duplicate-fqn-modules/module2/src/main/java/com/example/UserService.java new file mode 100644 index 000000000..9daea3d45 --- /dev/null +++ b/gitnexus/test/fixtures/lang-resolution/java-duplicate-fqn-modules/module2/src/main/java/com/example/UserService.java @@ -0,0 +1,6 @@ +package com.example; + +public class UserService { + public void ping() { + } +} diff --git a/gitnexus/test/integration/resolvers/helpers.ts b/gitnexus/test/integration/resolvers/helpers.ts index cc2697849..bd5eaa474 100644 --- a/gitnexus/test/integration/resolvers/helpers.ts +++ b/gitnexus/test/integration/resolvers/helpers.ts @@ -34,6 +34,13 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly { }); }); +describe('Java same-module priority for duplicate FQNs', () => { + let result: PipelineResult; + + beforeAll(async () => { + result = await runPipelineFromRepo(path.join(FIXTURES, 'java-duplicate-fqn-modules'), () => {}); + }, 60000); + + it('resolves Module1App.run calls to module1 UserService, not module2', () => { + const calls = getRelationships(result, 'CALLS'); + const module1ToModule1 = calls.filter( + (c) => + c.source === 'run' && + c.target === 'UserService' && + c.sourceFilePath === 'module1/src/main/java/com/example/Module1App.java' && + c.targetFilePath === 'module1/src/main/java/com/example/UserService.java', + ); + const module1ToModule2 = calls.filter( + (c) => + c.source === 'run' && + c.target === 'UserService' && + c.sourceFilePath === 'module1/src/main/java/com/example/Module1App.java' && + c.targetFilePath === 'module2/src/main/java/com/example/UserService.java', + ); + const module1ToAnyUserService = calls.filter( + (c) => + c.source === 'run' && + c.target === 'UserService' && + c.sourceFilePath === 'module1/src/main/java/com/example/Module1App.java' && + /module[12]\/src\/main\/java\/com\/example\/UserService\.java/.test(c.targetFilePath), + ); + + expect(module1ToModule1.length).toBe(1); + expect(module1ToModule2.length).toBe(0); + expect(module1ToAnyUserService.length).toBe(1); + }); + + it('resolves Module2App.run calls to module2 UserService, not module1', () => { + const calls = getRelationships(result, 'CALLS'); + const module2ToModule2 = calls.filter( + (c) => + c.source === 'run' && + c.target === 'UserService' && + c.sourceFilePath === 'module2/src/main/java/com/example/Module2App.java' && + c.targetFilePath === 'module2/src/main/java/com/example/UserService.java', + ); + const module2ToModule1 = calls.filter( + (c) => + c.source === 'run' && + c.target === 'UserService' && + c.sourceFilePath === 'module2/src/main/java/com/example/Module2App.java' && + c.targetFilePath === 'module1/src/main/java/com/example/UserService.java', + ); + const module2ToAnyUserService = calls.filter( + (c) => + c.source === 'run' && + c.target === 'UserService' && + c.sourceFilePath === 'module2/src/main/java/com/example/Module2App.java' && + /module[12]\/src\/main\/java\/com\/example\/UserService\.java/.test(c.targetFilePath), + ); + + expect(module2ToModule2.length).toBe(1); + expect(module2ToModule1.length).toBe(0); + expect(module2ToAnyUserService.length).toBe(1); + }); +}); + // --------------------------------------------------------------------------- // Member-call resolution: obj.method() resolves through pipeline // ---------------------------------------------------------------------------