mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-24 00:51:53 +00:00
Merge branch 'main' into feat/Desktop-app
This commit is contained in:
commit
04d678fd29
7 changed files with 145 additions and 0 deletions
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -0,0 +1,8 @@
|
|||
package com.example;
|
||||
|
||||
public class Module1App {
|
||||
public void run() {
|
||||
UserService service = new UserService();
|
||||
service.ping();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.example;
|
||||
|
||||
public class UserService {
|
||||
public void ping() {
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package com.example;
|
||||
|
||||
public class Module2App {
|
||||
public void run() {
|
||||
UserService service = new UserService();
|
||||
service.ping();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
package com.example;
|
||||
|
||||
public class UserService {
|
||||
public void ping() {
|
||||
}
|
||||
}
|
||||
|
|
@ -34,6 +34,13 @@ const LEGACY_RESOLVER_PARITY_EXPECTED_FAILURES: Readonly<Record<string, Readonly
|
|||
// which is only available in the registry-primary path.
|
||||
'resolves user.Save() to the method whose receiver type is declared in another package file',
|
||||
]),
|
||||
java: new Set([
|
||||
// Duplicate-FQN same-module path-affinity ordering is implemented in the
|
||||
// Java provider hook for the scope-resolution path. Legacy DAG parity runs
|
||||
// still use legacy owner/type resolution behavior and can bind cross-module.
|
||||
'resolves Module1App.run calls to module1 UserService, not module2',
|
||||
'resolves Module2App.run calls to module2 UserService, not module1',
|
||||
]),
|
||||
php: new Set([
|
||||
// Arity-narrowing in `pickUniqueGlobalCallable` rejects free-call
|
||||
// candidates that are definitively below required-parameter-count. The
|
||||
|
|
|
|||
|
|
@ -174,6 +174,72 @@ describe('Java call resolution with arity filtering', () => {
|
|||
});
|
||||
});
|
||||
|
||||
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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue