gitnexus 1.6.10-rc.98 → 1.6.10-rc.99

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.
@@ -1,6 +1,6 @@
1
1
  // gitnexus/src/core/ingestion/class-extractors/configs/jvm.ts
2
2
  import { SupportedLanguages } from '../../../../_shared/index.js';
3
- import { synthesizeJavaAnonymousClassName } from '../../utils/ast-helpers.js';
3
+ import { synthesizeJavaTypeIdentity } from '../../utils/ast-helpers.js';
4
4
  // ---------------------------------------------------------------------------
5
5
  // Java
6
6
  // ---------------------------------------------------------------------------
@@ -29,10 +29,10 @@ export const javaClassConfig = {
29
29
  'record_declaration',
30
30
  ],
31
31
  extractName(node) {
32
- if (node.type === 'object_creation_expression' || node.type === 'enum_constant') {
33
- return synthesizeJavaAnonymousClassName(node);
34
- }
35
- return undefined;
32
+ return synthesizeJavaTypeIdentity(node)?.name;
33
+ },
34
+ extractType(node) {
35
+ return synthesizeJavaTypeIdentity(node)?.label;
36
36
  },
37
37
  // An anonymous body whose name CANNOT be synthesized must not become a
38
38
  // Class node at all. Without this skip, `extract()`'s
@@ -45,7 +45,7 @@ export const javaClassConfig = {
45
45
  definitionNode !== undefined &&
46
46
  (definitionNode.type === 'object_creation_expression' ||
47
47
  definitionNode.type === 'enum_constant') &&
48
- synthesizeJavaAnonymousClassName(definitionNode) === undefined);
48
+ synthesizeJavaTypeIdentity(definitionNode) === undefined);
49
49
  },
50
50
  };
51
51
  // ---------------------------------------------------------------------------
@@ -1,5 +1,5 @@
1
1
  import { materializeClassAnnotationFacts, recordClassAnnotationCapture, } from '../../frameworks/spring/bean-candidates.js';
2
- import { nodeIfType, nodeToCapture, synthesizeJavaAnonymousClassName, syntheticCapture, } from '../../utils/ast-helpers.js';
2
+ import { javaLocalTypeDeclarationContainer, nodeIfType, nodeToCapture, synthesizeJavaTypeIdentity, syntheticCapture, } from '../../utils/ast-helpers.js';
3
3
  import { splitImportDeclaration } from './import-decomposer.js';
4
4
  import { computeJavaArityMetadata } from './arity-metadata.js';
5
5
  import { synthesizeJavaReceiverBinding } from './receiver-binding.js';
@@ -15,7 +15,11 @@ import { captureJavaSpringDiClassFact } from './spring-di.js';
15
15
  /** Declaration anchors that carry function-like arity metadata. */
16
16
  const FUNCTION_DECL_TAGS = ['@declaration.method', '@declaration.constructor'];
17
17
  /** tree-sitter-java node types that the method extractor accepts. */
