gitnexus 1.6.11-rc.21 → 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.
@@ -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
- if (key === classFile || key.endsWith(`/${classFile}`)) {
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 scoped = node.children.find((c) => c.type === 'scoped_identifier');
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
- const lastDot = text.lastIndexOf('.');
263
- const fqn = text.slice(0, lastDot);
264
- const name = text.slice(lastDot + 1);
265
- if (isStatic) {
266
- // import static a.b.C.F → local F from module a.b.C, original F.
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
- // import a.b.C → module IS the class FQN; originalName is the class
271
- // simple name. resolveJavaImport maps `a.b.C` → `a/b/C.java`.
272
- imports.set(name, { module: text, originalName: name });
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 { literals, exprs, imports: imports };
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 and import bindings
298
- * of one parsed Kotlin file into the {@link KotlinModuleConstants} shape 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 NO_UNFOLDABLE_DECLARATIONS = new Set();
148
- /**
149
- * Kotlin declaration keys known to exist but not fold, or an empty set when
150
- * `mc` came from another language binding.
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 and import bindings
732
- * of one parsed Kotlin file into the {@link KotlinModuleConstants} shape 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 (!isWildcard && identifier) {
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 (segments.length >= 2) {
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 || constants.exprs.size > 0 || constants.imports.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
  }
@@ -6783,6 +6783,17 @@ export class LocalBackend {
6783
6783
  target: params.target,
6784
6784
  direction: params.direction,
6785
6785
  };
6786
+ // Forward the target-selector params like the trace branch above.
6787
+ // @group impact used to drop target_uid/file_path/kind here, so a name
6788
+ // shared by same-named Api/Impl/Controller layers resolved ambiguously
6789
+ // in the member repo and the documented "re-call with target_uid"
6790
+ // disambiguation loop (impact tool schema) never worked in group mode.
6791
+ if (typeof params.target_uid === 'string')
6792
+ impactArgs.target_uid = params.target_uid;
6793
+ if (typeof params.file_path === 'string')
6794
+ impactArgs.file_path = params.file_path;
6795
+ if (typeof params.kind === 'string')
6796
+ impactArgs.kind = params.kind;
6786
6797
  if (params.maxDepth !== undefined)
6787
6798
  impactArgs.maxDepth = params.maxDepth;
6788
6799
  if (params.crossDepth !== undefined)