gitnexus 1.6.11-rc.20 → 1.6.11-rc.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/group/extractors/http-patterns/java.js +18 -4
- package/dist/core/group/extractors/http-patterns/kotlin.js +2 -0
- package/dist/core/ingestion/language-provider.d.ts +15 -0
- package/dist/core/ingestion/language-provider.js +22 -0
- package/dist/core/ingestion/languages/java.js +6 -7
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +6 -1
- package/dist/core/ingestion/route-extractors/constant-resolver.d.ts +16 -0
- package/dist/core/ingestion/route-extractors/constant-resolver.js +10 -0
- package/dist/core/ingestion/route-extractors/java-const-resolver.d.ts +27 -1
- package/dist/core/ingestion/route-extractors/java-const-resolver.js +161 -13
- package/dist/core/ingestion/route-extractors/kotlin-const-resolver.d.ts +7 -8
- package/dist/core/ingestion/route-extractors/kotlin-const-resolver.js +209 -23
- package/dist/core/ingestion/workers/parse-worker.js +4 -1
- package/dist/storage/parse-cache.js +8 -1
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@ import Java from 'tree-sitter-java';
|
|
|
2
2
|
import { compilePatterns, runCompiledPatterns, unquoteLiteral, } from '../tree-sitter-scanner.js';
|
|
3
3
|
import { springAnnotationHttpMethods, intersectSpringHttpMethods, isRouteMemberKey, findEnclosingClass, joinPath, } from '../../../ingestion/route-extractors/spring-shared.js';
|
|
4
4
|
import { REST_TEMPLATE_TO_HTTP, WEB_CLIENT_SHORT_TO_HTTP, WEB_CLIENT_LONG_VERB_RE, EXCHANGE_ANNOTATION_TO_HTTP, parseRequestLine, pushPrefix, scanSpringInheritanceProject, OPENFEIGN_FRAMEWORK, HTTP_INTERFACE_FRAMEWORK, FEIGN_CONFIDENCE, REQUEST_LINE_CONFIDENCE, EXCHANGE_CONFIDENCE, } from './spring-consumer-shared.js';
|
|
5
|
-
import { extractJavaModuleConstants, foldJavaOperands, isJavaConstantFile, parseJavaConstOperands, } from '../../../ingestion/route-extractors/java-const-resolver.js';
|
|
5
|
+
import { expandJavaWildcardStaticImports, extractJavaModuleConstants, foldJavaOperands, isJavaConstantFile, parseJavaConstOperands, prepareJavaRouteConstants, } from '../../../ingestion/route-extractors/java-const-resolver.js';
|
|
6
6
|
import { extractStaticPathExpression, inferOkHttpMethod, inferHttpClientMethod, okHttpUrlRootsAtBuilder, httpClientUriRootsAtNewBuilder, httpClientChainHasUriCall, } from './java-static-path.js';
|
|
7
7
|
/**
|
|
8
8
|
* Java HTTP plugin. Handles:
|
|
@@ -789,7 +789,10 @@ export const JAVA_HTTP_PLUGIN = {
|
|
|
789
789
|
if (!tree)
|
|
790
790
|
continue;
|
|
791
791
|
const mc = extractJavaModuleConstants(tree);
|
|
792
|
-
if (mc.literals.size > 0 ||
|
|
792
|
+
if (mc.literals.size > 0 ||
|
|
793
|
+
mc.exprs.size > 0 ||
|
|
794
|
+
mc.imports.size > 0 ||
|
|
795
|
+
(mc.wildcardImports?.length ?? 0) > 0) {
|
|
793
796
|
constants.set(rel, mc);
|
|
794
797
|
}
|
|
795
798
|
}
|
|
@@ -800,7 +803,14 @@ export const JAVA_HTTP_PLUGIN = {
|
|
|
800
803
|
continue;
|
|
801
804
|
}
|
|
802
805
|
}
|
|
803
|
-
|
|
806
|
+
// On-demand static imports (`import static a.b.C.*`) were recorded as
|
|
807
|
+
// pending class FQNs during extraction; materialize their bare-name
|
|
808
|
+
// bindings now that the whole map exists. A wildcard's target is itself
|
|
809
|
+
// a constants file, so it is necessarily a map entry — anything else
|
|
810
|
+
// degrades to the fold's skip floor. In-place: each entry is owned by
|
|
811
|
+
// this map, and every file is expanded exactly once.
|
|
812
|
+
const constantIndex = prepareJavaRouteConstants(constants);
|
|
813
|
+
return { constants, constantIndex };
|
|
804
814
|
},
|
|
805
815
|
scan(tree, repoContext, fileRel) {
|
|
806
816
|
const out = [];
|
|
@@ -832,8 +842,12 @@ export const JAVA_HTTP_PLUGIN = {
|
|
|
832
842
|
return foldConstants;
|
|
833
843
|
try {
|
|
834
844
|
const mc = extractJavaModuleConstants(tree);
|
|
835
|
-
|
|
845
|
+
// A file carrying ONLY wildcard static imports has an empty import
|
|
846
|
+
// table pre-expansion — overlay it too, then materialize the promised
|
|
847
|
+
// bindings against the repo map before it becomes a fold target.
|
|
848
|
+
if (mc.imports.size > 0 || (mc.wildcardImports?.length ?? 0) > 0) {
|
|
836
849
|
const merged = new Map(javaCtx.constants);
|
|
850
|
+
expandJavaWildcardStaticImports(mc, fileRel, merged, javaCtx.constantIndex);
|
|
837
851
|
merged.set(fileRel, mc);
|
|
838
852
|
foldConstants = merged;
|
|
839
853
|
}
|
|
@@ -1260,6 +1260,7 @@ function buildKotlinPlugin(language) {
|
|
|
1260
1260
|
if (mc.literals.size > 0 ||
|
|
1261
1261
|
mc.exprs.size > 0 ||
|
|
1262
1262
|
mc.imports.size > 0 ||
|
|
1263
|
+
(mc.wildcardImports?.length ?? 0) > 0 ||
|
|
1263
1264
|
unfoldableDeclarationsOf(mc).size > 0) {
|
|
1264
1265
|
// POSIX key (see `normalizeRel`); `readFile` above got the raw `rel`.
|
|
1265
1266
|
constants.set(normalizeRel(rel), mc);
|
|
@@ -1303,6 +1304,7 @@ function buildKotlinPlugin(language) {
|
|
|
1303
1304
|
if (mc.literals.size > 0 ||
|
|
1304
1305
|
mc.exprs.size > 0 ||
|
|
1305
1306
|
mc.imports.size > 0 ||
|
|
1307
|
+
(mc.wildcardImports?.length ?? 0) > 0 ||
|
|
1306
1308
|
unfoldableDeclarationsOf(mc).size > 0) {
|
|
1307
1309
|
foldIndex = overlayKotlinConstantIndex(kotlinCtx.index, fileKey, mc);
|
|
1308
1310
|
}
|
|
@@ -372,6 +372,15 @@ interface LanguageProviderConfig {
|
|
|
372
372
|
* {@link extractModuleConstants} accepts.
|
|
373
373
|
*/
|
|
374
374
|
readonly moduleConstantHeuristic?: (content: string) => boolean;
|
|
375
|
+
/**
|
|
376
|
+
* Prepare this language's harvested constants once the complete repo map is
|
|
377
|
+
* available and before route operands are folded. The parse phase passes only
|
|
378
|
+
* entries owned by this provider, so implementations can build one reusable
|
|
379
|
+
* language-specific index and may materialize deferred bindings in place.
|
|
380
|
+
*
|
|
381
|
+
* Default: undefined (the harvested constants are already fold-ready).
|
|
382
|
+
*/
|
|
383
|
+
readonly prepareRouteConstants?: (repo: RepoConstants) => void;
|
|
375
384
|
/**
|
|
376
385
|
* Fold one file's non-literal route-path operand list
|
|
377
386
|
* (`routePathExpr`/`routePathOperands` of an `ExtractedDecoratorRoute`)
|
|
@@ -723,6 +732,12 @@ export interface LanguageProvider extends Omit<LanguageProviderConfig, 'mroStrat
|
|
|
723
732
|
/** Check if a name is a built-in/stdlib function that should be filtered from the call graph. */
|
|
724
733
|
readonly isBuiltInName: (name: string) => boolean;
|
|
725
734
|
}
|
|
735
|
+
/**
|
|
736
|
+
* Run each provider's repo-constant preparation hook once over only the files
|
|
737
|
+
* that provider owns. Values are shared with `repo`, so in-place preparation
|
|
738
|
+
* is visible to the subsequent fold without copying the complete map.
|
|
739
|
+
*/
|
|
740
|
+
export declare function prepareRouteConstantsByProvider(repo: RepoConstants, providerForFile: (filePath: string) => Pick<LanguageProvider, 'prepareRouteConstants'> | null): void;
|
|
726
741
|
/** Define a language provider — required fields must be supplied, optional fields get sensible defaults. */
|
|
727
742
|
export declare function defineLanguage(config: LanguageProviderConfig): LanguageProvider;
|
|
728
743
|
export {};
|
|
@@ -45,6 +45,28 @@ export function shouldHarvestModuleConstants(provider, content) {
|
|
|
45
45
|
return false;
|
|
46
46
|
return !provider.moduleConstantHeuristic || provider.moduleConstantHeuristic(content);
|
|
47
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* Run each provider's repo-constant preparation hook once over only the files
|
|
50
|
+
* that provider owns. Values are shared with `repo`, so in-place preparation
|
|
51
|
+
* is visible to the subsequent fold without copying the complete map.
|
|
52
|
+
*/
|
|
53
|
+
export function prepareRouteConstantsByProvider(repo, providerForFile) {
|
|
54
|
+
const slices = new Map();
|
|
55
|
+
for (const [filePath, constants] of repo) {
|
|
56
|
+
const provider = providerForFile(filePath);
|
|
57
|
+
if (!provider?.prepareRouteConstants)
|
|
58
|
+
continue;
|
|
59
|
+
let slice = slices.get(provider);
|
|
60
|
+
if (!slice) {
|
|
61
|
+
slice = new Map();
|
|
62
|
+
slices.set(provider, slice);
|
|
63
|
+
}
|
|
64
|
+
slice.set(filePath, constants);
|
|
65
|
+
}
|
|
66
|
+
for (const [provider, slice] of slices) {
|
|
67
|
+
provider.prepareRouteConstants?.(slice);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
48
70
|
const DEFAULTS = {
|
|
49
71
|
mroStrategy: 'first-wins',
|
|
50
72
|
};
|
|
@@ -13,7 +13,7 @@ import { defineLanguage } from '../language-provider.js';
|
|
|
13
13
|
import { createLeadingDocDescriptionExtractor } from '../utils/ast-helpers.js';
|
|
14
14
|
import { javaTypeConfig } from '../type-extractors/jvm.js';
|
|
15
15
|
import { extractSpringRoutes, extractSpringTypes } from '../route-extractors/spring.js';
|
|
16
|
-
import { extractJavaModuleConstants, foldJavaOperands, isJavaConstantFile, } from '../route-extractors/java-const-resolver.js';
|
|
16
|
+
import { extractJavaModuleConstants, foldJavaOperands, isJavaConstantFile, prepareJavaRouteConstants, } from '../route-extractors/java-const-resolver.js';
|
|
17
17
|
import { javaExportChecker } from '../export-detection.js';
|
|
18
18
|
import { createImportResolver } from '../import-resolvers/resolver-factory.js';
|
|
19
19
|
import { javaImportConfig } from '../import-resolvers/configs/jvm.js';
|
|
@@ -202,11 +202,10 @@ export const javaProvider = defineLanguage({
|
|
|
202
202
|
// INTERFACES on this side only, which cost the graph its Route nodes while
|
|
203
203
|
// the group still published the contract).
|
|
204
204
|
moduleConstantHeuristic: (content) => isJavaConstantFile(content) ||
|
|
205
|
-
//
|
|
206
|
-
// constant ref
|
|
207
|
-
//
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
/\bimport\s+(?:static\s+)?[\w.]+\s*;/.test(content),
|
|
205
|
+
// Class imports and static (including on-demand) imports can bind a
|
|
206
|
+
// constant ref. Ordinary `import a.b.*;` is not a Java type import and is
|
|
207
|
+
// not expanded by extractJavaModuleConstants, so it must not harvest.
|
|
208
|
+
/\bimport\s+(?:static\s+[\w.]+(?:\.\*)?|[\w.]+)\s*;/.test(content),
|
|
209
|
+
prepareRouteConstants: prepareJavaRouteConstants,
|
|
211
210
|
foldRoutePathOperands: foldJavaOperands,
|
|
212
211
|
});
|
|
@@ -30,7 +30,8 @@ import { SCOPE_RESOLVERS } from '../scope-resolution/pipeline/registry.js';
|
|
|
30
30
|
import { DATA_ROUTE_TABLE_SOURCE } from '../route-extractors/data-route-table.js';
|
|
31
31
|
import { createWorkerPool, workerPoolDisabledByEnv, resolveAutoPoolSize, WorkerPoolInitializationError, WorkerPoolDisabledError, } from '../workers/worker-pool.js';
|
|
32
32
|
import { normalizeExtractedRoutePath } from '../route-extractors/route-path.js';
|
|
33
|
-
import { resolveOperands
|
|
33
|
+
import { resolveOperands } from '../route-extractors/python-const-resolver.js';
|
|
34
|
+
import { prepareRouteConstantsByProvider } from '../language-provider.js';
|
|
34
35
|
import { resolveInheritedSpringRoutes, } from '../route-extractors/spring-shared.js';
|
|
35
36
|
import fs from 'node:fs';
|
|
36
37
|
import { effectiveRamBytes, memoryAutopilotDisabled } from '../utils/effective-ram.js';
|
|
@@ -1022,6 +1023,10 @@ export async function runChunkedParseAndResolve(graph, scannedFiles, allPaths, t
|
|
|
1022
1023
|
for (const { filePath, constants } of allModuleConstants) {
|
|
1023
1024
|
repoConstants.set(filePath, constants);
|
|
1024
1025
|
}
|
|
1026
|
+
// Let each language prepare only its own constants slice before folding.
|
|
1027
|
+
// This is where deferred wildcard bindings can be materialized once per
|
|
1028
|
+
// provider without naming a language in the shared parse phase.
|
|
1029
|
+
prepareRouteConstantsByProvider(repoConstants, getProviderForFile);
|
|
1025
1030
|
const resolvedRoutes = [];
|
|
1026
1031
|
let skipped = 0;
|
|
1027
1032
|
for (const dr of allDecoratorRoutes) {
|
|
@@ -62,7 +62,23 @@ export interface ModuleConstants {
|
|
|
62
62
|
readonly literals: Map<string, string>;
|
|
63
63
|
readonly exprs: Map<string, readonly Operand[]>;
|
|
64
64
|
readonly imports: Map<string, ImportBinding>;
|
|
65
|
+
/**
|
|
66
|
+
* On-demand (wildcard) import specifiers whose bound member names could not
|
|
67
|
+
* be enumerated at extract time — Java `import static a.b.C.*;`, Python
|
|
68
|
+
* `from m import *`. The agnostic fold never reads this (it has no way to
|
|
69
|
+
* enumerate a target module's exports); a language binding materializes the
|
|
70
|
+
* promised bindings from a repo-wide map after extraction — see
|
|
71
|
+
* `expandJavaWildcardStaticImports` in the Java binding — so they resolve
|
|
72
|
+
* through the plain `imports` path with no special cases in the fold.
|
|
73
|
+
*/
|
|
74
|
+
readonly wildcardImports?: readonly string[];
|
|
65
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Declaration keys a language extractor found but could not fold. Java and
|
|
78
|
+
* Kotlin both use this metadata to keep lower-priority imports from replacing
|
|
79
|
+
* a real local declaration; other producers simply return the empty set.
|
|
80
|
+
*/
|
|
81
|
+
export declare function unfoldableDeclarationsOf(mc: ModuleConstants | undefined): ReadonlySet<string>;
|
|
66
82
|
/** Repo-wide map: unique file key (e.g. `app/constants.py`) → that file's
|
|
67
83
|
* {@link ModuleConstants}. */
|
|
68
84
|
export type RepoConstants = ReadonlyMap<string, ModuleConstants>;
|
|
@@ -32,6 +32,16 @@ const MAX_RESOLVE_DEPTH = 8;
|
|
|
32
32
|
* so we floor to `null` (skip) instead (#2393). The depth cap bounds recursion but
|
|
33
33
|
* NOT output size, which grows multiplicatively; this bounds the output. */
|
|
34
34
|
export const MAX_FOLD_LENGTH = 8192;
|
|
35
|
+
const NO_UNFOLDABLE_DECLARATIONS = new Set();
|
|
36
|
+
/**
|
|
37
|
+
* Declaration keys a language extractor found but could not fold. Java and
|
|
38
|
+
* Kotlin both use this metadata to keep lower-priority imports from replacing
|
|
39
|
+
* a real local declaration; other producers simply return the empty set.
|
|
40
|
+
*/
|
|
41
|
+
export function unfoldableDeclarationsOf(mc) {
|
|
42
|
+
const declarations = mc?.unfoldableDeclarations;
|
|
43
|
+
return declarations instanceof Set ? declarations : NO_UNFOLDABLE_DECLARATIONS;
|
|
44
|
+
}
|
|
35
45
|
/**
|
|
36
46
|
* Fold an operand list to its concatenated literal, or `null` if any operand is
|
|
37
47
|
* unresolvable (an unknown name, a non-string term, a cycle, or a depth overrun).
|
|
@@ -42,6 +42,10 @@
|
|
|
42
42
|
import type Parser from 'tree-sitter';
|
|
43
43
|
import { type ImportResolver, type ModuleConstants, type Operand, type RepoConstants } from './constant-resolver.js';
|
|
44
44
|
export type { ImportBinding, ModuleConstants, Operand, RepoConstants, } from './constant-resolver.js';
|
|
45
|
+
export interface JavaModuleConstants extends ModuleConstants {
|
|
46
|
+
/** Declaration keys whose initializer exists but cannot be folded. */
|
|
47
|
+
readonly unfoldableDeclarations: ReadonlySet<string>;
|
|
48
|
+
}
|
|
45
49
|
export declare function isJavaConstantFile(source: string): boolean;
|
|
46
50
|
/**
|
|
47
51
|
* The Java {@link ImportResolver}: map a fully-qualified import specifier to
|
|
@@ -96,7 +100,7 @@ export declare function parseJavaConstOperands(node: Parser.SyntaxNode | null |
|
|
|
96
100
|
* Last-wins in source order; a non-foldable rebind (`X = compute()`) drops X
|
|
97
101
|
* to unresolvable rather than keeping a stale literal.
|
|
98
102
|
*/
|
|
99
|
-
export declare function extractJavaModuleConstants(tree: Parser.Tree):
|
|
103
|
+
export declare function extractJavaModuleConstants(tree: Parser.Tree): JavaModuleConstants;
|
|
100
104
|
/**
|
|
101
105
|
* Resolve a single Java constant referenced in `fileKey` to its literal string
|
|
102
106
|
* value, folding `+` concatenation and following import chains via
|
|
@@ -112,3 +116,25 @@ export declare function resolveJavaConstant(fileKey: string, name: string, repo:
|
|
|
112
116
|
* `fileKey`, or null when any piece is unresolvable (skip floor).
|
|
113
117
|
*/
|
|
114
118
|
export declare function foldJavaOperands(fileKey: string, operands: readonly Operand[], repo: RepoConstants): string | null;
|
|
119
|
+
/**
|
|
120
|
+
* Constant-defining file keys used by Java import resolution.
|
|
121
|
+
*
|
|
122
|
+
* Build once per repo pass. Recomputing this set for every wildcard-importing
|
|
123
|
+
* controller makes expansion quadratic in controller count.
|
|
124
|
+
*/
|
|
125
|
+
export declare function buildJavaConstantKeys(repo: RepoConstants): ReadonlySet<string>;
|
|
126
|
+
export interface JavaConstantIndex {
|
|
127
|
+
readonly keys: ReadonlySet<string>;
|
|
128
|
+
/** Every resolvable path suffix as a dotted module name; null means ambiguous. */
|
|
129
|
+
readonly byModule: ReadonlyMap<string, string | null>;
|
|
130
|
+
/** Direct members by constant-defining file, built once for all importers. */
|
|
131
|
+
readonly membersByFile: ReadonlyMap<string, ReadonlySet<string>>;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Build all Java import suffixes once, turning repeated wildcard target lookup
|
|
135
|
+
* from O(importers × constant files) into O(path segments + importers).
|
|
136
|
+
*/
|
|
137
|
+
export declare function buildJavaConstantIndex(repo: RepoConstants): JavaConstantIndex;
|
|
138
|
+
export declare function expandJavaWildcardStaticImports(mc: ModuleConstants, _fileKey: string, repo: RepoConstants, index?: JavaConstantIndex): ModuleConstants;
|
|
139
|
+
/** Prepare every Java constants entry with one shared suffix index. */
|
|
140
|
+
export declare function prepareJavaRouteConstants(repo: RepoConstants): JavaConstantIndex;
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
* the import is ambiguous and returns null (skip floor), never a wrong path.
|
|
41
41
|
*/
|
|
42
42
|
import { unquoteSpringLiteral } from './spring-shared.js';
|
|
43
|
-
import { MAX_FOLD_LENGTH, } from './constant-resolver.js';
|
|
43
|
+
import { MAX_FOLD_LENGTH, unfoldableDeclarationsOf, } from './constant-resolver.js';
|
|
44
44
|
/**
|
|
45
45
|
* Cheap content gate: can this Java file DEFINE a string constant that a route
|
|
46
46
|
* annotation might reference?
|
|
@@ -121,10 +121,15 @@ export const resolveJavaImport = (_importingFileKey, moduleSpec, repoKeys) => {
|
|
|
121
121
|
// trimming the last segment when the direct hit fails covers both shapes.
|
|
122
122
|
const asPath = moduleSpec.replace(/\./g, '/');
|
|
123
123
|
const classFile = `${asPath}.java`;
|
|
124
|
+
// Compare in POSIX space: on Windows the repo keys can carry backslash
|
|
125
|
+
// separators, which would otherwise never match a '/'-joined class file
|
|
126
|
+
// (observed as 675 calls with zero hits on a backslash-keyed repo).
|
|
127
|
+
const toPosix = (p) => p.replace(/\\/g, '/');
|
|
124
128
|
// Exact package-path suffix match, unique or nothing.
|
|
125
129
|
let hit = null;
|
|
126
130
|
for (const key of repoKeys) {
|
|
127
|
-
|
|
131
|
+
const posixKey = toPosix(key);
|
|
132
|
+
if (posixKey === classFile || posixKey.endsWith(`/${classFile}`)) {
|
|
128
133
|
if (hit !== null)
|
|
129
134
|
return null; // 2+ modules carry this FQN — unresolvable
|
|
130
135
|
hit = key;
|
|
@@ -251,25 +256,48 @@ export function extractJavaModuleConstants(tree) {
|
|
|
251
256
|
const literals = new Map();
|
|
252
257
|
const exprs = new Map();
|
|
253
258
|
const imports = new Map();
|
|
259
|
+
const unfoldableDeclarations = new Set();
|
|
260
|
+
// On-demand static imports (`import static a.b.C.*`) — expanded post-map
|
|
261
|
+
// by expandJavaWildcardStaticImports below.
|
|
262
|
+
const wildcardImports = [];
|
|
254
263
|
// Pass 1: imports (both shapes).
|
|
255
264
|
const walkImports = (node) => {
|
|
256
265
|
if (node.type === 'import_declaration') {
|
|
257
266
|
// import a.b.C; | import static a.b.C; | import static a.b.C.F;
|
|
267
|
+
// import static a.b.C.*; — asterisk is a sibling of scoped_identifier
|
|
268
|
+
// (tree-sitter-java), not the last path segment. Same detection as
|
|
269
|
+
// import-decomposer.ts (`static-wildcard`).
|
|
258
270
|
const isStatic = node.children.some((c) => c.type === 'static' && c.text === 'static');
|
|
259
|
-
const
|
|
271
|
+
const isWildcard = node.children.some((c) => c.type === 'asterisk');
|
|
272
|
+
const scoped = node.children.find((c) => c.type === 'scoped_identifier') ??
|
|
273
|
+
node.children.find((c) => c.type === 'identifier');
|
|
260
274
|
if (scoped) {
|
|
261
275
|
const text = scoped.text;
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
imports.set(name, { module: fqn, originalName: name });
|
|
276
|
+
if (isStatic && isWildcard) {
|
|
277
|
+
// Class FQN only — members are materialized post-map.
|
|
278
|
+
if (text.length > 0 && !wildcardImports.includes(text)) {
|
|
279
|
+
wildcardImports.push(text);
|
|
280
|
+
}
|
|
268
281
|
}
|
|
269
282
|
else {
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
283
|
+
const lastDot = text.lastIndexOf('.');
|
|
284
|
+
const fqn = text.slice(0, lastDot);
|
|
285
|
+
const name = text.slice(lastDot + 1);
|
|
286
|
+
if (isStatic) {
|
|
287
|
+
// Preserve the declaring type in the target lookup. Constants from
|
|
288
|
+
// multiple types share one file-level map, so a bare `F` could
|
|
289
|
+
// otherwise resolve to a sibling type's flattened field.
|
|
290
|
+
const declaringClass = fqn.slice(fqn.lastIndexOf('.') + 1);
|
|
291
|
+
imports.set(name, {
|
|
292
|
+
module: fqn,
|
|
293
|
+
originalName: `${declaringClass}.${name}`,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
// import a.b.C → module IS the class FQN; originalName is the class
|
|
298
|
+
// simple name. resolveJavaImport maps `a.b.C` → `a/b/C.java`.
|
|
299
|
+
imports.set(name, { module: text, originalName: name });
|
|
300
|
+
}
|
|
273
301
|
}
|
|
274
302
|
}
|
|
275
303
|
}
|
|
@@ -328,6 +356,7 @@ export function extractJavaModuleConstants(tree) {
|
|
|
328
356
|
if (operands === null) {
|
|
329
357
|
literals.delete(name);
|
|
330
358
|
exprs.delete(name);
|
|
359
|
+
unfoldableDeclarations.add(name);
|
|
331
360
|
// …and the static IMPORT of the same simple name. A local
|
|
332
361
|
// `static final String` shadows `import static a.b.C.PATH` inside
|
|
333
362
|
// that class (JLS 6.4.1), so the correct answer for a non-foldable
|
|
@@ -348,9 +377,13 @@ export function extractJavaModuleConstants(tree) {
|
|
|
348
377
|
if (qname) {
|
|
349
378
|
literals.delete(qname);
|
|
350
379
|
exprs.delete(qname);
|
|
380
|
+
unfoldableDeclarations.add(qname);
|
|
351
381
|
}
|
|
352
382
|
continue;
|
|
353
383
|
}
|
|
384
|
+
unfoldableDeclarations.delete(name);
|
|
385
|
+
if (qname)
|
|
386
|
+
unfoldableDeclarations.delete(qname);
|
|
354
387
|
const literalValue = operands.length === 1 && operands[0].kind === 'literal'
|
|
355
388
|
? operands[0].value
|
|
356
389
|
: null;
|
|
@@ -419,7 +452,13 @@ export function extractJavaModuleConstants(tree) {
|
|
|
419
452
|
}
|
|
420
453
|
};
|
|
421
454
|
walkTypes(tree.rootNode, false);
|
|
422
|
-
return {
|
|
455
|
+
return {
|
|
456
|
+
literals,
|
|
457
|
+
exprs,
|
|
458
|
+
imports: imports,
|
|
459
|
+
wildcardImports,
|
|
460
|
+
unfoldableDeclarations,
|
|
461
|
+
};
|
|
423
462
|
}
|
|
424
463
|
function newFoldState(repo) {
|
|
425
464
|
const constantKeys = new Set();
|
|
@@ -524,6 +563,8 @@ function computeJavaFold(fileKey, name, state, depth) {
|
|
|
524
563
|
const expr = mc.exprs.get(name);
|
|
525
564
|
if (expr !== undefined)
|
|
526
565
|
return foldOperands(fileKey, expr, state, depth + 1);
|
|
566
|
+
if (unfoldableDeclarationsOf(mc).has(name))
|
|
567
|
+
return null;
|
|
527
568
|
const imp = mc.imports.get(name);
|
|
528
569
|
if (imp !== undefined) {
|
|
529
570
|
const targetFile = resolveJavaImport(fileKey, imp.module, constantKeys);
|
|
@@ -567,3 +608,110 @@ export function foldJavaOperands(fileKey, operands, repo) {
|
|
|
567
608
|
const out = foldOperands(fileKey, operands, newFoldState(repo), 0);
|
|
568
609
|
return out === '' ? null : out;
|
|
569
610
|
}
|
|
611
|
+
/**
|
|
612
|
+
* Constant-defining file keys used by Java import resolution.
|
|
613
|
+
*
|
|
614
|
+
* Build once per repo pass. Recomputing this set for every wildcard-importing
|
|
615
|
+
* controller makes expansion quadratic in controller count.
|
|
616
|
+
*/
|
|
617
|
+
export function buildJavaConstantKeys(repo) {
|
|
618
|
+
const repoKeys = new Set();
|
|
619
|
+
for (const [key, target] of repo) {
|
|
620
|
+
if (target.literals.size > 0 || target.exprs.size > 0)
|
|
621
|
+
repoKeys.add(key);
|
|
622
|
+
}
|
|
623
|
+
return repoKeys;
|
|
624
|
+
}
|
|
625
|
+
/** Direct static members owned by `classSimple`, excluding nested-type members. */
|
|
626
|
+
function directJavaMembers(target, classSimple) {
|
|
627
|
+
const members = new Set();
|
|
628
|
+
const prefix = `${classSimple}.`;
|
|
629
|
+
for (const map of [target.literals, target.exprs]) {
|
|
630
|
+
for (const key of map.keys()) {
|
|
631
|
+
if (!key.startsWith(prefix))
|
|
632
|
+
continue;
|
|
633
|
+
const member = key.slice(prefix.length);
|
|
634
|
+
if (member.length > 0 && !member.includes('.'))
|
|
635
|
+
members.add(member);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
return members;
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Build all Java import suffixes once, turning repeated wildcard target lookup
|
|
642
|
+
* from O(importers × constant files) into O(path segments + importers).
|
|
643
|
+
*/
|
|
644
|
+
export function buildJavaConstantIndex(repo) {
|
|
645
|
+
const keys = buildJavaConstantKeys(repo);
|
|
646
|
+
const byModule = new Map();
|
|
647
|
+
const membersByFile = new Map();
|
|
648
|
+
for (const key of keys) {
|
|
649
|
+
const normalized = key.replace(/\\/g, '/').replace(/^\.\//, '');
|
|
650
|
+
if (!normalized.endsWith('.java'))
|
|
651
|
+
continue;
|
|
652
|
+
const segments = normalized.slice(0, -'.java'.length).split('/');
|
|
653
|
+
const classSimple = segments[segments.length - 1];
|
|
654
|
+
const constants = repo.get(key);
|
|
655
|
+
if (constants)
|
|
656
|
+
membersByFile.set(key, directJavaMembers(constants, classSimple));
|
|
657
|
+
for (let start = 0; start < segments.length; start++) {
|
|
658
|
+
const moduleName = segments.slice(start).join('.');
|
|
659
|
+
const existing = byModule.get(moduleName);
|
|
660
|
+
if (existing === undefined)
|
|
661
|
+
byModule.set(moduleName, key);
|
|
662
|
+
else if (existing !== key)
|
|
663
|
+
byModule.set(moduleName, null);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
return { keys, byModule, membersByFile };
|
|
667
|
+
}
|
|
668
|
+
export function expandJavaWildcardStaticImports(mc, _fileKey, repo, index = buildJavaConstantIndex(repo)) {
|
|
669
|
+
const wildcards = mc.wildcardImports;
|
|
670
|
+
if (!wildcards || wildcards.length === 0)
|
|
671
|
+
return mc;
|
|
672
|
+
// Resolve targets against constant-DEFINING files only. Ingestion's harvest
|
|
673
|
+
// also admits import-only files; measuring uniqueness over every key made
|
|
674
|
+
// a duplicate empty FQN floor ingestion to skip while group still folded
|
|
675
|
+
// (#2980 R4).
|
|
676
|
+
const explicitImports = new Set(mc.imports.keys());
|
|
677
|
+
const pending = new Map();
|
|
678
|
+
for (const fqn of wildcards) {
|
|
679
|
+
const targetKey = index.byModule.get(fqn) ?? null;
|
|
680
|
+
if (targetKey === null)
|
|
681
|
+
continue;
|
|
682
|
+
const classSimple = fqn.slice(fqn.lastIndexOf('.') + 1);
|
|
683
|
+
const members = index.membersByFile.get(targetKey);
|
|
684
|
+
if (!members)
|
|
685
|
+
continue;
|
|
686
|
+
for (const name of members) {
|
|
687
|
+
// Same-file declarations and explicit imports have higher precedence
|
|
688
|
+
// than on-demand imports. An unfoldable declaration must remain a skip,
|
|
689
|
+
// not be resurrected from a wildcard target.
|
|
690
|
+
if (mc.literals.has(name) ||
|
|
691
|
+
mc.exprs.has(name) ||
|
|
692
|
+
unfoldableDeclarationsOf(mc).has(name) ||
|
|
693
|
+
explicitImports.has(name)) {
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
const binding = { module: fqn, originalName: `${classSimple}.${name}` };
|
|
697
|
+
const previous = pending.get(name);
|
|
698
|
+
if (previous === undefined)
|
|
699
|
+
pending.set(name, binding);
|
|
700
|
+
else if (previous !== null && previous.module !== fqn)
|
|
701
|
+
pending.set(name, null);
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
for (const [name, binding] of pending) {
|
|
705
|
+
if (binding !== null)
|
|
706
|
+
mc.imports.set(name, binding);
|
|
707
|
+
}
|
|
708
|
+
return mc;
|
|
709
|
+
}
|
|
710
|
+
/** Prepare every Java constants entry with one shared suffix index. */
|
|
711
|
+
export function prepareJavaRouteConstants(repo) {
|
|
712
|
+
const index = buildJavaConstantIndex(repo);
|
|
713
|
+
for (const [fileKey, mc] of repo) {
|
|
714
|
+
expandJavaWildcardStaticImports(mc, fileKey, repo, index);
|
|
715
|
+
}
|
|
716
|
+
return index;
|
|
717
|
+
}
|
|
@@ -95,6 +95,7 @@
|
|
|
95
95
|
* @PostMapping(ApiPaths.ORDERS) // qualified
|
|
96
96
|
* @PostMapping(com.example.app.api.ApiPaths.ORDERS) // FQN-qualified
|
|
97
97
|
* @PostMapping(ORDERS) // single-name import
|
|
98
|
+
* @PostMapping(ORDERS) after import com.example.api.* // package-star import
|
|
98
99
|
* @PostMapping(ApiPaths.BASE + "/orders") // inline concat
|
|
99
100
|
*
|
|
100
101
|
* Which ANNOTATIONS count as routes is a separate question this module has no
|
|
@@ -136,6 +137,7 @@
|
|
|
136
137
|
import type Parser from 'tree-sitter';
|
|
137
138
|
import { type ModuleConstants, type Operand, type RepoConstants } from './constant-resolver.js';
|
|
138
139
|
export type { ImportBinding, ModuleConstants, Operand, RepoConstants, } from './constant-resolver.js';
|
|
140
|
+
export { unfoldableDeclarationsOf } from './constant-resolver.js';
|
|
139
141
|
/**
|
|
140
142
|
* What {@link extractKotlinModuleConstants} returns: the agnostic
|
|
141
143
|
* {@link ModuleConstants} plus the one piece of per-file metadata JVM import
|
|
@@ -162,12 +164,9 @@ export interface KotlinModuleConstants extends ModuleConstants {
|
|
|
162
164
|
readonly packageName: string;
|
|
163
165
|
/** Declaration keys whose initializer cannot be folded. */
|
|
164
166
|
readonly unfoldableDeclarations: ReadonlySet<string>;
|
|
167
|
+
/** Top-level properties and types that shadow lower-priority star imports. */
|
|
168
|
+
readonly topLevelDeclarations: ReadonlySet<string>;
|
|
165
169
|
}
|
|
166
|
-
/**
|
|
167
|
-
* Kotlin declaration keys known to exist but not fold, or an empty set when
|
|
168
|
-
* `mc` came from another language binding.
|
|
169
|
-
*/
|
|
170
|
-
export declare function unfoldableDeclarationsOf(mc: ModuleConstants | undefined): ReadonlySet<string>;
|
|
171
170
|
/**
|
|
172
171
|
* The name a backtick-quoted Kotlin identifier denotes: `` `api` `` → `api`.
|
|
173
172
|
*
|
|
@@ -294,9 +293,9 @@ export declare function resolveKotlinImportWithIndex(rawModuleSpec: string, inde
|
|
|
294
293
|
*/
|
|
295
294
|
export declare function parseKotlinConstOperands(node: Parser.SyntaxNode | null | undefined, depth?: number): Operand[] | null;
|
|
296
295
|
/**
|
|
297
|
-
* Extract the declared package, file-level string constants
|
|
298
|
-
* of one parsed Kotlin file into the
|
|
299
|
-
* resolver consumes.
|
|
296
|
+
* Extract the declared package, file-level string constants, named imports and
|
|
297
|
+
* package-star import scopes of one parsed Kotlin file into the
|
|
298
|
+
* {@link KotlinModuleConstants} shape the resolver consumes.
|
|
300
299
|
*
|
|
301
300
|
* Constants come from the three carriers Kotlin allows a caller to reach without
|
|
302
301
|
* an instance: file top level, `object` members, and `companion object` members.
|
|
@@ -95,6 +95,7 @@
|
|
|
95
95
|
* @PostMapping(ApiPaths.ORDERS) // qualified
|
|
96
96
|
* @PostMapping(com.example.app.api.ApiPaths.ORDERS) // FQN-qualified
|
|
97
97
|
* @PostMapping(ORDERS) // single-name import
|
|
98
|
+
* @PostMapping(ORDERS) after import com.example.api.* // package-star import
|
|
98
99
|
* @PostMapping(ApiPaths.BASE + "/orders") // inline concat
|
|
99
100
|
*
|
|
100
101
|
* Which ANNOTATIONS count as routes is a separate question this module has no
|
|
@@ -134,7 +135,8 @@
|
|
|
134
135
|
* point at, and no change here is needed.
|
|
135
136
|
*/
|
|
136
137
|
import { unquoteSpringLiteral } from './spring-shared.js';
|
|
137
|
-
import { MAX_FOLD_LENGTH, } from './constant-resolver.js';
|
|
138
|
+
import { MAX_FOLD_LENGTH, unfoldableDeclarationsOf, } from './constant-resolver.js';
|
|
139
|
+
export { unfoldableDeclarationsOf } from './constant-resolver.js';
|
|
138
140
|
/**
|
|
139
141
|
* The declared `package` of the file `mc` describes, or null when the entry did
|
|
140
142
|
* not come from {@link extractKotlinModuleConstants} and therefore cannot be
|
|
@@ -144,14 +146,11 @@ function declaredPackageOf(mc) {
|
|
|
144
146
|
const declared = mc?.packageName;
|
|
145
147
|
return typeof declared === 'string' ? declared : null;
|
|
146
148
|
}
|
|
147
|
-
const
|
|
148
|
-
/**
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
export function unfoldableDeclarationsOf(mc) {
|
|
153
|
-
const declarations = mc?.unfoldableDeclarations;
|
|
154
|
-
return declarations instanceof Set ? declarations : NO_UNFOLDABLE_DECLARATIONS;
|
|
149
|
+
const NO_TOP_LEVEL_DECLARATIONS = new Set();
|
|
150
|
+
/** Kotlin top-level names known to shadow package-star imports. */
|
|
151
|
+
function topLevelDeclarationsOf(mc) {
|
|
152
|
+
const declarations = mc?.topLevelDeclarations;
|
|
153
|
+
return declarations instanceof Set ? declarations : NO_TOP_LEVEL_DECLARATIONS;
|
|
155
154
|
}
|
|
156
155
|
/** Source extensions a Kotlin declaration can live in. */
|
|
157
156
|
const KOTLIN_EXTENSIONS = ['.kt', '.kts'];
|
|
@@ -365,6 +364,13 @@ function declaresTopLevelName(mc, name) {
|
|
|
365
364
|
}
|
|
366
365
|
/** Does this file contribute declarations to Kotlin import ambiguity? */
|
|
367
366
|
function contributesKotlinConstants(mc) {
|
|
367
|
+
return (mc.literals.size > 0 ||
|
|
368
|
+
mc.exprs.size > 0 ||
|
|
369
|
+
unfoldableDeclarationsOf(mc).size > 0 ||
|
|
370
|
+
topLevelDeclarationsOf(mc).size > 0);
|
|
371
|
+
}
|
|
372
|
+
/** Foldable or explicitly unfoldable constants that must live in index projections. */
|
|
373
|
+
function hasIndexedConstants(mc) {
|
|
368
374
|
return mc.literals.size > 0 || mc.exprs.size > 0 || unfoldableDeclarationsOf(mc).size > 0;
|
|
369
375
|
}
|
|
370
376
|
/** Top-level names declared by one file (`Outer.X` contributes `Outer`). */
|
|
@@ -380,6 +386,8 @@ function topLevelDeclarationNames(mc) {
|
|
|
380
386
|
const dot = key.indexOf('.');
|
|
381
387
|
names.add(dot < 0 ? key : key.slice(0, dot));
|
|
382
388
|
}
|
|
389
|
+
for (const name of topLevelDeclarationsOf(mc))
|
|
390
|
+
names.add(name);
|
|
383
391
|
return names;
|
|
384
392
|
}
|
|
385
393
|
/** Every declaration key recorded for a file, foldable or not. */
|
|
@@ -428,17 +436,71 @@ export function buildKotlinConstantIndex(repo) {
|
|
|
428
436
|
}
|
|
429
437
|
return { repo, constantKeys, byPackage: mutablePackages, byFqn };
|
|
430
438
|
}
|
|
439
|
+
/** Read-only one-entry overlay without copying the repo-wide constant map. */
|
|
440
|
+
class KotlinConstantOverlay {
|
|
441
|
+
base;
|
|
442
|
+
overlayKey;
|
|
443
|
+
overlayValue;
|
|
444
|
+
[Symbol.toStringTag] = 'KotlinConstantOverlay';
|
|
445
|
+
constructor(base, overlayKey, overlayValue) {
|
|
446
|
+
this.base = base;
|
|
447
|
+
this.overlayKey = overlayKey;
|
|
448
|
+
this.overlayValue = overlayValue;
|
|
449
|
+
}
|
|
450
|
+
get size() {
|
|
451
|
+
return this.base.size + (this.base.has(this.overlayKey) ? 0 : 1);
|
|
452
|
+
}
|
|
453
|
+
get(key) {
|
|
454
|
+
return key === this.overlayKey ? this.overlayValue : this.base.get(key);
|
|
455
|
+
}
|
|
456
|
+
has(key) {
|
|
457
|
+
return key === this.overlayKey || this.base.has(key);
|
|
458
|
+
}
|
|
459
|
+
*entries() {
|
|
460
|
+
let replaced = false;
|
|
461
|
+
for (const [key, value] of this.base) {
|
|
462
|
+
if (key === this.overlayKey) {
|
|
463
|
+
replaced = true;
|
|
464
|
+
yield [key, this.overlayValue];
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
yield [key, value];
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (!replaced)
|
|
471
|
+
yield [this.overlayKey, this.overlayValue];
|
|
472
|
+
}
|
|
473
|
+
*keys() {
|
|
474
|
+
for (const [key] of this.entries())
|
|
475
|
+
yield key;
|
|
476
|
+
}
|
|
477
|
+
*values() {
|
|
478
|
+
for (const [, value] of this.entries())
|
|
479
|
+
yield value;
|
|
480
|
+
}
|
|
481
|
+
[Symbol.iterator]() {
|
|
482
|
+
return this.entries();
|
|
483
|
+
}
|
|
484
|
+
forEach(callbackfn, thisArg) {
|
|
485
|
+
for (const [key, value] of this.entries())
|
|
486
|
+
callbackfn.call(thisArg, value, key, this);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
431
489
|
/**
|
|
432
490
|
* Add one scan-time file without rebuilding the base index when it only imports
|
|
433
491
|
* constants. A newly discovered declaration is rare and rebuilds once for that
|
|
434
492
|
* file's scan, never once per route.
|
|
435
493
|
*/
|
|
436
494
|
export function overlayKotlinConstantIndex(index, fileKey, mc) {
|
|
495
|
+
// Same-file shadows are read from `mc` itself. Rebuild only when this key's
|
|
496
|
+
// constant projections would change; a controller with a class name but no
|
|
497
|
+
// foldable constants stays on the overlay so scan stays linear.
|
|
498
|
+
const existing = index.repo.get(fileKey);
|
|
499
|
+
if (!hasIndexedConstants(mc) && (!existing || !hasIndexedConstants(existing))) {
|
|
500
|
+
return { ...index, repo: new KotlinConstantOverlay(index.repo, fileKey, mc) };
|
|
501
|
+
}
|
|
437
502
|
const repo = new Map(index.repo);
|
|
438
|
-
const replacing = repo.has(fileKey);
|
|
439
503
|
repo.set(fileKey, mc);
|
|
440
|
-
if (!replacing && !contributesKotlinConstants(mc))
|
|
441
|
-
return { ...index, repo };
|
|
442
504
|
return buildKotlinConstantIndex(repo);
|
|
443
505
|
}
|
|
444
506
|
/**
|
|
@@ -728,9 +790,9 @@ function declaredPackage(root) {
|
|
|
728
790
|
.join('.');
|
|
729
791
|
}
|
|
730
792
|
/**
|
|
731
|
-
* Extract the declared package, file-level string constants
|
|
732
|
-
* of one parsed Kotlin file into the
|
|
733
|
-
* resolver consumes.
|
|
793
|
+
* Extract the declared package, file-level string constants, named imports and
|
|
794
|
+
* package-star import scopes of one parsed Kotlin file into the
|
|
795
|
+
* {@link KotlinModuleConstants} shape the resolver consumes.
|
|
734
796
|
*
|
|
735
797
|
* Constants come from the three carriers Kotlin allows a caller to reach without
|
|
736
798
|
* an instance: file top level, `object` members, and `companion object` members.
|
|
@@ -777,20 +839,26 @@ export function extractKotlinModuleConstants(tree) {
|
|
|
777
839
|
const literals = new Map();
|
|
778
840
|
const exprs = new Map();
|
|
779
841
|
const imports = new Map();
|
|
842
|
+
const wildcardImports = [];
|
|
780
843
|
const unfoldableDeclarations = new Set();
|
|
844
|
+
const topLevelDeclarations = new Set();
|
|
781
845
|
// Pass 1: imports.
|
|
782
846
|
const walkImports = (node) => {
|
|
783
847
|
if (node.type === 'import_header') {
|
|
784
|
-
// `import a.b.*` binds no single name — nothing to key the fold on, and
|
|
785
|
-
// guessing which package member a bare reference came from is exactly the
|
|
786
|
-
// wrong answer. Skipped, so such a reference floors to skip.
|
|
787
848
|
const isWildcard = node.children.some((c) => c.type === 'wildcard_import');
|
|
788
849
|
const identifier = node.children.find((c) => c.type === 'identifier');
|
|
789
|
-
if (
|
|
850
|
+
if (identifier) {
|
|
790
851
|
const segments = identifier.namedChildren
|
|
791
852
|
.filter((c) => c.type === 'simple_identifier')
|
|
792
853
|
.map((c) => unquoteKotlinIdentifier(c.text));
|
|
793
|
-
if (
|
|
854
|
+
if (isWildcard) {
|
|
855
|
+
// Package star (`pkg.*`) or classifier star (`Type.*`). Resolution
|
|
856
|
+
// decides which reading the specifier actually names.
|
|
857
|
+
const scope = segments.join('.');
|
|
858
|
+
if (scope.length > 0 && !wildcardImports.includes(scope))
|
|
859
|
+
wildcardImports.push(scope);
|
|
860
|
+
}
|
|
861
|
+
else if (segments.length >= 2) {
|
|
794
862
|
const spec = segments.join('.');
|
|
795
863
|
const originalName = segments[segments.length - 1];
|
|
796
864
|
const aliasNode = node.children
|
|
@@ -859,6 +927,25 @@ export function extractKotlinModuleConstants(tree) {
|
|
|
859
927
|
};
|
|
860
928
|
/** Prepend a qualified scope unless it is already the innermost scope. */
|
|
861
929
|
const withScope = (scope, scopes) => scope === null || scopes[0] === scope ? scopes : [scope, ...scopes];
|
|
930
|
+
// Package-star imports have lower priority than declarations in this file,
|
|
931
|
+
// including declarations the string-constant extractor intentionally does
|
|
932
|
+
// not harvest (a `var`, plain class, or unfoldable property). Record their
|
|
933
|
+
// names separately so a star import cannot turn one of those shadows into a
|
|
934
|
+
// false route constant.
|
|
935
|
+
for (const child of tree.rootNode.children ?? []) {
|
|
936
|
+
if (child.type === 'property_declaration') {
|
|
937
|
+
const declaration = child.children.find((c) => c.type === 'variable_declaration');
|
|
938
|
+
const name = declaration?.namedChildren.find((c) => c.type === 'simple_identifier');
|
|
939
|
+
if (name)
|
|
940
|
+
topLevelDeclarations.add(unquoteKotlinIdentifier(name.text));
|
|
941
|
+
continue;
|
|
942
|
+
}
|
|
943
|
+
if (child.type === 'object_declaration' || child.type === 'class_declaration') {
|
|
944
|
+
const name = child.children.find((c) => c.type === 'type_identifier');
|
|
945
|
+
if (name)
|
|
946
|
+
topLevelDeclarations.add(unquoteKotlinIdentifier(name.text));
|
|
947
|
+
}
|
|
948
|
+
}
|
|
862
949
|
const walkDeclarations = (node, enclosingType, scopes) => {
|
|
863
950
|
for (const child of node.children ?? []) {
|
|
864
951
|
if (child.type === 'object_declaration') {
|
|
@@ -958,8 +1045,10 @@ export function extractKotlinModuleConstants(tree) {
|
|
|
958
1045
|
literals,
|
|
959
1046
|
exprs,
|
|
960
1047
|
imports,
|
|
1048
|
+
wildcardImports,
|
|
961
1049
|
packageName: declaredPackage(tree.rootNode),
|
|
962
1050
|
unfoldableDeclarations,
|
|
1051
|
+
topLevelDeclarations,
|
|
963
1052
|
};
|
|
964
1053
|
}
|
|
965
1054
|
function newFoldState(repo, index) {
|
|
@@ -1032,8 +1121,81 @@ function resolveImportedName(fileKey, imp, state, depth) {
|
|
|
1032
1121
|
return null;
|
|
1033
1122
|
return resolveWithState(owner.fileKey, `${owner.localName}.${imp.originalName}`, state, depth);
|
|
1034
1123
|
}
|
|
1124
|
+
/**
|
|
1125
|
+
* Resolve one name contributed by Kotlin star imports.
|
|
1126
|
+
*
|
|
1127
|
+
* Star imports have lower priority than local declarations and explicit
|
|
1128
|
+
* imports; callers enforce that ordering before reaching this helper. A name
|
|
1129
|
+
* must identify one declaration across every imported scope. Two stars
|
|
1130
|
+
* exporting the same name, a duplicated FQN, or a package and a classifier
|
|
1131
|
+
* that disagree on the target, floor to null rather than guessing.
|
|
1132
|
+
*
|
|
1133
|
+
* A specifier is tried as a package (`import pkg.*`) and as a classifier
|
|
1134
|
+
* (`import Type.*` — object, class, or companion members). Kotlin allows both.
|
|
1135
|
+
*/
|
|
1136
|
+
function resolveKotlinWildcardImportTarget(mc, name, index) {
|
|
1137
|
+
const scopes = mc.wildcardImports;
|
|
1138
|
+
if (!scopes || scopes.length === 0)
|
|
1139
|
+
return null;
|
|
1140
|
+
let resolved = null;
|
|
1141
|
+
for (const rawScope of scopes) {
|
|
1142
|
+
const scope = unquoteKotlinDottedName(rawScope);
|
|
1143
|
+
const candidates = [];
|
|
1144
|
+
const bucket = index.byPackage.get(scope);
|
|
1145
|
+
if (bucket?.declarers.has(name)) {
|
|
1146
|
+
const fileKey = bucket.declarers.get(name);
|
|
1147
|
+
if (fileKey === null || fileKey === undefined)
|
|
1148
|
+
return null;
|
|
1149
|
+
candidates.push({ fileKey, localName: name });
|
|
1150
|
+
}
|
|
1151
|
+
const owner = index.byFqn.get(scope);
|
|
1152
|
+
if (owner === null)
|
|
1153
|
+
return null;
|
|
1154
|
+
if (owner !== undefined) {
|
|
1155
|
+
const localName = `${owner.localName}.${name}`;
|
|
1156
|
+
const target = index.repo.get(owner.fileKey);
|
|
1157
|
+
if (target &&
|
|
1158
|
+
(target.literals.has(localName) ||
|
|
1159
|
+
target.exprs.has(localName) ||
|
|
1160
|
+
unfoldableDeclarationsOf(target).has(localName))) {
|
|
1161
|
+
candidates.push({ fileKey: owner.fileKey, localName });
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
for (const candidate of candidates) {
|
|
1165
|
+
if (resolved !== null &&
|
|
1166
|
+
(resolved.fileKey !== candidate.fileKey || resolved.localName !== candidate.localName)) {
|
|
1167
|
+
return null;
|
|
1168
|
+
}
|
|
1169
|
+
resolved = candidate;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
return resolved;
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Resolve a top-level name visible from the importing file's own package.
|
|
1176
|
+
* `undefined` means the package does not declare the name; `null` means it is
|
|
1177
|
+
* ambiguous and must floor rather than fall through to a star import.
|
|
1178
|
+
*/
|
|
1179
|
+
function resolveKotlinSamePackageTarget(fileKey, name, index) {
|
|
1180
|
+
const packageName = declaredPackageOf(index.repo.get(fileKey));
|
|
1181
|
+
if (packageName === null)
|
|
1182
|
+
return undefined;
|
|
1183
|
+
const bucket = index.byPackage.get(packageName);
|
|
1184
|
+
if (!bucket?.declarers.has(name))
|
|
1185
|
+
return undefined;
|
|
1186
|
+
const declaringFile = bucket.declarers.get(name);
|
|
1187
|
+
if (declaringFile === null || declaringFile === undefined)
|
|
1188
|
+
return null;
|
|
1189
|
+
// Same-file literals, expressions, and shadows are handled before this step.
|
|
1190
|
+
if (declaringFile === fileKey)
|
|
1191
|
+
return undefined;
|
|
1192
|
+
return { fileKey: declaringFile, localName: name };
|
|
1193
|
+
}
|
|
1035
1194
|
function computeKotlinFold(fileKey, name, state, depth) {
|
|
1036
1195
|
const { repo } = state.index;
|
|
1196
|
+
const mc = repo.get(fileKey);
|
|
1197
|
+
if (!mc)
|
|
1198
|
+
return null;
|
|
1037
1199
|
// Qualified reference (`ApiPaths.ORDERS`): constants and imports are keyed by
|
|
1038
1200
|
// their IN-FILE name, so a dotted name never hits directly. Split head.tail,
|
|
1039
1201
|
// resolve the head through the importing file's type import, then look the
|
|
@@ -1056,6 +1218,19 @@ function computeKotlinFold(fileKey, name, state, depth) {
|
|
|
1056
1218
|
// uses the declaring type's real name.
|
|
1057
1219
|
return resolveWithState(target.fileKey, `${target.localName}.${tail}`, state, depth + 1);
|
|
1058
1220
|
}
|
|
1221
|
+
// A same-file top-level declaration outranks every star import.
|
|
1222
|
+
if (!topLevelDeclarationsOf(mc).has(head)) {
|
|
1223
|
+
const samePackageTarget = resolveKotlinSamePackageTarget(fileKey, head, state.index);
|
|
1224
|
+
if (samePackageTarget === null)
|
|
1225
|
+
return null;
|
|
1226
|
+
if (samePackageTarget !== undefined) {
|
|
1227
|
+
return resolveWithState(samePackageTarget.fileKey, `${samePackageTarget.localName}.${tail}`, state, depth + 1);
|
|
1228
|
+
}
|
|
1229
|
+
const wildcardTarget = resolveKotlinWildcardImportTarget(mc, head, state.index);
|
|
1230
|
+
if (wildcardTarget !== null) {
|
|
1231
|
+
return resolveWithState(wildcardTarget.fileKey, `${wildcardTarget.localName}.${tail}`, state, depth + 1);
|
|
1232
|
+
}
|
|
1233
|
+
}
|
|
1059
1234
|
// Un-imported qualified name (FQN form `com.example.app.api.ApiPaths.ORDERS`):
|
|
1060
1235
|
// try the longest dotted prefix that resolves to a file.
|
|
1061
1236
|
const parts = name.split('.');
|
|
@@ -1077,9 +1252,6 @@ function computeKotlinFold(fileKey, name, state, depth) {
|
|
|
1077
1252
|
// an operand may itself be a QUALIFIED reference (`X = ApiPaths.Y + "/tail"`)
|
|
1078
1253
|
// and the core only knows bare names: it would look `ApiPaths.Y` up in maps
|
|
1079
1254
|
// keyed by simple name, miss, and floor the whole chain to null.
|
|
1080
|
-
const mc = repo.get(fileKey);
|
|
1081
|
-
if (!mc)
|
|
1082
|
-
return null;
|
|
1083
1255
|
const literal = mc.literals.get(name);
|
|
1084
1256
|
if (literal !== undefined)
|
|
1085
1257
|
return literal;
|
|
@@ -1089,6 +1261,20 @@ function computeKotlinFold(fileKey, name, state, depth) {
|
|
|
1089
1261
|
const imp = mc.imports.get(name);
|
|
1090
1262
|
if (imp !== undefined)
|
|
1091
1263
|
return resolveImportedName(fileKey, imp, state, depth + 1);
|
|
1264
|
+
// Any local declaration still shadows a lower-priority package-star import,
|
|
1265
|
+
// including a var/plain type that is absent from the constant maps.
|
|
1266
|
+
if (topLevelDeclarationsOf(mc).has(name) || unfoldableDeclarationsOf(mc).has(name))
|
|
1267
|
+
return null;
|
|
1268
|
+
const samePackageTarget = resolveKotlinSamePackageTarget(fileKey, name, state.index);
|
|
1269
|
+
if (samePackageTarget === null)
|
|
1270
|
+
return null;
|
|
1271
|
+
if (samePackageTarget !== undefined) {
|
|
1272
|
+
return resolveWithState(samePackageTarget.fileKey, samePackageTarget.localName, state, depth + 1);
|
|
1273
|
+
}
|
|
1274
|
+
const wildcardTarget = resolveKotlinWildcardImportTarget(mc, name, state.index);
|
|
1275
|
+
if (wildcardTarget !== null) {
|
|
1276
|
+
return resolveWithState(wildcardTarget.fileKey, wildcardTarget.localName, state, depth + 1);
|
|
1277
|
+
}
|
|
1092
1278
|
return null;
|
|
1093
1279
|
}
|
|
1094
1280
|
/**
|
|
@@ -2328,7 +2328,10 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
2328
2328
|
// without booting a worker.
|
|
2329
2329
|
if (provider.extractModuleConstants && shouldHarvestModuleConstants(provider, parseContent)) {
|
|
2330
2330
|
const constants = provider.extractModuleConstants(tree);
|
|
2331
|
-
if (constants.literals.size > 0 ||
|
|
2331
|
+
if (constants.literals.size > 0 ||
|
|
2332
|
+
constants.exprs.size > 0 ||
|
|
2333
|
+
constants.imports.size > 0 ||
|
|
2334
|
+
(constants.wildcardImports?.length ?? 0) > 0) {
|
|
2332
2335
|
(result.moduleConstants ??= []).push({ filePath: file.path, constants });
|
|
2333
2336
|
}
|
|
2334
2337
|
}
|
|
@@ -680,7 +680,14 @@ import { copyV8CacheIfPresent, tryLoadV8Cache, writeV8CacheFile } from './v8-sid
|
|
|
680
680
|
// cache can replay stale names and declaration metadata.
|
|
681
681
|
// 85 -> 86: Kotlin interface property accessors now record isAbstract on the
|
|
682
682
|
// synthetic Method. A warm v85 cache replays them as concrete.
|
|
683
|
-
|
|
683
|
+
// 86 -> 87: ModuleConstants gained wildcardImports (static-import-asterisk
|
|
684
|
+
// materialization) — a warm v86 cache has no wildcard bindings, so folding
|
|
685
|
+
// would skip them and drop wildcard-imported route constants. origin/main at
|
|
686
|
+
// allocation is 86 (#2885).
|
|
687
|
+
// 87 -> 88: Java ModuleConstants now preserves unfoldable declaration names
|
|
688
|
+
// across worker/cache replay so wildcard expansion cannot resurrect an imported
|
|
689
|
+
// member hidden by a local field. A warm v87 cache lacks that shadow metadata.
|
|
690
|
+
const SCHEMA_BUMP = 88;
|
|
684
691
|
const GITNEXUS_PKG_VERSION = (() => {
|
|
685
692
|
try {
|
|
686
693
|
// package.json sits at gitnexus/package.json — two levels up from
|
package/package.json
CHANGED