mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-29 06:32:28 +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>
214 lines
9.2 KiB
TypeScript
214 lines
9.2 KiB
TypeScript
/**
|
|
* Java import resolution against declared packages (#2953).
|
|
*
|
|
* The property: an import names a type in a package, and what puts a file in a
|
|
* package is that file's own `package` declaration. Every arm below is a case
|
|
* where the answer differs from what the file's PATH suggests — which is the
|
|
* point, because path shape is what the previous resolver used and why it could
|
|
* not tell a JDK import from a local one.
|
|
*/
|
|
import { describe, it, expect } from 'vitest';
|
|
import {
|
|
buildJavaPackageIndex,
|
|
resolveJavaModule,
|
|
type JavaPackageIndex,
|
|
} from '../../src/core/ingestion/languages/java/module-resolution.js';
|
|
import { interpretJavaImport } from '../../src/core/ingestion/languages/java/interpret.js';
|
|
import type { JvmPackageFact } from '../../src/core/ingestion/languages/jvm/package-facts.js';
|
|
import type { Capture, CaptureMatch, ParsedFile } from 'gitnexus-shared';
|
|
|
|
/** Build an index from `filePath -> declared package`. */
|
|
function indexOf(declarations: Readonly<Record<string, string | null>>): JavaPackageIndex {
|
|
const parsedFiles = Object.keys(declarations).map(
|
|
(filePath) => ({ filePath }) as unknown as ParsedFile,
|
|
);
|
|
const packageOf = (filePath: string): JvmPackageFact | undefined => {
|
|
const declared = declarations[filePath];
|
|
if (declared === undefined) return undefined;
|
|
// `null` models a malformed package header, which `extractJvmPackageFact`
|
|
// reports as `unknown` — the file is in no package anyone can name.
|
|
return declared === null ? { status: 'unknown' } : { status: 'known', packageName: declared };
|
|
};
|
|
return buildJavaPackageIndex(parsedFiles, packageOf);
|
|
}
|
|
|
|
function capture(text: string): Capture {
|
|
return { text } as Capture;
|
|
}
|
|
|
|
const WORKSPACE = indexOf({
|
|
'src/main/java/com/example/App.java': 'com.example',
|
|
'src/main/java/com/example/Utils.java': 'com.example',
|
|
'src/main/java/com/example/model/User.java': 'com.example.model',
|
|
'src/main/java/com/example/model/Order.java': 'com.example.model',
|
|
// Path and package deliberately disagree: this is legal Java, and the
|
|
// declaration is what counts.
|
|
'weird/place/Detached.java': 'com.example.detached',
|
|
// A file whose PATH looks like the JDK's but which declares its own package.
|
|
'vendor/java/util/List.java': 'vendor.java.util',
|
|
});
|
|
|
|
describe('type imports', () => {
|
|
it('resolves a type to the file declaring its package', () => {
|
|
expect(resolveJavaModule('com.example.model.User', WORKSPACE)).toBe(
|
|
'src/main/java/com/example/model/User.java',
|
|
);
|
|
});
|
|
|
|
it('resolves a file whose path does not match its package', () => {
|
|
// Nothing about `weird/place/Detached.java` suggests `com.example.detached`
|
|
// except the declaration, which is exactly what a path matcher cannot read.
|
|
expect(resolveJavaModule('com.example.detached.Detached', WORKSPACE)).toBe(
|
|
'weird/place/Detached.java',
|
|
);
|
|
});
|
|
|
|
it('resolves a static member import to the type that owns it', () => {
|
|
// `com.example.Utils` is the type; `format` is a member inside it. The
|
|
// longest DECLARED package prefix is `com.example`, so the split lands
|
|
// there rather than treating `Utils` as a package.
|
|
expect(resolveJavaModule('com.example.Utils.format', WORKSPACE)).toBe(
|
|
'src/main/java/com/example/Utils.java',
|
|
);
|
|
});
|
|
|
|
it('prefers the longest declared package prefix', () => {
|
|
// `com.example` also declares types, so a shorter split would have to be
|
|
// rejected on the type name alone; the longest package wins first.
|
|
expect(resolveJavaModule('com.example.model.Order', WORKSPACE)).toBe(
|
|
'src/main/java/com/example/model/Order.java',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('one package+type declared under several source roots', () => {
|
|
// Not legal within one compilation, but one repository can hold several
|
|
// build modules or sample projects that each declare `models.User`. The
|
|
// import cannot say which one it means; the importer's location does, and a
|
|
// tie refuses rather than binding whichever file was parsed first.
|
|
const ROOTS = indexOf({
|
|
'alpha/src/main/java/models/User.java': 'models',
|
|
'beta/src/main/java/models/User.java': 'models',
|
|
'beta/src/test/java/models/User.java': 'models',
|
|
'beta/src/main/java/services/UserService.java': 'services',
|
|
});
|
|
|
|
it.each<[string, string | undefined, string | null]>([
|
|
[
|
|
'its own module',
|
|
'beta/src/main/java/services/UserService.java',
|
|
'beta/src/main/java/models/User.java',
|
|
],
|
|
[
|
|
'its own source root over a sibling root',
|
|
'beta/src/test/java/services/UserServiceTest.java',
|
|
'beta/src/test/java/models/User.java',
|
|
],
|
|
['another module', 'alpha/src/main/java/app/App.java', 'alpha/src/main/java/models/User.java'],
|
|
['nowhere nearer one copy than another', 'gamma/src/main/java/app/App.java', null],
|
|
['an unknown importer', undefined, null],
|
|
])('resolves an import from %s', (_title, fromFile, expected) => {
|
|
expect(resolveJavaModule('models.User', ROOTS, fromFile)).toBe(expected);
|
|
});
|
|
|
|
it('leaves a type declared once independent of the importer', () => {
|
|
expect(resolveJavaModule('services.UserService', ROOTS, 'gamma/App.java')).toBe(
|
|
'beta/src/main/java/services/UserService.java',
|
|
);
|
|
});
|
|
});
|
|
|
|
describe('imports of things outside the repository (#2953)', () => {
|
|
it('does not resolve a JDK import onto a same-named local file', () => {
|
|
// The reported defect. `vendor/java/util/List.java` ends in `java/util/List`
|
|
// and the old resolver bound `java.util.List` straight to it. No file
|
|
// declares package `java.util`, so there is nothing here to name.
|
|
expect(resolveJavaModule('java.util.List', WORKSPACE)).toBeNull();
|
|
});
|
|
|
|
it('still resolves the local file under the package it DOES declare', () => {
|
|
// The paired positive: the same file is reachable, just not by the JDK's
|
|
// name for it. Without this arm the one above could pass by resolving
|
|
// nothing at all.
|
|
expect(resolveJavaModule('vendor.java.util.List', WORKSPACE)).toBe(
|
|
'vendor/java/util/List.java',
|
|
);
|
|
});
|
|
|
|
it('does not resolve a third-party import', () => {
|
|
expect(resolveJavaModule('org.springframework.stereotype.Service', WORKSPACE)).toBeNull();
|
|
expect(resolveJavaModule('com.google.common.collect.Lists', WORKSPACE)).toBeNull();
|
|
});
|
|
|
|
it('does not resolve a declared package with an undeclared type', () => {
|
|
// `com.example` is real; `Missing` is not. A partial match is not a match.
|
|
expect(resolveJavaModule('com.example.Missing', WORKSPACE)).toBeNull();
|
|
});
|
|
|
|
it('does not resolve a bare single-segment name', () => {
|
|
// Java forbids importing from the default package, so there is no spelling
|
|
// of a one-segment import that names an in-repo type.
|
|
expect(resolveJavaModule('App', WORKSPACE)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('wildcard imports', () => {
|
|
it('answers with EVERY file in the package, not one', () => {
|
|
// The old resolver returned the first `.java` in the package directory in
|
|
// file-set iteration order — an answer decided by the file list rather than
|
|
// by the import.
|
|
expect(resolveJavaModule('com.example.model.*', WORKSPACE)).toEqual([
|
|
'src/main/java/com/example/model/User.java',
|
|
'src/main/java/com/example/model/Order.java',
|
|
]);
|
|
});
|
|
|
|
it('keeps a static wildcard on a type distinct from a same-named package', () => {
|
|
const index = indexOf({
|
|
'src/main/java/com/example/Utils.java': 'com.example',
|
|
'src/main/java/com/example/Utils/Nested.java': 'com.example.Utils',
|
|
});
|
|
const parsed = interpretJavaImport({
|
|
'@import.kind': capture('static-wildcard'),
|
|
'@import.source': capture('com.example.Utils'),
|
|
'@import.name': capture('*'),
|
|
} as CaptureMatch);
|
|
|
|
expect(parsed).toEqual({ kind: 'wildcard', targetRaw: 'com.example.Utils' });
|
|
if (parsed === null || parsed.targetRaw === null) throw new Error('import was not interpreted');
|
|
expect(resolveJavaModule(parsed.targetRaw, index)).toBe('src/main/java/com/example/Utils.java');
|
|
expect(resolveJavaModule('com.example.Utils.*', index)).toEqual([
|
|
'src/main/java/com/example/Utils/Nested.java',
|
|
]);
|
|
});
|
|
|
|
it('resolves nothing for a wildcard on an undeclared package', () => {
|
|
expect(resolveJavaModule('java.util.*', WORKSPACE)).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe('the index itself', () => {
|
|
it('counts files whose package header could not be read', () => {
|
|
const index = indexOf({ 'src/Broken.java': null, 'src/Fine.java': 'ok.pkg' });
|
|
|
|
expect(index.unreadablePackageFiles).toBe(1);
|
|
// The unreadable file is in no package, so nothing can import it — but the
|
|
// readable one beside it is unaffected.
|
|
expect(resolveJavaModule('ok.pkg.Fine', index)).toBe('src/Fine.java');
|
|
});
|
|
|
|
it('indexes the default package but refuses imports from it', () => {
|
|
const index = indexOf({ 'Root.java': '' });
|
|
|
|
// Same-package visibility still needs the file bucketed…
|
|
expect(index.filesByPackage.get('')).toEqual(['Root.java']);
|
|
// …but `import Root;` is not legal Java and resolves to nothing.
|
|
expect(resolveJavaModule('Root', index)).toBeNull();
|
|
});
|
|
|
|
it('is empty for an empty workspace, and resolves nothing', () => {
|
|
const empty = buildJavaPackageIndex([], () => undefined);
|
|
|
|
expect(resolveJavaModule('com.example.model.User', empty)).toBeNull();
|
|
});
|
|
});
|