18
- const FUNCTION_NODE_TYPES = ['method_declaration', 'constructor_declaration'];
18
+ const FUNCTION_NODE_TYPES = [
19
+ 'method_declaration',
20
+ 'constructor_declaration',
21
+ 'compact_constructor_declaration',
22
+ ];
19
23
  const JAVA_CALLABLE_CAPTURE_OPTIONS = {
20
24
  functionNodeTypes: new Set([...FUNCTION_NODE_TYPES, 'lambda_expression']),
21
25
  callNodeTypes: new Set(['method_invocation']),
@@ -34,6 +38,26 @@ const JAVA_CALLABLE_CAPTURE_OPTIONS = {
34
38
  callableProtocolMethods: new Set(['run', 'apply', 'accept', 'call']),
35
39
  normalizeQualifiedName: (raw) => raw.replaceAll('::', '.'),
36
40
  };
41
+ /** Visibility of a local type begins at its declaration and ends with its
42
+ * immediately enclosing block (JLS 6.3). A Java-only synthetic Block scope
43
+ * models that range without changing shared resolver selection semantics. */
44
+ function javaLocalTypeVisibilityScope(node) {
45
+ const container = javaLocalTypeDeclarationContainer(node);
46
+ if (container === null)
47
+ return undefined;
48
+ return {
49
+ '@scope.block': {
50
+ name: '@scope.block',
51
+ range: {
52
+ startLine: node.startPosition.row + 1,
53
+ startCol: node.startPosition.column,
54
+ endLine: container.endPosition.row + 1,
55
+ endCol: container.endPosition.column,
56
+ },
57
+ text: node.text,
58
+ },
59
+ };
60
+ }
37
61
  /** Suppress read.member emissions when the field_access is already
38
62
  * covered by a method_invocation (object of a call) or an
39
63
  * assignment_expression (write target). */
@@ -96,6 +120,22 @@ export function emitJavaScopeCaptures(sourceText, filePath, cachedTree) {
96
120
  recordClassAnnotationCapture(classAnnotations, filePath, annotatedClass, annotationName.text);
97
121
  continue;
98
122
  }
123
+ const typeDeclaration = [
124
+ nodeMap['@declaration.class'],
125
+ nodeMap['@declaration.enum'],
126
+ nodeMap['@declaration.record'],
127
+ nodeMap['@declaration.interface'],
128
+ ].find((node) => node !== undefined);
129
+ const localTypeIdentity = typeDeclaration === undefined ? undefined : synthesizeJavaTypeIdentity(typeDeclaration);
130
+ if (localTypeIdentity?.bindingName !== undefined &&
131
+ grouped['@declaration.name'] !== undefined &&
132
+ typeDeclaration !== undefined) {
133
+ grouped['@declaration.binding-name'] = grouped['@declaration.name'];
134
+ grouped['@declaration.name'] = syntheticCapture('@declaration.name', typeDeclaration, localTypeIdentity.name);
135
+ const visibilityScope = javaLocalTypeVisibilityScope(typeDeclaration);
136
+ if (visibilityScope !== undefined)
137
+ out.push(visibilityScope);
138
+ }
99
139
  // Decompose each `import_declaration`. `@import.statement` is captured
100
140
  // directly on the `import_declaration` node.
101
141
  if (grouped['@import.statement'] !== undefined) {
@@ -218,8 +258,8 @@ export function emitJavaScopeCaptures(sourceText, filePath, cachedTree) {
218
258
  }
219
259
  /**
220
260
  * Synthesize `@declaration.class` matches for anonymous class bodies
221
- * (`new Runnable() { ... }`), named by the same javac-style authority
222
- * (`synthesizeJavaAnonymousClassName` → `Worker$N`) the structure phase
261
+ * (`new Runnable() { ... }`), named by the same javac-compatible authority
262
+ * (`synthesizeJavaTypeIdentity` → `Worker$N`) the structure phase
223
263
  * uses — the two layers agree by construction (#2550).
224
264
  *
225
265
  * The anchor is the `class_body` node: it shares its range with the
@@ -232,15 +272,15 @@ export function emitJavaScopeCaptures(sourceText, filePath, cachedTree) {
232
272
  function synthesizeJavaAnonymousClassDeclarations(rootNode) {
233
273
  const out = [];
234
274
  for (const oce of rootNode.descendantsOfType('object_creation_expression')) {
235
- const name = synthesizeJavaAnonymousClassName(oce);
236
- if (name === undefined)
275
+ const identity = synthesizeJavaTypeIdentity(oce);
276
+ if (identity === undefined)
237
277
  continue;
238
278
  const body = oce.namedChildren.find((c) => c.type === 'class_body');
239
279
  if (body === undefined)
240
280
  continue;
241
281
  out.push({
242
282
  '@declaration.class': nodeToCapture('@declaration.class', body),
243
- '@declaration.name': syntheticCapture('@declaration.name', body, name),
283
+ '@declaration.name': syntheticCapture('@declaration.name', body, identity.name),
244
284
  });
245
285
  // Inheritance: the anonymous class extends/implements its constructed
246
286
  // type. Anchor the `@reference.inherits` on the `class_body` — its
@@ -278,7 +318,7 @@ function synthesizeJavaAnonymousClassDeclarations(rootNode) {
278
318
  out.push({
279
319
  '@type-binding.annotation': nodeToCapture('@type-binding.annotation', declNode),
280
320
  '@type-binding.name': nodeToCapture('@type-binding.name', varName),
281
- '@type-binding.type': syntheticCapture('@type-binding.type', oce, name),
321
+ '@type-binding.type': syntheticCapture('@type-binding.type', oce, identity.name),
282
322
  });
283
323
  }
284
324
  }
@@ -293,11 +333,11 @@ function synthesizeJavaAnonymousClassDeclarations(rootNode) {
293
333
  const hostEnum = javaEnclosingEnumNameOf(constant);
294
334
  const bodyNode = constant.childForFieldName?.('body');
295
335
  const isBodied = bodyNode !== null && bodyNode !== undefined && bodyNode.type === 'class_body';
296
- const bodiedName = synthesizeJavaAnonymousClassName(constant);
297
- if (bodiedName !== undefined && isBodied) {
336
+ const bodiedIdentity = synthesizeJavaTypeIdentity(constant);
337
+ if (bodiedIdentity !== undefined && isBodied) {
298
338
  out.push({
299
339
  '@declaration.class': nodeToCapture('@declaration.class', bodyNode),
300
- '@declaration.name': syntheticCapture('@declaration.name', bodyNode, bodiedName),
340
+ '@declaration.name': syntheticCapture('@declaration.name', bodyNode, bodiedIdentity.name),
301
341
  });
302
342
  if (hostEnum !== undefined) {
303
343
  out.push({
@@ -324,7 +364,7 @@ function synthesizeJavaAnonymousClassDeclarations(rootNode) {
324
364
  // the `object_creation_expression` branch, which skips on synthesis
325
365
  // failure. `hostEnum` is used only for genuinely body-less constants.
326
366
  const constantNameNode = constant.childForFieldName?.('name');
327
- const constantType = isBodied ? bodiedName : hostEnum;
367
+ const constantType = isBodied ? bodiedIdentity?.name : hostEnum;
328
368
  if (constantNameNode !== null && constantNameNode !== undefined && constantType !== undefined) {
329
369
  out.push({
330
370
  '@type-binding.annotation': nodeToCapture('@type-binding.annotation', constant),
@@ -53,6 +53,7 @@ const JAVA_SCOPE_QUERY = `
53
53
 
54
54
  (method_declaration) @scope.function
55
55
  (constructor_declaration) @scope.function
56
+ (compact_constructor_declaration) @scope.function
56
57
 
57
58
  ;; Declarations — types
58
59
  (class_declaration
@@ -522,7 +522,8 @@ function parseJsonStringArrayCapture(cap) {
522
522
  }
523
523
  }
524
524
  function deriveDeclarationName(match, def) {
525
- const nameCap = match['@declaration.name'] ??
525
+ const nameCap = match['@declaration.binding-name'] ??
526
+ match['@declaration.name'] ??
526
527
  match[Object.keys(match).find((k) => k.startsWith('@declaration.') && k.endsWith('.name')) ?? ''];
527
528
  if (nameCap !== undefined)
528
529
  return nameCap.text;
@@ -1,4 +1,4 @@
1
- import { findChild, synthesizeJavaAnonymousClassName, } from '../utils/ast-helpers.js';
1
+ import { findChild, synthesizeJavaTypeIdentity } from '../utils/ast-helpers.js';
2
2
  import { extractSimpleTypeName, extractVarName, extractGenericTypeArgs, resolveIterableElementType, methodToTypeArgPosition, extractElementTypeFromString, } from './shared.js';
3
3
  // ── Java ──────────────────────────────────────────────────────────────────
4
4
  const JAVA_DECLARATION_NODE_TYPES = new Set([
@@ -13,7 +13,7 @@ const anonymousInitializerTypeName = (declarator) => {
13
13
  const valueNode = declarator.childForFieldName('value');
14
14
  if (!valueNode || valueNode.type !== 'object_creation_expression')
15
15
  return undefined;
16
- return synthesizeJavaAnonymousClassName(valueNode);
16
+ return synthesizeJavaTypeIdentity(valueNode)?.name;
17
17
  };
18
18
  /** Java: Type x = ...; Type x; */
19
19
  const extractJavaDeclaration = (node, env) => {
@@ -102,7 +102,31 @@ export interface EnclosingClassInfo {
102
102
  */
103
103
  qualifiedClassId?: string;
104
104
  }
105
- export declare const synthesizeJavaAnonymousClassName: (node: SyntaxNode) => string | undefined;
105
+ /**
106
+ * GitNexus's source-type-relative Java identity for local and anonymous
107
+ * types. It follows javac's `$N` allocation but intentionally omits the
108
+ * package prefix because graph ids already include the source file path.
109
+ */
110
+ export interface JavaSynthesizedTypeIdentity {
111
+ readonly name: string;
112
+ readonly label: 'Class' | 'Enum' | 'Record' | 'Interface';
113
+ readonly bindingName?: string;
114
+ }
115
+ /** A legal local type declaration is a class, enum, record, or interface
116
+ * directly occupying a block-statement position. Annotation interfaces are
117
+ * deliberately excluded: javac rejects local annotation declarations. */
118
+ export declare const javaLocalTypeDeclarationContainer: (node: SyntaxNode) => SyntaxNode | null;
119
+ /**
120
+ * Authoritative Java local/anonymous type identity.
121
+ *
122
+ * JLS 13.1 defines the shape and immediate-host prefix. OpenJDK javac's
123
+ * Check.localClassName allocates N independently for each
124
+ * (enclosing binary name, local simple name) pair; anonymous types use the
125
+ * empty simple name and therefore have their own sequence. Package names are
126
+ * omitted from this project identity because graph ids already include the
127
+ * file path.
128
+ */
129
+ export declare const synthesizeJavaTypeIdentity: (node: SyntaxNode) => JavaSynthesizedTypeIdentity | undefined;
106
130
  export declare const findEnclosingClassInfo: (node: SyntaxNode, filePath: string, resolveEnclosingOwner?: (node: SyntaxNode) => SyntaxNode | null,
107
131
  /**
108
132
  * Optional (#1978): returns the enclosing type's fully-qualified name
@@ -360,31 +360,29 @@ export function getLabelFromCaptures(captureMap, provider) {
360
360
  * `MAX_ENCLOSING_WALK_ITERATIONS` defense-in-depth guard below prevents
361
361
  * pathological hooks from creating an infinite loop. */
362
362
  const MAX_ENCLOSING_WALK_ITERATIONS = 4096;
363
- /**
364
- * Synthesize a javac-style name for a Java anonymous class body:
365
- * `new Runnable() { ... }` inside top-level class `Worker` becomes
366
- * `Worker$1` (`$N` = 1-based source order of anonymous bodies within the
367
- * top-level class). Returns undefined when the node is not an
368
- * `object_creation_expression` carrying a `class_body` child — which also
369
- * keeps this a no-op for C#, whose `object_creation_expression` uses
370
- * `initializer_expression`, never `class_body` (#2550).
371
- *
372
- * The SAME name must be produced by every layer that keys the anonymous
373
- * class (structure-phase node id, enclosing-owner walk, scope-side
374
- * declaration synthesis, receiver typeBinding) — they agree by all calling
375
- * this one helper.
376
- */
377
- /** Type-declaration node types that can host (and name) a Java anonymous
378
- * class body. Naming follows JLS 13.1: the binary name is the
379
- * IMMEDIATELY enclosing type's binary name + `$N`, so the synthesized
380
- * name is the `$`-joined chain of enclosing host names
381
- * (`EnumWrap$Mode$1`), numbered per immediate host in source order. */
382
- const JAVA_ANON_HOST_TYPES = new Set([
383
- 'class_declaration',
384
- 'enum_declaration',
385
- 'interface_declaration',
386
- 'record_declaration',
363
+ /** Named Java declarations that can host, or themselves be, local types. */
364
+ const JAVA_NAMED_TYPE_NODE_LABELS = new Map([
365
+ ['class_declaration', 'Class'],
366
+ ['enum_declaration', 'Enum'],
367
+ ['interface_declaration', 'Interface'],
368
+ ['record_declaration', 'Record'],
369
+ ]);
370
+ const JAVA_ANON_HOST_TYPES = new Set(JAVA_NAMED_TYPE_NODE_LABELS.keys());
371
+ const JAVA_LOCAL_TYPE_CONTAINERS = new Set([
372
+ 'block',
373
+ 'constructor_body',
374
+ 'switch_block_statement_group',
387
375
  ]);
376
+ /** A legal local type declaration is a class, enum, record, or interface
377
+ * directly occupying a block-statement position. Annotation interfaces are
378
+ * deliberately excluded: javac rejects local annotation declarations. */
379
+ export const javaLocalTypeDeclarationContainer = (node) => {
380
+ if (!JAVA_NAMED_TYPE_NODE_LABELS.has(node.type))
381
+ return null;
382
+ const parent = node.parent;
383
+ return parent !== null && JAVA_LOCAL_TYPE_CONTAINERS.has(parent.type) ? parent : null;
384
+ };
385
+ const isJavaLocalTypeNode = (node) => javaLocalTypeDeclarationContainer(node) !== null;
388
386
  /** The two Java anonymous-class-body shapes (#2550/#2555): an
389
387
  * `object_creation_expression` with a `class_body` child
390
388
  * (`new Runnable() { ... }`), and an `enum_constant` with a `body:`
@@ -392,10 +390,7 @@ const JAVA_ANON_HOST_TYPES = new Set([
392
390
  const isJavaAnonymousBodyNode = (node) => (node.type === 'object_creation_expression' &&
393
391
  node.namedChildren?.some((c) => c.type === 'class_body') === true) ||
394
392
  (node.type === 'enum_constant' && node.childForFieldName?.('body')?.type === 'class_body');
395
- /** Nearest ancestor of `node` that is an enclosing TYPE per JLS 13.1 —
396
- * a named host declaration OR another anonymous body (both shapes).
397
- * Anonymous ancestors chain through: an anon inside an anon is
398
- * `Host$1$1`, and an anon inside an enum constant body is `E$1$1`. */
393
+ /** Nearest ancestor of `node` that is an enclosing type per JLS 13.1. */
399
394
  const nearestJavaEnclosingType = (node) => {
400
395
  let cursor = node.parent;
401
396
  let iterations = 0;
@@ -408,84 +403,134 @@ const nearestJavaEnclosingType = (node) => {
408
403
  }
409
404
  return null;
410
405
  };
411
- /** Per-parse-tree memo of anonymous-body numbering: tree → (startIndex →
412
- * synthesized name). Keyed by the tree OBJECT via WeakMap so entries die
413
- * with the parse; without it every call re-scans the host subtree
414
- * (`descendantsOfType`), and the helper is called from four independent
415
- * layers per anonymous body — quadratic on anon-heavy files (old-style
416
- * listener-per-widget Java). */
417
- const javaAnonNameMemo = new WeakMap();
418
- export const synthesizeJavaAnonymousClassName = (node) => {
419
- if (!isJavaAnonymousBodyNode(node))
420
- return undefined;
406
+ /** Parse-tree-bounded memo. Sequence ordinals are built once per tree, avoiding
407
+ * a host-candidate scan for every extraction/ownership consumer. */
408
+ const javaTypeIdentityMemo = new WeakMap();
409
+ const javaHostKey = (node) => `${node.type}:${node.startIndex}`;
410
+ const javaIdentityCandidatesBelow = (root) => {
411
+ const seen = new Set();
412
+ const candidates = [];
413
+ for (const type of [
414
+ 'object_creation_expression',
415
+ 'enum_constant',
416
+ ...JAVA_NAMED_TYPE_NODE_LABELS.keys(),
417
+ ]) {
418
+ for (const candidate of root.descendantsOfType?.(type) ?? []) {
419
+ if (!isJavaAnonymousBodyNode(candidate) && !isJavaLocalTypeNode(candidate))
420
+ continue;
421
+ const key = javaHostKey(candidate);
422
+ if (seen.has(key))
423
+ continue;
424
+ seen.add(key);
425
+ candidates.push(candidate);
426
+ }
427
+ }
428
+ return candidates.sort((left, right) => left.startIndex - right.startIndex);
429
+ };
430
+ const buildJavaTypeIdentityState = (root) => {
431
+ const ordinalByStart = new Map();
432
+ const sequenceCounts = new Map();
433
+ for (const candidate of javaIdentityCandidatesBelow(root)) {
434
+ const host = nearestJavaEnclosingType(candidate);
435
+ if (host === null)
436
+ continue;
437
+ const isAnonymous = isJavaAnonymousBodyNode(candidate);
438
+ const bindingName = isAnonymous ? '' : candidate.childForFieldName?.('name')?.text;
439
+ // Anonymous types deliberately use the empty sequence key; malformed named
440
+ // declarations must not enter that sequence.
441
+ if (!isAnonymous && !bindingName)
442
+ continue;
443
+ const sequenceKey = `${javaHostKey(host)}:${bindingName}`;
444
+ const ordinal = (sequenceCounts.get(sequenceKey) ?? 0) + 1;
445
+ sequenceCounts.set(sequenceKey, ordinal);
446
+ ordinalByStart.set(candidate.startIndex, ordinal);
447
+ }
448
+ return { byStart: new Map(), ordinalByStart };
449
+ };
450
+ const javaTypeIdentityStateFor = (node) => {
421
451
  const tree = node.tree;
422
- if (tree !== undefined) {
423
- const cached = javaAnonNameMemo.get(tree)?.get(node.startIndex);
424
- if (cached !== undefined)
425
- return cached;
452
+ if (tree === undefined) {
453
+ const host = nearestJavaEnclosingType(node);
454
+ return buildJavaTypeIdentityState(host ?? node);
455
+ }
456
+ let state = javaTypeIdentityMemo.get(tree);
457
+ if (state === undefined) {
458
+ state = buildJavaTypeIdentityState(tree.rootNode ?? node);
459
+ javaTypeIdentityMemo.set(tree, state);
460
+ }
461
+ return state;
462
+ };
463
+ /** Source-type-relative binary name of a Java enclosing type, including
464
+ * synthesized local/anonymous hosts and named member-type chains. */
465
+ const javaBinaryNameOfType = (node) => {
466
+ if (isJavaAnonymousBodyNode(node) || isJavaLocalTypeNode(node)) {
467
+ return synthesizeJavaTypeIdentity(node)?.name;
426
468
  }
427
- // JLS 13.1: the binary name is the IMMEDIATELY ENCLOSING TYPE's binary
428
- // name + `$N`. The enclosing type may itself be anonymous — then its
429
- // own synthesized name is the prefix (recursion, memo-bounded):
430
- // `NestHost$1$1` for an anon inside an anon, `E$1$1` for an anon
431
- // inside an enum constant body. For a named enclosing type the prefix
432
- // is the `$`-joined chain of named hosts (`EnumWrap$Mode`).
469
+ if (!JAVA_ANON_HOST_TYPES.has(node.type))
470
+ return undefined;
471
+ const simpleName = node.childForFieldName?.('name')?.text;
472
+ if (simpleName === undefined || simpleName.length === 0)
473
+ return undefined;
433
474
  const enclosing = nearestJavaEnclosingType(node);
434
475
  if (enclosing === null)
476
+ return simpleName;
477
+ const enclosingName = javaBinaryNameOfType(enclosing);
478
+ return enclosingName === undefined ? undefined : `${enclosingName}$${simpleName}`;
479
+ };
480
+ /**
481
+ * Authoritative Java local/anonymous type identity.
482
+ *
483
+ * JLS 13.1 defines the shape and immediate-host prefix. OpenJDK javac's
484
+ * Check.localClassName allocates N independently for each
485
+ * (enclosing binary name, local simple name) pair; anonymous types use the
486
+ * empty simple name and therefore have their own sequence. Package names are
487
+ * omitted from this project identity because graph ids already include the
488
+ * file path.
489
+ */
490
+ export const synthesizeJavaTypeIdentity = (node) => {
491
+ const localLabel = JAVA_NAMED_TYPE_NODE_LABELS.get(node.type);
492
+ const isLocal = localLabel !== undefined && isJavaLocalTypeNode(node);
493
+ const isAnonymous = isJavaAnonymousBodyNode(node);
494
+ const enclosing = nearestJavaEnclosingType(node);
495
+ const memberSimpleName = !isLocal && !isAnonymous && localLabel !== undefined
496
+ ? node.childForFieldName?.('name')?.text
497
+ : undefined;
498
+ const synthesizedHostIdentity = memberSimpleName !== undefined && enclosing !== null
499
+ ? synthesizeJavaTypeIdentity(enclosing)
500
+ : undefined;
501
+ if (!isLocal && !isAnonymous && synthesizedHostIdentity === undefined)
435
502
  return undefined;
436
- let prefix;
437
- if (isJavaAnonymousBodyNode(enclosing)) {
438
- const enclosingName = synthesizeJavaAnonymousClassName(enclosing);
439
- if (enclosingName === undefined)
440
- return undefined;
441
- prefix = enclosingName;
442
- }
443
- else {
444
- const hostNames = [];
445
- let cursor = enclosing;
446
- let iterations = 0;
447
- while (cursor) {
448
- if (++iterations > MAX_ENCLOSING_WALK_ITERATIONS)
449
- return undefined;
450
- if (JAVA_ANON_HOST_TYPES.has(cursor.type)) {
451
- const hostName = cursor.childForFieldName?.('name')?.text;
452
- if (hostName === undefined || hostName.length === 0)
453
- return undefined;
454
- hostNames.unshift(hostName);
455
- }
456
- cursor = cursor.parent;
457
- }
458
- prefix = hostNames.join('$');
459
- }
460
- // All anonymous bodies (both shapes) whose immediately enclosing TYPE
461
- // is THIS one, in source order. `descendantsOfType` over the subtree
462
- // also finds bodies belonging to nested enclosing types — filter them
463
- // out by re-deriving each candidate's own enclosing type.
464
- const candidates = [
465
- ...(enclosing.descendantsOfType?.('object_creation_expression') ?? []),
466
- ...(enclosing.descendantsOfType?.('enum_constant') ?? []),
467
- ]
468
- .filter(isJavaAnonymousBodyNode)
469
- .filter((c) => {
470
- const host = nearestJavaEnclosingType(c);
471
- return (host !== null && host.startIndex === enclosing.startIndex && host.type === enclosing.type);
472
- })
473
- .sort((a, b) => a.startIndex - b.startIndex);
474
- if (tree !== undefined) {
475
- let byStart = javaAnonNameMemo.get(tree);
476
- if (byStart === undefined) {
477
- byStart = new Map();
478
- javaAnonNameMemo.set(tree, byStart);
479
- }
480
- for (let i = 0; i < candidates.length; i++) {
481
- byStart.set(candidates[i].startIndex, `${prefix}$${i + 1}`);
482
- }
483
- return byStart.get(node.startIndex);
503
+ if (enclosing === null)
504
+ return undefined;
505
+ const state = javaTypeIdentityStateFor(node);
506
+ const cached = state.byStart.get(node.startIndex);
507
+ if (cached !== undefined)
508
+ return cached;
509
+ const prefix = javaBinaryNameOfType(enclosing);
510
+ if (prefix === undefined)
511
+ return undefined;
512
+ if (memberSimpleName !== undefined) {
513
+ const identity = {
514
+ name: `${prefix}$${memberSimpleName}`,
515
+ label: localLabel,
516
+ bindingName: memberSimpleName,
517
+ };
518
+ state.byStart.set(node.startIndex, identity);
519
+ return identity;
484
520
  }
485
- const index = candidates.findIndex((c) => c.startIndex === node.startIndex);
486
- if (index === -1)
521
+ const bindingName = isLocal ? node.childForFieldName?.('name')?.text : undefined;
522
+ if (isLocal && !bindingName)
487
523
  return undefined;
488
- return `${prefix}$${index + 1}`;
524
+ const ordinal = state.ordinalByStart.get(node.startIndex);
525
+ if (ordinal === undefined)
526
+ return undefined;
527
+ const identity = {
528
+ name: `${prefix}$${ordinal}${bindingName ?? ''}`,
529
+ label: isAnonymous ? 'Class' : localLabel,
530
+ ...(bindingName === undefined ? {} : { bindingName }),
531
+ };
532
+ state.byStart.set(node.startIndex, identity);
533
+ return identity;
489
534
  };
490
535
  export const findEnclosingClassInfo = (node, filePath, resolveEnclosingOwner,
491
536
  /**
@@ -553,12 +598,12 @@ getQualifiedOwnerName) => {
553
598
  // enum constant, and every C# `object_creation_expression`), so the
554
599
  // walk continues unchanged for those — including on to
555
600
  // `enum_declaration`, which sits in CLASS_CONTAINER_TYPES below.
556
- if (current.type === 'object_creation_expression' || current.type === 'enum_constant') {
557
- const anonName = synthesizeJavaAnonymousClassName(current);
558
- if (anonName !== undefined) {
601
+ if (isJavaAnonymousBodyNode(current) || JAVA_ANON_HOST_TYPES.has(current.type)) {
602
+ const identity = synthesizeJavaTypeIdentity(current);
603
+ if (identity !== undefined) {
559
604
  return {
560
- classId: generateId('Class', `${filePath}:${anonName}`),
561
- className: anonName,
605
+ classId: generateId(identity.label, `${filePath}:${identity.name}`),
606
+ className: identity.name,
562
607
  };
563
608
  }
564
609
  }
@@ -56,6 +56,9 @@ import { fileURLToPath } from 'url';
56
56
  // method injection sites plus bean-name and @Primary provider metadata.
57
57
  // v20: Java/Kotlin capture side-channels persist package and class-annotation
58
58
  // facts for shared Spring Bean resolution.
59
+ // v21: Java local class/enum/record/interface captures use javac-compatible,
60
+ // source-type-relative JLS 13.1 identities and declaration-to-block scopes
61
+ // (#2562).
59
62
  // v19: Java enum constant bodies emit E$N Class nodes; anonymous naming uses
60
63
  // JLS 13.1 immediate-host chains (#2555).
61
64
  // v18: Worker$N anonymous bodies. v17: callable-value-flow operand identity.
@@ -431,8 +431,14 @@ export interface RepoMeta {
431
431
  * spurious edges and these new resolved edges are cross-file, so a pre-v12
432
432
  * top-up would leave unchanged Rust files stale either way; force a full
433
433
  * re-analyze instead.
434
- */
435
- export declare const INCREMENTAL_SCHEMA_VERSION = 12;
434
+ * v13: Java local classes, enums, records, and interfaces use
435
+ * source-type-relative JLS 13.1 identities (`Outer$1Local`). Number allocation
436
+ * matches javac: one sequence per (enclosing type, local simple name), with a
437
+ * separate sequence for anonymous types. Existing type/member ids, lexical
438
+ * bindings, and ownership edges must not be mixed with newly named unchanged
439
+ * Java files; force a full re-analyze.
440
+ */
441
+ export declare const INCREMENTAL_SCHEMA_VERSION = 13;
436
442
  export interface IndexedRepo {
437
443
  repoPath: string;
438
444
  storagePath: string;
@@ -152,8 +152,14 @@ export const cloneDirBelongsToEntry = (cloneDir, entryPath) => registryPathEqual
152
152
  * spurious edges and these new resolved edges are cross-file, so a pre-v12
153
153
  * top-up would leave unchanged Rust files stale either way; force a full
154
154
  * re-analyze instead.
155
+ * v13: Java local classes, enums, records, and interfaces use
156
+ * source-type-relative JLS 13.1 identities (`Outer$1Local`). Number allocation
157
+ * matches javac: one sequence per (enclosing type, local simple name), with a
158
+ * separate sequence for anonymous types. Existing type/member ids, lexical
159
+ * bindings, and ownership edges must not be mixed with newly named unchanged
160
+ * Java files; force a full re-analyze.
155
161
  */
156
- export const INCREMENTAL_SCHEMA_VERSION = 12;
162
+ export const INCREMENTAL_SCHEMA_VERSION = 13;
157
163
  const GITNEXUS_DIR = '.gitnexus';
158
164
  const GITNEXUS_EXCLUDE_ENTRY = `${GITNEXUS_DIR}/`;
159
165
  export const INDEX_METADATA_FILE = 'gitnexus.json';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gitnexus",
3
- "version": "1.6.10-rc.98",
3
+ "version": "1.6.10-rc.99",
4
4
  "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.",
5
5
  "author": "Abhigyan Patwari",
6
6
  "license": "PolyForm-Noncommercial-1.0.0",