mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-11 08:37:35 +00:00
* fix(scope): bind same-namespace inheritance bases before sibling visibility exists Cause: same-package / same-namespace visibility (`populateNamespaceSiblings`) is populated after the inheritance pre-pass and `buildMro`. A base written without an import (`class C extends B`, B in another file of the package) could bind only through the workspace-unique simple-name fallback, which any other package declaring a `B` defeats: no EXTENDS edge, an empty MRO, and lost calls through inherited members. In the combined lang-resolution corpus `java-grandparent-resolution` `c.greet().save()` dropped because `java-unrelated-method-collision` declares default-package A and B. Moving the sibling hook ahead of the pre-pass was measured and rejected. It retargeted correct edges and minted wrong ones, because the sibling channels carry candidates the pre-pass must not see: C# global-namespace types are consulted at every scope (same-file `Cat : Animal` bound another fixture's global Animal), namespace buckets merge across source roots with no proximity order (PHP, C#), and a qualified `extends app.base.Box` bound through a same-package `app.Box`. Fix: an optional `ScopeResolver.resolveImplicitInheritanceBase`, consulted by the pre-pass only when the shared chain bound nothing, the base was written bare (no qualifier, name capture at the start of the base expression), and no import claims the name. It adds edges and never retargets one. `resolveSameNamespaceInheritanceBase` picks the candidate in the deriving class's namespace, nearest by shared path prefix, refusing a tie. Java and Kotlin answer from their package facts, C# from the exact `namespacePrefix`. Corpus A/B (main vs this, same corpus): EXTENDS/IMPLEMENTS +42, all within their own fixture, 0 removed. CALLS: six `super.save()` edges keep their target but are now MRO-dispatched (`global`, as in isolation), one moves off another fixture's BaseModel.Save onto its own, and 7 are new (both grandparent `greet` calls, a default-method `validate`, the new fixtures). Receiver bench: call drops 128 -> 127, all kinds 192 -> 191, shape arm unchanged. Scope-capture fingerprints re-baselined for the added fixtures only (parked, --check passes on the old ones). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(java): resolve a duplicated package+type import to the importer's own root Cause: `buildJavaPackageIndex` kept the first file declaring each package+type. One repository can hold several build modules or sample projects that each declare `models.User`, so every `import models.User;` bound whichever copy was parsed first, a wrong IMPORTS edge at full confidence that carries the receiver types behind it. In the combined lang-resolution corpus 24 fixtures declare `models.User`, and e.g. `java-heritage` UserService.processUser called `java-assignment-chain/models/User.save`. Fix: the index keeps every declaring file in a sparse `duplicateTypes` map (only colliding pairs, so a normal workspace pays nothing), and a type import resolves to the declaring file sharing the longest leading path with the importer (its own module or source root). A tie, or an unknown importer, refuses rather than guessing. Wildcard imports are unchanged. Corpus A/B (previous commit vs this, same corpus): 40 CALLS edges that crossed into another fixture's User/Repo/Address/UserService are gone, 55 CALLS/ACCESSES edges now land in their own fixture (0 cross-fixture), and `java-parent-resolution` User IMPLEMENTS Serializable moves off `java-heritage` onto its own interface. Receiver-unresolved drops unchanged; import-target and receiver benches pass. Scope-capture java fingerprint re-baselined for the added fixture only (parked, --check passes on the old one). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(scope): respect base candidate visibility * fix(scope): match written JVM base arity --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
100 lines
5.1 KiB
TypeScript
100 lines
5.1 KiB
TypeScript
import { afterAll, beforeAll, describe, expect, it } from 'vitest';
|
|
import fs from 'node:fs';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import {
|
|
getRelationships,
|
|
runPipelineFromRepo,
|
|
writeFixtureRepo,
|
|
type PipelineResult,
|
|
} from './resolvers/helpers.js';
|
|
|
|
describe('review probes for candidate visibility', () => {
|
|
let root: string;
|
|
let result: PipelineResult;
|
|
beforeAll(async () => {
|
|
root = fs.mkdtempSync(path.join(os.tmpdir(), 'gn-source-visibility-'));
|
|
writeFixtureRepo(root, {
|
|
'alpha/src/main/java/models/User.java':
|
|
'package models; public class User { public void save() {} }',
|
|
'beta/src/test/java/models/User.java':
|
|
'package models; public class User { public void save() {} }',
|
|
'beta/src/main/java/services/Service.java':
|
|
'package services; import models.User; public class Service { void run(User value) { value.save(); } }',
|
|
'alpha/src/main/java/parents/Base.java': 'package parents; public class Base {}',
|
|
'beta/src/test/java/parents/Base.java': 'package parents; public class Base {}',
|
|
'beta/src/main/java/parents/Child.java':
|
|
'package parents; public class Child extends Base {}',
|
|
'gamma/src/main/java/nested/A.java':
|
|
'package nested; public class A { public static class B {} }',
|
|
'gamma/src/main/java/nested/C.java':
|
|
'package nested; public class C extends A { public static class D extends B {} }',
|
|
'gamma/src/main/java/nested/B.java': 'package nested; public class B {}',
|
|
'gamma/src/main/java/other/B.java': 'package other; public class B {}',
|
|
});
|
|
writeFixtureRepo(root, {
|
|
'alias/p/B.kt': 'package aliasp\nopen class B',
|
|
'alias/p/C.kt': 'package aliasp\nimport aliasq.B\nclass AliasChild : B()',
|
|
'alias/q/Types.kt': 'package aliasq\nopen class Real\ntypealias B = Real',
|
|
'claims/p/B.java': 'package claimsp; public class B {}',
|
|
'claims/q/B.java': 'package claimsq; public class B {}',
|
|
'claims/p/C.java':
|
|
'package claimsp; import external.B; public class ExternalChild extends B {}',
|
|
'beta/src/test/java/services/TestService.java':
|
|
'package services; import models.User; public class TestService { void run(User value) { value.save(); } }',
|
|
'beta/src/test/java/parents/TestChild.java':
|
|
'package parents; public class TestChild extends Base {}',
|
|
'alpha/src/main/kotlin/ktparents/Base.kt': 'package ktparents\nopen class Base',
|
|
'beta/src/test/kotlin/ktparents/Base.kt': 'package ktparents\nopen class Base',
|
|
'beta/src/main/kotlin/ktparents/Child.kt': 'package ktparents\nclass Child : Base()',
|
|
});
|
|
result = await runPipelineFromRepo(root, () => {}, {});
|
|
}, 120000);
|
|
afterAll(() => {
|
|
if (root) fs.rmSync(root, { recursive: true, force: true });
|
|
});
|
|
it('does not dispatch production receivers to a test-only duplicate', () => {
|
|
const edges = getRelationships(result, 'CALLS').filter(
|
|
(e) => e.sourceFilePath === 'beta/src/main/java/services/Service.java' && e.target === 'save',
|
|
);
|
|
expect(edges.map((e) => e.targetFilePath)).toEqual(['alpha/src/main/java/models/User.java']);
|
|
});
|
|
it('does not bind a production base to a test-only duplicate', () => {
|
|
const edges = getRelationships(result, 'EXTENDS').filter(
|
|
(e) => e.source === 'Child' && e.sourceFilePath.endsWith('.java'),
|
|
);
|
|
expect(edges.map((e) => e.targetFilePath)).toEqual(['alpha/src/main/java/parents/Base.java']);
|
|
});
|
|
it('does not override a resolved Kotlin typealias import with a package sibling', () => {
|
|
const edges = getRelationships(result, 'EXTENDS').filter((e) => e.source === 'AliasChild');
|
|
expect(edges).not.toContainEqual(expect.objectContaining({ targetFilePath: 'alias/p/B.kt' }));
|
|
});
|
|
it('does not override an unresolved explicit Java import with a package sibling', () => {
|
|
expect(getRelationships(result, 'EXTENDS').filter((e) => e.source === 'ExternalChild')).toEqual(
|
|
[],
|
|
);
|
|
});
|
|
it('preserves the nearer test declaration for a test importer', () => {
|
|
const edges = getRelationships(result, 'CALLS').filter(
|
|
(e) =>
|
|
e.sourceFilePath === 'beta/src/test/java/services/TestService.java' && e.target === 'save',
|
|
);
|
|
expect(edges.map((e) => e.targetFilePath)).toEqual(['beta/src/test/java/models/User.java']);
|
|
});
|
|
it('preserves the nearer test base for a test deriving class', () => {
|
|
const edges = getRelationships(result, 'EXTENDS').filter((e) => e.source === 'TestChild');
|
|
expect(edges.map((e) => e.targetFilePath)).toEqual(['beta/src/test/java/parents/Base.java']);
|
|
});
|
|
it('excludes test-only Kotlin bases from production inheritance', () => {
|
|
const edges = getRelationships(result, 'EXTENDS').filter(
|
|
(e) => e.source === 'Child' && e.sourceFilePath.endsWith('.kt'),
|
|
);
|
|
expect(edges.map((e) => e.targetFilePath)).toEqual(['alpha/src/main/kotlin/ktparents/Base.kt']);
|
|
});
|
|
it('does not replace an inherited nested base with a package-level homonym', () => {
|
|
const edges = getRelationships(result, 'EXTENDS').filter((e) => e.source === 'D');
|
|
expect(edges).not.toContainEqual(
|
|
expect.objectContaining({ targetFilePath: 'gamma/src/main/java/nested/B.java' }),
|
|
);
|
|
});
|
|
});
|