mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-08 03:08:13 +00:00
Merge branch 'main' into fix/ts-object-method-exports-luyua9
This commit is contained in:
commit
8721f61dac
10 changed files with 252 additions and 5 deletions
6
eval/uv.lock
generated
6
eval/uv.lock
generated
|
|
@ -760,11 +760,11 @@ wheels = [
|
|||
|
||||
[[package]]
|
||||
name = "idna"
|
||||
version = "3.11"
|
||||
version = "3.15"
|
||||
source = { registry = "https://pypi.org/simple" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/0703ccc57f3a7233505399edb88de3cbd678da106337b9fcde432b65ed60/idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902", size = 194582, upload-time = "2025-10-12T14:55:20.501Z" }
|
||||
sdist = { url = "https://files.pythonhosted.org/packages/82/77/7b3966d0b9d1d31a36ddf1746926a11dface89a83409bf1483f0237aa758/idna-3.15.tar.gz", hash = "sha256:ca962446ea538f7092a95e057da437618e886f4d349216d2b1e294abfdb65fdc", size = 199245, upload-time = "2026-05-12T22:45:57.011Z" }
|
||||
wheels = [
|
||||
{ url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" },
|
||||
{ url = "https://files.pythonhosted.org/packages/d2/23/408243171aa9aaba178d3e2559159c24c1171a641aa83b67bdd3394ead8e/idna-3.15-py3-none-any.whl", hash = "sha256:048adeaf8c2d788c40fee287673ccaa74c24ffd8dcf09ffa555a2fbb59f10ac8", size = 72340, upload-time = "2026-05-12T22:45:55.733Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -248,6 +248,29 @@ function tryRealpath(p: string): string {
|
|||
*/
|
||||
export function resolveWorktreeCwd(repoPath: string, launchCwd: string): string {
|
||||
try {
|
||||
// Verify repoPath is a git root before comparing against its canonical
|
||||
// root. If getGitRoot returns a different path, repoPath is an arbitrary
|
||||
// subdirectory — skip both the linked-worktree guard and auto-detection
|
||||
// and fall through to the repoPath fallback.
|
||||
const repoGitRoot = getGitRoot(repoPath);
|
||||
const repoCanonical =
|
||||
repoGitRoot && tryRealpath(repoGitRoot) === tryRealpath(repoPath)
|
||||
? getCanonicalRepoRoot(repoPath)
|
||||
: null;
|
||||
|
||||
// Early exit: if repoPath is a linked worktree (differs from its canonical
|
||||
// main-checkout root), return it unchanged. Do NOT override it with the
|
||||
// server's launch directory — that would silently replace the explicitly-
|
||||
// resolved worktree index with the main checkout.
|
||||
//
|
||||
// getCanonicalRepoRoot returns the main-checkout path for both the checkout
|
||||
// and all linked worktrees:
|
||||
// repoPath === canonical → main checkout (auto-detect may fire below)
|
||||
// repoPath !== canonical → linked worktree (return as-is)
|
||||
if (repoCanonical && tryRealpath(repoPath) !== tryRealpath(repoCanonical)) {
|
||||
return repoPath;
|
||||
}
|
||||
|
||||
const launchGitRoot = getGitRoot(launchCwd);
|
||||
if (launchGitRoot) {
|
||||
// Normalise via realpathSync before comparing so macOS /var → /private/var
|
||||
|
|
@ -256,8 +279,12 @@ export function resolveWorktreeCwd(repoPath: string, launchCwd: string): string
|
|||
const realRepo = tryRealpath(repoPath);
|
||||
if (realLaunch !== realRepo) {
|
||||
const launchCanonical = getCanonicalRepoRoot(launchCwd);
|
||||
const repoCanonical = getCanonicalRepoRoot(repoPath);
|
||||
if (launchCanonical && repoCanonical && launchCanonical === repoCanonical) {
|
||||
// Use tryRealpath on both canonical values for cross-platform safety.
|
||||
if (
|
||||
launchCanonical &&
|
||||
repoCanonical &&
|
||||
tryRealpath(launchCanonical) === tryRealpath(repoCanonical)
|
||||
) {
|
||||
return launchGitRoot;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -189,6 +189,81 @@ describe('resolveWorktreeCwd — auto-detection helper', () => {
|
|||
rmSync(repoB, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('returns worktreeDir unchanged when repoPath IS a linked worktree and launchCwd is the main checkout', () => {
|
||||
// Regression for: detect_changes returns no changes when the MCP server
|
||||
// runs from the main checkout but the resolved repo index is a separately-
|
||||
// indexed linked worktree (issue #1659 / dpearson2699 report).
|
||||
//
|
||||
// Before the fix, resolveWorktreeCwd would detect that launchCwd (main
|
||||
// checkout) and repoPath (worktree) share the same canonical root and
|
||||
// wrongly override repoPath with the main-checkout path, causing git diff
|
||||
// to run from the wrong directory and return 0 changes.
|
||||
const repoDir = mkdtempSync(path.join(os.tmpdir(), 'gitnexus-rwc-idx-wt-'));
|
||||
try {
|
||||
execSync('git init -q', { cwd: repoDir, stdio: 'ignore' });
|
||||
execSync('git config user.email "test@example.com"', { cwd: repoDir, stdio: 'ignore' });
|
||||
execSync('git config user.name "Test"', { cwd: repoDir, stdio: 'ignore' });
|
||||
writeFileSync(path.join(repoDir, 'x.ts'), 'export const x = 1;\n');
|
||||
execSync('git add x.ts', { cwd: repoDir, stdio: 'ignore' });
|
||||
execSync('git commit -q -m "initial"', { cwd: repoDir, stdio: 'ignore' });
|
||||
|
||||
const worktreeDir = path.join(repoDir, 'wt-indexed');
|
||||
execSync(`git worktree add -q -b indexed "${worktreeDir}"`, {
|
||||
cwd: repoDir,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
|
||||
// Simulate: repo registry entry points to the worktree (repoPath = worktreeDir)
|
||||
// but the MCP server was launched from the main checkout (launchCwd = repoDir).
|
||||
// resolveWorktreeCwd must NOT override the correct worktree path with repoDir.
|
||||
const result = resolveWorktreeCwd(worktreeDir, repoDir);
|
||||
expect(realpathSync.native(result)).toBe(realpathSync.native(worktreeDir));
|
||||
expect(realpathSync.native(result)).not.toBe(realpathSync.native(repoDir));
|
||||
} finally {
|
||||
try {
|
||||
execSync('git worktree remove -f wt-indexed', { cwd: repoDir, stdio: 'ignore' });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
rmSync(repoDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it('returns worktreeA unchanged when both repoPath and launchCwd are different linked worktrees of the same repo', () => {
|
||||
// Covers: repoPath = wt-A (indexed), launchCwd = wt-B (server launched from another worktree).
|
||||
// The guard fires on repoPath being a linked worktree regardless of what launchCwd is,
|
||||
// so wt-A must be returned unchanged — not wt-B, not the main checkout.
|
||||
const repoDir = mkdtempSync(path.join(os.tmpdir(), 'gitnexus-rwc-two-wt-'));
|
||||
try {
|
||||
execSync('git init -q', { cwd: repoDir, stdio: 'ignore' });
|
||||
execSync('git config user.email "test@example.com"', { cwd: repoDir, stdio: 'ignore' });
|
||||
execSync('git config user.name "Test"', { cwd: repoDir, stdio: 'ignore' });
|
||||
writeFileSync(path.join(repoDir, 'x.ts'), 'export const x = 1;\n');
|
||||
execSync('git add x.ts', { cwd: repoDir, stdio: 'ignore' });
|
||||
execSync('git commit -q -m "initial"', { cwd: repoDir, stdio: 'ignore' });
|
||||
|
||||
const worktreeA = path.join(repoDir, 'wt-a');
|
||||
const worktreeB = path.join(repoDir, 'wt-b');
|
||||
execSync(`git worktree add -q -b branch-a "${worktreeA}"`, { cwd: repoDir, stdio: 'ignore' });
|
||||
execSync(`git worktree add -q -b branch-b "${worktreeB}"`, { cwd: repoDir, stdio: 'ignore' });
|
||||
|
||||
// repoPath = wt-A (the indexed worktree), launchCwd = wt-B (where the server runs).
|
||||
// resolveWorktreeCwd must return wt-A — the indexed path — unchanged.
|
||||
const result = resolveWorktreeCwd(worktreeA, worktreeB);
|
||||
expect(realpathSync.native(result)).toBe(realpathSync.native(worktreeA));
|
||||
expect(realpathSync.native(result)).not.toBe(realpathSync.native(worktreeB));
|
||||
expect(realpathSync.native(result)).not.toBe(realpathSync.native(repoDir));
|
||||
} finally {
|
||||
try {
|
||||
execSync('git worktree remove -f wt-a', { cwd: repoDir, stdio: 'ignore' });
|
||||
execSync('git worktree remove -f wt-b', { cwd: repoDir, stdio: 'ignore' });
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
rmSync(repoDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ── Guard logic via real path arithmetic ─────────────────────────────────────
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue