mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-11-11 08:37:35 +00:00
* fix(scope): follow namespace factory return types and record untyped drops `import mod as rp; x = rp.make(); x.m()` lost the receiver type even when `make` is declared `-> C`: `x` was bound to the raw dotted name `rp.make`, and nothing followed a dotted name to a function's return type. The inline `rp.make().m()` failed the same way in the compound resolver, which read `ns.f()` only as a construction. Resolve `ns.f` to `f`'s return annotation when `ns` is a namespace import, the fact `propagateImportedReturnTypes` already mirrors for a named import. The post-finalize chain follow uses it for bindings and the compound resolver for inline calls. Only sites that emitted nothing change: the full lang-resolution corpus emits the identical 2,838 CALLS/ACCESSES edges. Record the drops that left `impact` at "exact": - a plain receiver bound to a namespace function with no usable return type (`x = ns.untyped()`); - an outer call whose `file:line:col` key a resolved inner site already claimed (`ns.untyped().m()`, `s.repo.Delete(id)`). Only a duplicate capture of the same member hides a drop now. Receiver baseline: call drops 128 -> 151, reads 44 -> 48, ten shape-arm cells INVISIBLE-GAP -> VISIBLE-GAP; every new drop has no CALLS edge to its member (BASELINE.md lists them). The python.factory-qualified accuracy gap is removed; the python-scope fingerprint and capture golden gain the new fixture only. Fixes #3498 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(dart): type locals from prefixed-import factory calls `import 'mod.dart' as rp; final x = rp.make(); x.m()` gave `x` no type: local call inference accepted only a bare callee (`Callee()`), and the prefix-stripping type normalizer would have reduced `rp.make` to `make`. Bind a local initialized by exactly `prefix.member(...)` (direct or awaited) to the dotted callee and keep its prefix, so the shared chain follow types it from the namespace function's return annotation. Only call-derived bindings keep the prefix; annotated `prefix.Type` spellings are unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(zig): type locals from namespace-module factory calls `const rp = @import("mod.zig"); const x = rp.make(); x.m()` bound `x` to the namespace handle `rp` itself: the call-return rule reads a non-value head as the type being constructed (`Counter.init()`), and import bindings are not values. The member name was gone before the shared chain follow could use the new namespace return-type lookup. For a module-level handle on a lowercase file (a namespace module, by Zig convention), bind the dotted callee `rp.make` so the shared chain follow types it from `make`'s return annotation. File-struct handles (`@import("Page.zig")`, any spelling of the handle) keep naming the struct, so `Page.init()` is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(bench): rebaseline captures and receiver counts for Dart and Zig - receiver-resolution: the Dart fixture's untyped factory adds one call drop (151 -> 152); every typed form in the Dart and Zig fixtures resolves. - scope-capture (dart): two existing dart-method-enrichment locals initialized by `obj.method()` / `Type.method()` gain a call-derived binding, plus the new fixture's captures. A full lang-resolution A/B emits the identical edges outside the new fixtures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * 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> * refactor(scope): expose the position a receiver-chain fold reaches `foldReceiverChain` answers "which class", so a chain whose last step is declared as a type the program does not contain (`Dictionary<string, User>`, `String`) reads only as a miss. That miss is informative to the receiver-origin classifier: every hop was typed by a program declaration and the last one names a library type. Extract the fold loop into `foldReceiverChainState` and export `foldReceiverChainPosition`, which returns the final position including one whose declared type named no class, and reports nothing to `recordReceiverType`. `foldReceiverChain` keeps its behaviour: it now wraps the same loop and makes the receiver-type report from the final state, as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(scope): classify library-rooted receiver drops from provider evidence `classifyReceiverOrigin` had one source of positive external evidence, `LanguageProvider.isBuiltInName`. That set also filters callee names out of the call graph, so it could not grow to cover host globals, stdlib imports or library-typed values without dropping real edges, and every such drop hedged `impact()` to `lower-bound`. It also read a base name as evidence even where the program bound that name itself: a parameter named `console` with an unresolved type, or an unresolved import of a built-in spelling from the program's own module, came back `external`. Add two provider data hooks read only by the classifier: `externalReceiverNames` (globals, platform roots, library types) and `isExternalModuleSpecifier` (import specifiers reserved for the runtime or stdlib). The classifier now answers `external` when: - the base is unbound at the site and is a built-in or listed name; - the base is bound only by imports that resolved to no program file and all name a library module; - the receiver chain folds, through declarations each owner makes, to a position declared as a library type (`this.data.Keys` with `Dictionary<string, User> data`), and the program declares nothing by the dropped member's name (an extension method or trait impl could be the real target). The fold skips ancestor-hoisted bindings, which can lend another class's member type to the step. A base the program binds (a parameter, a local, a resolved import, an import from a non-library module) never takes a name-based verdict. The in-program arms are unchanged and still win first. No drop is added or removed and no edge changes. Java supplies its reserved `java` package root for both hooks: the JVM refuses to define classes under `java.*`, so `java.util.Optional.of(...)` and `import java.util.Map` are rooted in the platform. Without the specifier hook, the stricter import rule would have demoted the existing `import java.util.List; List.of(...)` verdict to unknown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(typescript): name JavaScript host globals as the program boundary `document.body.appendChild(...)`, `window.history.pushState(...)` and `Buffer.from(raw).toString()` were recorded `unknown`: none of `document`, `window`, `Buffer`, `process` or `globalThis` is in `BUILT_INS`, and they cannot be added there because that set also filters callee names, so `process` would drop every call to a program function named `process`. Each drop hedged `impact()` to `lower-bound` although no program node could have been the target. List the host objects in `EXTERNAL_RECEIVER_NAMES` and supply it to the TypeScript, JavaScript and Vue providers as `externalReceiverNames`. A name counts only where the program binds nothing by it and declares nothing by it, so a `document: Doc` parameter, an untyped `window` parameter, a local `navigator` and a program `class Buffer` keep their verdicts. `location`, `history`, `self` and `crypto` stay out: they are ordinary local names. Corpus: 5 call drops move from unknown to external. `process.env.*` (3 reads) stays in-program there because three fixtures declare a top-level `function process()`, one of them in a global script. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(python): name standard-library imports as the program boundary `os.environ.setdefault(...)` after `import os`, and `Path(__file__).resolve()` after `from pathlib import Path`, were recorded `unknown`. The imports resolve to no file in the program, but an unresolved import alone cannot be told apart from an in-program module the resolver missed, and nothing said that `os` and `pathlib` belong to the interpreter. Supply `isExternalModuleSpecifier` from a curated set of standard-library modules, matched on the specifier's first segment (`os.path` -> `os`). A relative specifier never matches. The classifier still requires the import to have resolved to no program file, so a local `os.py` that the resolver finds wins, and a parameter named `os` is the program's own. Generic nouns an application plausibly uses for its own modules (`types`, `queue`, `signal`, `secrets`, `string`, `email`, `http`, ...) are left out. Third-party packages are not listed: an unresolved `fastapi` import stays `unknown`. Corpus: 4 call drops move from unknown to external (`os.environ.setdefault` x3, `Path(__file__).resolve()`). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(zig): name @import("std") and @import("builtin") as the program boundary `std.sort.pdq(...)` and `const Allocator = std.mem.Allocator;` were recorded `unknown`: `std` is bound by `const std = @import("std")`, an import the resolver deliberately leaves external, and nothing told the classifier that the module is the standard library rather than an in-program file it failed to find. Supply `isExternalModuleSpecifier` for the two module names Zig reserves: `std` and the compiler-generated `builtin`. Exact names only, so `@import("std.zig")` (a file path) stays `unknown`, as does `root` (the program's own root module) and any module `build.zig` names. Corpus: 9 drops move from unknown to external: `std.sort.pdq` (2 calls, 2 reads), `std.mem.Allocator` (1 read), and the `zig-qualified-literal` literals `std.Thread.Mutex{}` and `std.mem.Allocator{...}` (2 calls, 2 reads). The last four are that fixture's edge negative control (`useExternal` must emit no edge, which still holds); their receiver is std, so `external` is the right origin. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(csharp): name the System namespace and Dictionary as the program boundary `System.Console.WriteLine(...)` was recorded `unknown`: `Console` is in `BUILT_INS` but the chain base is `System`, which is not, and `this.data.Keys` with `Dictionary<string, User> data` was recorded `in-program` because the base `this` resolves to a program class even though the member lives on a BCL type. Supply `externalReceiverNames` with `System` (the BCL root namespace), `Dictionary` (the one collection capture does not reduce to its element, because it takes two type arguments) and `string` (a keyword alias that no program can declare). The base check covers `System.Console`; the chain fold reaches `Dictionary<string, User>` through the program's own field declaration. A program `class System`, a field of a program type, and an extension method the program declares on `Dictionary` all keep their verdicts. Corpus: 3 call drops move from unknown to external (`System.Console.WriteLine`). The two `this.data.Keys/Values` reads in `csharp-nested-member-foreach` stay in-program there: the fixture's global-namespace `App` is one of several in the merged corpus, so `this` binds another fixture's `App` and the fold declines. Analysed alone, both read `external`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(kotlin): name java.lang.System and the platform roots as the program boundary `System.out.println(...)` in Kotlin was recorded `unknown`: Kotlin's `BUILT_INS` lists stdlib function names, not `System`, which Kotlin/JVM imports by default from `java.lang`. Supply `externalReceiverNames` with `System` and the package roots no program can declare into (`java`, refused by the JVM; `kotlin`, reserved by the compiler for the standard library), and `isExternalModuleSpecifier` for imports under those roots. A program `object System` keeps the receiver `in-program`. Corpus: 1 call drop moves from unknown to external (`kotlin-object-literal-scope` `System.out.println`, whose decoy `println` on the enclosing object still gets no edge). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(bench): rebaseline receiver-origin buckets for library receivers The preceding commits label library- and runtime-rooted drops `external` instead of `unknown`. Call drops by origin move from 55 in-program, 44 external and 29 unknown to 55, 62 and 11. Total drops (192), call drops (128), every per-extension and per-shape count, and every shape-arm state are unchanged; a same-corpus A/B against main keeps all 2,838 CALLS/ACCESSES edges byte-identical. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(rust): type locals from module-qualified factory calls `mod modx; use crate::modx as rp; let x = rp::make(); x.m()` left `x` untyped although `make` is declared `-> C`. The call-return rule kept only the text before the first `::` (the `Foo::new()` idiom), so `x` was bound to the module `rp` and the function name was lost. `modx::make()` and `crate::modx::make()` failed the same way, and the inline `rp::make().m()` failed in the compound resolver, which cannot split a `::` path. With a `let modx = Other;` in scope, `modx::make()` even typed `x` as `Other` and emitted a wrong `Other.m` edge. - Rust keeps the whole callee path when every segment is lower-case: a module function, by rustc's lint-enforced casing. `Foo::new()` and `Self::new()` still read as the type. - New `ScopeResolver.resolveQualifiedCallee` hook. The shared chain follow, the compound call branch and the drop recorder hand a callee text no scope binds to the provider, which returns the callable it names; the shared side reads that callable's return annotation by definition identity and never splits the path. Rust answers with the module-tree walk behind `resolveQualifiedFreeCall`, so a call's type and its CALLS edge come from the same resolution. - A receiver bound to an in-program module function with no return annotation (`x = rp::untyped()`) records a `receiver-unresolved` drop, as #3498 does for `ns.untyped()`. - `rp::make()` did not resolve as a call either: the module-name filter rejected the alias `rp`, and under Cargo the import binds the module's `mod modx;` declaration rather than `modx.rs`. The `use` candidate now admits an alias that names a module and reads a module from its top-level declaration. A full lang-resolution A/B against the parent emits identical CALLS and ACCESSES edges outside the new fixture, except one new correct edge: `rust-ambiguous` `main -> models::Handler.handle`, typed from `let h = services::create_handler()`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(bench): rebaseline rust captures and receiver counts - receiver-resolution: the new rust-qualified-factory-return fixture's two untyped sites (`x = rp::untyped(); x.m()` and inline `modx::untyped().m()`) are each a call and a callee-position read drop: call drops 152 -> 154, reads 48 -> 50, all kinds 220 -> 224. No other drop moves in a same-corpus A/B, and shape-arm cells are unchanged. - scope-capture (rust): corpus growth only. emitRustScopeCaptures is untouched, and parking the new fixture reproduces the prior fingerprint byte-for-byte; it adds 131 capture groups and 3 fixture entries. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(scope): ask resolveQualifiedCallee about dotted callee paths A dotted call-return binding (`x -> com.acme.mod.make`) whose head no namespace import binds came back from followChainPostFinalize untouched. The dotted branch tried only namespaceCallReturnType, which needs a two-segment path headed by a namespace import. The inline `com.acme.mod.make().m()` failed the same way in the compound resolver: once the object part resolved to no class and named no namespace import, the call branch returned nothing. A language whose package paths are dotted and never imported (Kotlin) could type neither form. Both sites now pass the dotted text to the provider's resolveQualifiedCallee after their own lookups miss, as they already did for undotted callees, and read the returned callable's return annotation by definition identity. The contract documents the two new consult points, and says that a provider must refuse a path whose head the call site can see as a value or type. No provider answers dotted text yet: Rust's hook reads only `::` paths. No edge moves, and the Rust, Python, Dart and Zig resolver suites pass unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(kotlin): type locals from package-qualified factory calls `val x = kqfmod.make(); x.m()` left `x` untyped although `make` is declared `: C`. `com.acme.mod.make()` failed the same way, and the inline `kqfmod.make().m()` dropped. Three gaps lined up: - the constructor-inferred query matches only a bare callee; - the navigation-call inference handles only typed locals and same-file classes; - nothing resolved a package name, because Kotlin imports declarations, not packages, so no import edge names `kqfmod`. The named-import form (`import kqfmod.make; make()`) already worked. - The local-assignment synthesis binds `x` to the whole callee path when the callee is a dotted path of plain names. The head must be lower-case and must not be declared or imported in the file as a value or type. A `@type.qualified-callee` marker stops interpretKotlinTypeBinding from reducing the path to the function name. Typed-local and same-file-class receivers (`repo.find()`, `Logger.create()`) are tried first, so they keep their meaning. - New Kotlin resolveQualifiedCallee. The package is everything before the last segment. A package spans files, and across them it must declare exactly one top-level function with that name. The index of package facts and module bindings is built once per pass. The hook refuses overloads. It also refuses a head the call site can see: anything the lexical claim walk finds, plus top-level names in same-package sibling files and star-imported packages, which that walk misses. - An untyped package function (`val x = kqfmod.untyped()`) now records a receiver-unresolved drop instead of an exact miss. New fixture kotlin-package-qualified-factory covers: - single- and multi-segment packages, and inline calls; - the untyped drop and the named-import control; - local, parameter, property, import, star-import and same-package values named like the package; - overloads, and companion/object member calls. A full lang-resolution A/B against the parent emits identical CALLS/ACCESSES edges and drops outside the new fixture. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(bench): rebaseline kotlin captures and receiver counts - receiver-resolution: the new kotlin-package-qualified-factory fixture records six drops, all in app/Callers.kt. Two are call drops on untyped receivers: `x.m()` on `val x = kqfmod.untyped()` and the inline `kqfmod.untyped().m()`. The other four come from `com.kqfacme.deep.make()` in deepCall and inlineDeep: each is a call drop plus a read drop on the `deep` receiver path. Call drops 154 -> 158, reads 50 -> 52, all kinds 224 -> 230. A same-corpus A/B shows no other drop change. Shape-arm cells are unchanged. - scope-capture (kotlin): moving the new fixture out of the corpus reproduces the prior fingerprint byte-for-byte. Base and new emitters differ by exactly six matches over the same corpus, all of them in the new fixture. Synthetic counts are unchanged, and scaling is 1.039 < 1.5. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * perf(scope): reuse qualified call file sets * docs(scope): clarify finalized type refinement * test(kotlin): sync measured capture counts * fix(rust): scope generic alias parameters * fix(scope): respect base candidate visibility * test(scope): preserve library member collision hedge * fix(scope): cover more library receiver evidence * fix(vue): classify imported runtime receivers * fix(scope): match written JVM base arity * fix(kotlin): refuse unproven package receivers * fix(kotlin): prove companion receiver headers * test(kotlin): complete JVM scope fixtures * fix(kotlin): unify package receiver shadow checks --------- Co-authored-by: Gergo Magyar <gergomagyar0@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
322 lines
13 KiB
TypeScript
322 lines
13 KiB
TypeScript
import type { ScopeResolutionIndexes } from '../../src/core/ingestion/model/scope-resolution-indexes.js';
|
|
import type { ParsedFile, SymbolDefinition } from 'gitnexus-shared';
|
|
import { describe, expect, it } from 'vitest';
|
|
import { createJvmPackageSiblingVisibility } from '../../src/core/ingestion/languages/jvm/package-siblings.js';
|
|
import { collectJavaCaptureSideChannel } from '../../src/core/ingestion/languages/java/capture-side-channel.js';
|
|
import { emitJavaScopeCaptures } from '../../src/core/ingestion/languages/java/captures.js';
|
|
import {
|
|
isJavaPackageSiblingVisibilityIncomplete,
|
|
populateJavaPackageSiblings,
|
|
} from '../../src/core/ingestion/languages/java/package-siblings.js';
|
|
import { setJavaPackageFact } from '../../src/core/ingestion/languages/java/package-facts.js';
|
|
import { collectKotlinCaptureSideChannel } from '../../src/core/ingestion/languages/kotlin/capture-side-channel.js';
|
|
import { emitKotlinScopeCaptures } from '../../src/core/ingestion/languages/kotlin/captures.js';
|
|
import {
|
|
isKotlinPackageSiblingVisibilityIncomplete,
|
|
populateKotlinPackageSiblings,
|
|
} from '../../src/core/ingestion/languages/kotlin/package-siblings.js';
|
|
import { setKotlinPackageFact } from '../../src/core/ingestion/languages/kotlin/package-facts.js';
|
|
import type { JvmPackageFact } from '../../src/core/ingestion/languages/jvm/package-facts.js';
|
|
|
|
interface LanguagePackageHarness {
|
|
readonly label: string;
|
|
readonly extension: string;
|
|
readonly namedSource: string;
|
|
readonly defaultSource: string;
|
|
readonly malformedSource: string;
|
|
readonly brokenBodySource: string;
|
|
readonly emit: (source: string, filePath: string) => unknown;
|
|
readonly collect: (filePath: string) => { packageFact: JvmPackageFact } | undefined;
|
|
readonly setFact: (filePath: string, fact: JvmPackageFact) => void;
|
|
readonly populate: (
|
|
parsedFiles: readonly ParsedFile[],
|
|
indexes: ScopeResolutionIndexes,
|
|
context: { fileContents: ReadonlyMap<string, string> },
|
|
) => void;
|
|
readonly isIncomplete: (filePath: string) => boolean;
|
|
}
|
|
|
|
const harnesses: readonly LanguagePackageHarness[] = [
|
|
{
|
|
label: 'Java',
|
|
extension: '.java',
|
|
namedSource: 'package com.example;\nclass Named {}',
|
|
defaultSource: 'class Default {}',
|
|
malformedSource: 'package ;\nclass Malformed {}',
|
|
brokenBodySource: 'package com.valid;\nclass Broken { void f( }',
|
|
emit: emitJavaScopeCaptures,
|
|
collect: collectJavaCaptureSideChannel,
|
|
setFact: setJavaPackageFact,
|
|
populate: populateJavaPackageSiblings,
|
|
isIncomplete: isJavaPackageSiblingVisibilityIncomplete,
|
|
},
|
|
{
|
|
label: 'Kotlin',
|
|
extension: '.kt',
|
|
namedSource: 'package com.example\nclass Named',
|
|
defaultSource: 'class Default',
|
|
malformedSource: 'package ;\nclass Malformed',
|
|
brokenBodySource: 'package com.valid\nclass Broken { fun f( }',
|
|
emit: emitKotlinScopeCaptures,
|
|
collect: collectKotlinCaptureSideChannel,
|
|
setFact: setKotlinPackageFact,
|
|
populate: populateKotlinPackageSiblings,
|
|
isIncomplete: isKotlinPackageSiblingVisibilityIncomplete,
|
|
},
|
|
];
|
|
|
|
/** A file with only a module scope — `moduleWithClass` without the class half. */
|
|
function parsedFile(filePath: string, index: number): ParsedFile {
|
|
return moduleWithClass(filePath, index);
|
|
}
|
|
|
|
function emptyIndexes(): ScopeResolutionIndexes {
|
|
return { bindingAugmentations: new Map() } as unknown as ScopeResolutionIndexes;
|
|
}
|
|
|
|
for (const harness of harnesses) {
|
|
describe(`${harness.label} JVM package facts`, () => {
|
|
it('captures named/default packages and isolates package-header errors', () => {
|
|
const namedPath = `src/Named${harness.extension}`;
|
|
harness.emit(harness.namedSource, namedPath);
|
|
expect(harness.collect(namedPath)?.packageFact).toEqual({
|
|
status: 'known',
|
|
packageName: 'com.example',
|
|
});
|
|
|
|
const defaultPath = `src/Default${harness.extension}`;
|
|
harness.emit(harness.defaultSource, defaultPath);
|
|
expect(harness.collect(defaultPath)?.packageFact).toEqual({
|
|
status: 'known',
|
|
packageName: '',
|
|
});
|
|
|
|
const malformedPath = `src/Malformed${harness.extension}`;
|
|
harness.emit(harness.malformedSource, malformedPath);
|
|
expect(harness.collect(malformedPath)?.packageFact).toEqual({ status: 'unknown' });
|
|
|
|
const brokenBodyPath = `src/BrokenBody${harness.extension}`;
|
|
harness.emit(harness.brokenBodySource, brokenBodyPath);
|
|
expect(harness.collect(brokenBodyPath)?.packageFact).toEqual({
|
|
status: 'known',
|
|
packageName: 'com.valid',
|
|
});
|
|
});
|
|
|
|
it('marks a capped package incomplete without affecting other package names', () => {
|
|
const source = harness.namedSource;
|
|
const parsedFiles = Array.from({ length: 501 }, (_, index) => {
|
|
const filePath = `src/com/capped/Type${index}${harness.extension}`;
|
|
harness.setFact(filePath, { status: 'known', packageName: 'com.capped' });
|
|
return parsedFile(filePath, index);
|
|
});
|
|
const fileContents = new Map(parsedFiles.map((parsed) => [parsed.filePath, source]));
|
|
|
|
harness.populate(parsedFiles, emptyIndexes(), { fileContents });
|
|
|
|
expect(harness.isIncomplete(parsedFiles[0].filePath)).toBe(true);
|
|
expect(harness.isIncomplete(`src/other/Complete${harness.extension}`)).toBe(false);
|
|
});
|
|
|
|
it('fails wildcard visibility closed when a source file produced no ParsedFile', () => {
|
|
const first = parsedFile(`src/A${harness.extension}`, 1);
|
|
const second = parsedFile(`src/B${harness.extension}`, 2);
|
|
harness.setFact(first.filePath, { status: 'known', packageName: 'com.example' });
|
|
harness.setFact(second.filePath, { status: 'known', packageName: 'com.example' });
|
|
const skippedPath = `src/Skipped${harness.extension}`;
|
|
const fileContents = new Map([
|
|
[first.filePath, harness.namedSource],
|
|
[second.filePath, harness.namedSource],
|
|
[skippedPath, harness.malformedSource],
|
|
]);
|
|
|
|
harness.populate([first, second], emptyIndexes(), { fileContents });
|
|
|
|
expect(harness.isIncomplete(first.filePath)).toBe(true);
|
|
expect(harness.isIncomplete(second.filePath)).toBe(true);
|
|
});
|
|
});
|
|
}
|
|
|
|
// ─── sibling injection cap (#2732) ───────────────────────────────────
|
|
//
|
|
// Driven through the shared JVM factory rather than a language facade: the
|
|
// cap is language-agnostic, and a synthetic fixture can place candidates at
|
|
// chosen path distances without hand-writing hundreds of real sources.
|
|
|
|
function classDef(nodeId: string, filePath: string, qualifiedName: string): SymbolDefinition {
|
|
return { nodeId, filePath, type: 'Class', qualifiedName } as unknown as SymbolDefinition;
|
|
}
|
|
|
|
function moduleWithClass(
|
|
filePath: string,
|
|
index: number,
|
|
def?: SymbolDefinition,
|
|
moduleTypeBindings?: ReadonlyMap<string, unknown>,
|
|
): ParsedFile {
|
|
const moduleId = `module:${index}`;
|
|
const scopes: Record<string, unknown>[] = [
|
|
{
|
|
id: moduleId,
|
|
kind: 'Module',
|
|
bindings: new Map(),
|
|
typeBindings: new Map(moduleTypeBindings ?? []),
|
|
ownedDefs: [],
|
|
},
|
|
];
|
|
if (def !== undefined) {
|
|
scopes.push({
|
|
id: `class:${index}`,
|
|
kind: 'Class',
|
|
parent: moduleId,
|
|
bindings: new Map(),
|
|
typeBindings: new Map(),
|
|
ownedDefs: [def],
|
|
});
|
|
}
|
|
return { filePath, scopes } as unknown as ParsedFile;
|
|
}
|
|
|
|
function jvmVisibility(facts: ReadonlyMap<string, JvmPackageFact>) {
|
|
return createJvmPackageSiblingVisibility({
|
|
languageLabel: 'JVM',
|
|
getPackageFact: (filePath) => facts.get(filePath),
|
|
});
|
|
}
|
|
|
|
/**
|
|
* `count` siblings of `com.example`, interleaved near/far so the retained set
|
|
* cannot be produced by simply truncating the input order: every odd index is
|
|
* a distant `vendor/` file, every even index sits beside the target. A working
|
|
* proximity sort keeps the near half; plain truncation would keep half of each.
|
|
*/
|
|
function interleavedPackage(
|
|
targetPath: string,
|
|
count: number,
|
|
moduleBinding?: (siblingName: string) => ReadonlyMap<string, unknown>,
|
|
): { parsedFiles: ParsedFile[]; facts: Map<string, JvmPackageFact> } {
|
|
const facts = new Map<string, JvmPackageFact>();
|
|
facts.set(targetPath, { status: 'known', packageName: 'com.example' });
|
|
const parsedFiles = [moduleWithClass(targetPath, 0)];
|
|
for (let index = 0; index < count; index++) {
|
|
const isFar = index % 2 === 1;
|
|
const name = isFar ? `FarType${index}` : `NearType${index}`;
|
|
const filePath = isFar ? `vendor/generated/${name}.java` : `src/com/example/near/${name}.java`;
|
|
facts.set(filePath, { status: 'known', packageName: 'com.example' });
|
|
parsedFiles.push(
|
|
moduleWithClass(
|
|
filePath,
|
|
index + 1,
|
|
classDef(`class:${index}`, filePath, `com.example.${name}`),
|
|
moduleBinding?.(name),
|
|
),
|
|
);
|
|
}
|
|
return { parsedFiles, facts };
|
|
}
|
|
|
|
function withMaxInjectedSiblings<T>(value: string | undefined, run: () => T): T {
|
|
const previous = process.env.GITNEXUS_MAX_INJECTED_SIBLINGS;
|
|
if (value === undefined) delete process.env.GITNEXUS_MAX_INJECTED_SIBLINGS;
|
|
else process.env.GITNEXUS_MAX_INJECTED_SIBLINGS = value;
|
|
try {
|
|
return run();
|
|
} finally {
|
|
if (previous === undefined) delete process.env.GITNEXUS_MAX_INJECTED_SIBLINGS;
|
|
else process.env.GITNEXUS_MAX_INJECTED_SIBLINGS = previous;
|
|
}
|
|
}
|
|
|
|
describe('JVM sibling injection cap (#2732)', () => {
|
|
const targetPath = 'src/com/example/Target.java';
|
|
|
|
it('keeps the nearest siblings, drops the rest, and injects exactly the cap', () => {
|
|
const { parsedFiles, facts } = interleavedPackage(targetPath, 10);
|
|
const visibility = jvmVisibility(facts);
|
|
const indexes = emptyIndexes();
|
|
|
|
withMaxInjectedSiblings('4', () =>
|
|
visibility.populateNamespaceSiblings(parsedFiles, indexes, {
|
|
fileContents: new Map(parsedFiles.map((parsed) => [parsed.filePath, 'class Type {}'])),
|
|
}),
|
|
);
|
|
|
|
const injected = indexes.bindingAugmentations.get('module:0');
|
|
expect([...(injected?.keys() ?? [])].sort()).toEqual([
|
|
'NearType0',
|
|
'NearType2',
|
|
'NearType4',
|
|
'NearType6',
|
|
]);
|
|
});
|
|
|
|
it('marks a file whose sibling set was truncated as visibility-incomplete', () => {
|
|
const { parsedFiles, facts } = interleavedPackage(targetPath, 10);
|
|
const visibility = jvmVisibility(facts);
|
|
|
|
withMaxInjectedSiblings('4', () =>
|
|
visibility.populateNamespaceSiblings(parsedFiles, emptyIndexes(), {
|
|
fileContents: new Map(parsedFiles.map((parsed) => [parsed.filePath, 'class Type {}'])),
|
|
}),
|
|
);
|
|
|
|
// Spring bean/DI/conditional attribution keys off this flag; a truncated
|
|
// sibling set must never read as exact package visibility.
|
|
expect(visibility.isVisibilityIncomplete(targetPath)).toBe(true);
|
|
});
|
|
|
|
it('bounds type bindings by the same sibling set it bounded bindings by', () => {
|
|
// Each sibling module scope carries a type binding named after itself, so
|
|
// the merged set names exactly which siblings were treated as visible.
|
|
const { parsedFiles, facts } = interleavedPackage(
|
|
targetPath,
|
|
10,
|
|
(name) => new Map([[`Binding_${name}`, { source: 'import' }]]),
|
|
);
|
|
const visibility = jvmVisibility(facts);
|
|
|
|
withMaxInjectedSiblings('4', () =>
|
|
visibility.populateNamespaceSiblings(parsedFiles, emptyIndexes(), {
|
|
fileContents: new Map(parsedFiles.map((parsed) => [parsed.filePath, 'class Type {}'])),
|
|
}),
|
|
);
|
|
|
|
const targetModule = parsedFiles[0].scopes.find((scope) => scope.kind === 'Module');
|
|
expect([...(targetModule?.typeBindings.keys() ?? [])].sort()).toEqual([
|
|
'Binding_NearType0',
|
|
'Binding_NearType2',
|
|
'Binding_NearType4',
|
|
'Binding_NearType6',
|
|
]);
|
|
});
|
|
|
|
it('injects every sibling and stays complete when the cap is disabled', () => {
|
|
const { parsedFiles, facts } = interleavedPackage(targetPath, 10);
|
|
const visibility = jvmVisibility(facts);
|
|
const indexes = emptyIndexes();
|
|
|
|
withMaxInjectedSiblings('0', () =>
|
|
visibility.populateNamespaceSiblings(parsedFiles, indexes, {
|
|
fileContents: new Map(parsedFiles.map((parsed) => [parsed.filePath, 'class Type {}'])),
|
|
}),
|
|
);
|
|
|
|
expect(indexes.bindingAugmentations.get('module:0')?.size).toBe(10);
|
|
expect(visibility.isVisibilityIncomplete(targetPath)).toBe(false);
|
|
});
|
|
|
|
it('applies the documented default of 200 when the env var is unset', () => {
|
|
const { parsedFiles, facts } = interleavedPackage(targetPath, 402);
|
|
const visibility = jvmVisibility(facts);
|
|
const indexes = emptyIndexes();
|
|
|
|
withMaxInjectedSiblings(undefined, () =>
|
|
visibility.populateNamespaceSiblings(parsedFiles, indexes, {
|
|
fileContents: new Map(parsedFiles.map((parsed) => [parsed.filePath, 'class Type {}'])),
|
|
}),
|
|
);
|
|
|
|
expect(indexes.bindingAugmentations.get('module:0')?.size).toBe(200);
|
|
expect(visibility.isVisibilityIncomplete(targetPath)).toBe(true);
|
|
});
|
|
});
|