gitnexus 1.6.6-rc.26 → 1.6.6-rc.28

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.
Files changed (47) hide show
  1. package/README.md +10 -0
  2. package/dist/cli/analyze.d.ts +2 -0
  3. package/dist/cli/analyze.js +75 -2
  4. package/dist/cli/index.js +6 -0
  5. package/dist/cli/wiki.js +19 -0
  6. package/dist/core/ingestion/languages/kotlin/arity-metadata.d.ts +7 -0
  7. package/dist/core/ingestion/languages/kotlin/arity-metadata.js +20 -0
  8. package/dist/core/ingestion/languages/kotlin/arity.d.ts +2 -0
  9. package/dist/core/ingestion/languages/kotlin/arity.js +15 -0
  10. package/dist/core/ingestion/languages/kotlin/cache-stats.d.ts +7 -0
  11. package/dist/core/ingestion/languages/kotlin/cache-stats.js +15 -0
  12. package/dist/core/ingestion/languages/kotlin/captures.d.ts +2 -0
  13. package/dist/core/ingestion/languages/kotlin/captures.js +376 -0
  14. package/dist/core/ingestion/languages/kotlin/import-decomposer.d.ts +3 -0
  15. package/dist/core/ingestion/languages/kotlin/import-decomposer.js +37 -0
  16. package/dist/core/ingestion/languages/kotlin/import-target.d.ts +6 -0
  17. package/dist/core/ingestion/languages/kotlin/import-target.js +60 -0
  18. package/dist/core/ingestion/languages/kotlin/index.d.ts +8 -0
  19. package/dist/core/ingestion/languages/kotlin/index.js +8 -0
  20. package/dist/core/ingestion/languages/kotlin/interpret.d.ts +4 -0
  21. package/dist/core/ingestion/languages/kotlin/interpret.js +70 -0
  22. package/dist/core/ingestion/languages/kotlin/merge-bindings.d.ts +2 -0
  23. package/dist/core/ingestion/languages/kotlin/merge-bindings.js +25 -0
  24. package/dist/core/ingestion/languages/kotlin/owners.d.ts +2 -0
  25. package/dist/core/ingestion/languages/kotlin/owners.js +50 -0
  26. package/dist/core/ingestion/languages/kotlin/query.d.ts +3 -0
  27. package/dist/core/ingestion/languages/kotlin/query.js +112 -0
  28. package/dist/core/ingestion/languages/kotlin/receiver-binding.d.ts +3 -0
  29. package/dist/core/ingestion/languages/kotlin/receiver-binding.js +100 -0
  30. package/dist/core/ingestion/languages/kotlin/scope-resolver.d.ts +17 -0
  31. package/dist/core/ingestion/languages/kotlin/scope-resolver.js +37 -0
  32. package/dist/core/ingestion/languages/kotlin/simple-hooks.d.ts +4 -0
  33. package/dist/core/ingestion/languages/kotlin/simple-hooks.js +19 -0
  34. package/dist/core/ingestion/languages/kotlin.js +10 -0
  35. package/dist/core/ingestion/languages/typescript/captures.js +59 -10
  36. package/dist/core/ingestion/parsing-processor.js +61 -7
  37. package/dist/core/ingestion/pipeline-phases/parse-impl.js +312 -74
  38. package/dist/core/ingestion/pipeline.d.ts +50 -0
  39. package/dist/core/ingestion/scope-resolution/pipeline/registry.js +2 -0
  40. package/dist/core/ingestion/workers/parse-worker.js +54 -11
  41. package/dist/core/ingestion/workers/quarantine.d.ts +45 -0
  42. package/dist/core/ingestion/workers/quarantine.js +38 -0
  43. package/dist/core/ingestion/workers/worker-pool.d.ts +184 -2
  44. package/dist/core/ingestion/workers/worker-pool.js +814 -72
  45. package/dist/core/run-analyze.d.ts +8 -0
  46. package/dist/core/run-analyze.js +1 -1
  47. package/package.json +1 -1
package/README.md CHANGED
@@ -359,6 +359,16 @@ npx gitnexus analyze
359
359
 
360
360
  For repositories with very large source files, `GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES` controls the worker job byte budget. The default is **8388608 bytes (8 MB)**.
361
361
 
362
+ ### Worker pool resilience tuning
363
+
364
+ Three env vars expose the pool's resilience layers (respawn budget, cumulative-timeout cap, circuit breaker). Defaults are tuned for typical repos; bump them when an analyze legitimately needs more retries, or lower them to fail-fast on a known-bad shape.
365
+
366
+ | Variable | Default | Effect |
367
+ | ------------------------------------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
368
+ | `GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT` | `3` | Max replacement spawns per slot before the slot is dropped from the active rotation. |
369
+ | `GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS` | `5 × subBatchTimeoutMs` | Total retry wall-time budget per job before quarantining. Bounds exponentially-growing retry waits. |
370
+ | `GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD` | `max(3, poolSize)` | Per-slot consecutive deaths before the pool's circuit breaker trips. After tripping, dispatches require a fresh pool. |
371
+
362
372
  ## Privacy
363
373
 
364
374
  - All processing happens locally on your machine
@@ -69,6 +69,8 @@ export interface AnalyzeOptions {
69
69
  maxFileSize?: string;
70
70
  /** Override worker sub-batch idle timeout in seconds. */
71
71
  workerTimeout?: string;
72
+ /** Parse worker pool size; 0 disables workers (sequential fallback). */
73
+ workers?: string;
72
74
  embeddingThreads?: string;
73
75
  embeddingBatchSize?: string;
74
76
  embeddingSubBatchSize?: string;
@@ -133,10 +133,46 @@ function ensureHeap() {
133
133
  ` (Windows: set NODE_OPTIONS=--max-old-space-size=24576 && gitnexus analyze [your-args])\n` +
134
134
  ` If this persists, it may be a native crash unrelated to heap size.\n`, { recoveryHint: 'heap-oom-respawn' });
135
135
  }
136
- process.exitCode = e.status ?? 1;
136
+ const status = typeof e === 'object' && e !== null && 'status' in e && typeof e.status === 'number'
137
+ ? e.status
138
+ : 1;
139
+ process.exitCode = status;
137
140
  }
138
141
  return true;
139
142
  }
143
+ /**
144
+ * GITNEXUS_* env vars that `analyzeCommand` writes for backward-compatible
145
+ * downstream consumption. Snapshotted at function entry and restored in the
146
+ * finally block so that programmatic callers (tests, long-running hosts)
147
+ * don't see leaked state across invocations. `GITNEXUS_WORKER_POOL_SIZE` is
148
+ * NOT in this list: that knob is threaded through `runFullAnalysis` options
149
+ * (see `workerPoolSize` plumbing) so the CLI never has to mutate `process.env`
150
+ * for it in the first place.
151
+ */
152
+ const ANALYZE_CLI_ENV_KEYS = [
153
+ 'GITNEXUS_VERBOSE',
154
+ 'GITNEXUS_MAX_FILE_SIZE',
155
+ 'GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS',
156
+ 'GITNEXUS_EMBEDDING_THREADS',
157
+ 'GITNEXUS_EMBEDDING_BATCH_SIZE',
158
+ 'GITNEXUS_EMBEDDING_SUB_BATCH_SIZE',
159
+ 'GITNEXUS_EMBEDDING_DEVICE',
160
+ ];
161
+ const snapshotAnalyzeEnv = () => {
162
+ const snap = {};
163
+ for (const k of ANALYZE_CLI_ENV_KEYS)
164
+ snap[k] = process.env[k];
165
+ return snap;
166
+ };
167
+ const restoreAnalyzeEnv = (snap) => {
168
+ for (const k of ANALYZE_CLI_ENV_KEYS) {
169
+ const v = snap[k];
170
+ if (v === undefined)
171
+ delete process.env[k];
172
+ else
173
+ process.env[k] = v;
174
+ }
175
+ };
140
176
  /**
141
177
  * Whether the post-index skill step should run.
142
178
  *
@@ -159,6 +195,22 @@ export const analyzeCommand = async (inputPath, options) => {
159
195
  // async error that escapes the try/catch below (#1169) surfaces with
160
196
  // a stack trace and a non-zero exit code instead of a silent exit 0.
161
197
  installFatalHandlers();
198
+ // Snapshot the GITNEXUS_* env vars that the impl writes for downstream
199
+ // consumption, so they don't leak across `analyzeCommand` invocations in
200
+ // programmatic callers (tests, long-running hosts). `process.exit(0)` on
201
+ // the success path bypasses `finally` — intentional: when the process is
202
+ // exiting, restoration is moot. For early-return paths (validation
203
+ // errors) and the alreadyUpToDate fast path the finally restores the
204
+ // pre-call values.
205
+ const envSnap = snapshotAnalyzeEnv();
206
+ try {
207
+ await analyzeCommandImpl(inputPath, options);
208
+ }
209
+ finally {
210
+ restoreAnalyzeEnv(envSnap);
211
+ }
212
+ };
213
+ const analyzeCommandImpl = async (inputPath, options) => {
162
214
  if (options?.verbose) {
163
215
  process.env.GITNEXUS_VERBOSE = '1';
164
216
  }
@@ -174,6 +226,23 @@ export const analyzeCommand = async (inputPath, options) => {
174
226
  }
175
227
  process.env.GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS = String(Math.round(workerTimeoutSeconds * 1000));
176
228
  }
229
+ // `--workers` is threaded through `runFullAnalysis` options → PipelineOptions
230
+ // → createWorkerPool, intentionally bypassing the GITNEXUS_WORKER_POOL_SIZE
231
+ // env channel so this CLI surface never mutates `process.env` for pool size.
232
+ // Tests can therefore re-invoke analyzeCommand with different --workers
233
+ // values back-to-back and observe the value they passed, not whatever the
234
+ // previous call leaked.
235
+ let workerPoolSize;
236
+ if (options?.workers !== undefined) {
237
+ const parsedWorkers = Number(options.workers);
238
+ if (!Number.isInteger(parsedWorkers) || parsedWorkers < 0) {
239
+ cliError(' --workers must be a non-negative integer. ' +
240
+ 'Pass 0 to disable the worker pool (sequential fallback).\n');
241
+ process.exitCode = 1;
242
+ return;
243
+ }
244
+ workerPoolSize = parsedWorkers;
245
+ }
177
246
  // Parse `--embeddings [limit]`: `true` → default cap, string → numeric cap
178
247
  // (0 disables the cap entirely). Validated up here so failures match the
179
248
  // sibling-validation pattern (exit before bar.start() — otherwise
@@ -394,6 +463,10 @@ export const analyzeCommand = async (inputPath, options) => {
394
463
  // be able to accept the duplicate name without also paying the
395
464
  // cost of a full pipeline re-index. See #829 review round 2.
396
465
  allowDuplicateName: options?.allowDuplicateName,
466
+ // Worker pool size threaded from --workers, replacing the previous
467
+ // GITNEXUS_WORKER_POOL_SIZE env mutation. `undefined` defers to the
468
+ // env / auto-formula fallback inside the pipeline.
469
+ workerPoolSize,
397
470
  }, {
398
471
  onProgress: (_phase, percent, message) => {
399
472
  updateBar(percent, message);
@@ -515,7 +588,7 @@ export const analyzeCommand = async (inputPath, options) => {
515
588
  // eslint-disable-next-line no-console -- restoring after intentional progress-bar routing
516
589
  console.error = origError;
517
590
  bar.stop();
518
- const msg = err.message || String(err);
591
+ const msg = err instanceof Error ? err.message : String(err);
519
592
  // Registry name-collision from --name (#829) — surface as an
520
593
  // actionable error rather than a generic stack-trace.
521
594
  if (err instanceof RegistryNameCollisionError) {
package/dist/cli/index.js CHANGED
@@ -38,6 +38,7 @@ program
38
38
  .option('-v, --verbose', 'Enable verbose ingestion warnings (default: false)')
39
39
  .option('--max-file-size <kb>', 'Skip files larger than this (KB). Default: 512. Hard cap: 32768 (tree-sitter limit).')
40
40
  .option('--worker-timeout <seconds>', 'Worker sub-batch idle timeout before retry/fallback. Default: 30.')
41
+ .option('--workers <n>', 'Parse worker pool size. Default: cores-1 capped at 16. Pass 0 to disable workers (sequential).')
41
42
  .option('--embedding-threads <n>', 'Limit local ONNX embedding CPU threads')
42
43
  .option('--embedding-batch-size <n>', 'Number of nodes per embedding batch')
43
44
  .option('--embedding-sub-batch-size <n>', 'Number of chunks per embedding model call')
@@ -47,6 +48,11 @@ program
47
48
  ' GITNEXUS_MAX_FILE_SIZE=N Override large-file skip threshold (KB). Default 512, max 32768.\n' +
48
49
  ' GITNEXUS_WORKER_SUB_BATCH_TIMEOUT_MS=N Worker idle timeout in milliseconds. Default 30000.\n' +
49
50
  ' GITNEXUS_WORKER_SUB_BATCH_MAX_BYTES=N Worker job byte budget. Default 8388608.\n' +
51
+ ' GITNEXUS_WORKER_POOL_SIZE=N Parse worker count override. Default cores-1 capped at 16.\n' +
52
+ ' GITNEXUS_PARSE_CHUNK_CONCURRENCY=N Concurrent in-flight parse chunks. Default 2.\n' +
53
+ ' GITNEXUS_WORKER_MAX_RESPAWNS_PER_SLOT=N Max replacement spawns per slot before drop. Default 3.\n' +
54
+ ' GITNEXUS_WORKER_MAX_CUMULATIVE_TIMEOUT_MS=N Total retry wall-time per job. Default 5x sub-batch timeout.\n' +
55
+ ' GITNEXUS_WORKER_CONSECUTIVE_FAILURE_THRESHOLD=N Per-slot deaths to trip circuit breaker. Default max(3, poolSize).\n' +
50
56
  ' GITNEXUS_EMBEDDING_THREADS=N Limit local ONNX CPU threads for --embeddings.\n' +
51
57
  ' GITNEXUS_SEMANTIC_EXACT_SCAN_LIMIT=N Max embedding chunks for exact-scan fallback. Default 10000.\n' +
52
58
  '\nTip: `.gitnexusignore` supports `.gitignore`-style negation. Add e.g.\n' +
package/dist/cli/wiki.js CHANGED
@@ -80,6 +80,25 @@ function prompt(question, hide = false) {
80
80
  });
81
81
  }
82
82
  export const wikiCommand = async (inputPath, options) => {
83
+ // Snapshot GITNEXUS_VERBOSE at entry — wikiCommand mutates it (the impl
84
+ // below) so cursor-client (process.env-driven) sees the right value during
85
+ // this run. Restored in finally so back-to-back wiki calls in long-running
86
+ // hosts don't leak verbose state from one invocation to the next. Pairs
87
+ // with the same snapshot/restore pattern in `analyzeCommand`.
88
+ const originalVerbose = process.env.GITNEXUS_VERBOSE;
89
+ try {
90
+ await wikiCommandImpl(inputPath, options);
91
+ }
92
+ finally {
93
+ if (originalVerbose === undefined) {
94
+ delete process.env.GITNEXUS_VERBOSE;
95
+ }
96
+ else {
97
+ process.env.GITNEXUS_VERBOSE = originalVerbose;
98
+ }
99
+ }
100
+ };
101
+ const wikiCommandImpl = async (inputPath, options) => {
83
102
  // Set verbose mode globally for cursor-client to pick up
84
103
  if (options?.verbose) {
85
104
  process.env.GITNEXUS_VERBOSE = '1';
@@ -0,0 +1,7 @@
1
+ import type { SyntaxNode } from '../../utils/ast-helpers.js';
2
+ export interface KotlinArityMetadata {
3
+ readonly parameterCount: number | undefined;
4
+ readonly requiredParameterCount: number | undefined;
5
+ readonly parameterTypes: readonly string[] | undefined;
6
+ }
7
+ export declare function computeKotlinArityMetadata(fnNode: SyntaxNode): KotlinArityMetadata;
@@ -0,0 +1,20 @@
1
+ import { kotlinMethodConfig } from '../../method-extractors/configs/jvm.js';
2
+ export function computeKotlinArityMetadata(fnNode) {
3
+ const params = kotlinMethodConfig.extractParameters?.(fnNode) ?? [];
4
+ let hasVararg = false;
5
+ const parameterTypes = [];
6
+ for (const param of params) {
7
+ if (param.isVariadic)
8
+ hasVararg = true;
9
+ if (param.type !== null)
10
+ parameterTypes.push(param.type);
11
+ }
12
+ if (hasVararg)
13
+ parameterTypes.push('vararg');
14
+ const required = params.filter((p) => !p.isOptional && !p.isVariadic).length;
15
+ return {
16
+ parameterCount: hasVararg ? undefined : params.length,
17
+ requiredParameterCount: required,
18
+ parameterTypes: parameterTypes.length > 0 ? parameterTypes : undefined,
19
+ };
20
+ }
@@ -0,0 +1,2 @@
1
+ import type { Callsite, SymbolDefinition } from '../../../../_shared/index.js';
2
+ export declare function kotlinArityCompatibility(def: SymbolDefinition, callsite: Callsite): 'compatible' | 'unknown' | 'incompatible';
@@ -0,0 +1,15 @@
1
+ export function kotlinArityCompatibility(def, callsite) {
2
+ const min = def.requiredParameterCount;
3
+ const max = def.parameterCount;
4
+ if (min === undefined && max === undefined)
5
+ return 'unknown';
6
+ const argCount = callsite.arity;
7
+ if (!Number.isFinite(argCount) || argCount < 0)
8
+ return 'unknown';
9
+ const hasVararg = def.parameterTypes?.some((t) => t === 'vararg') ?? false;
10
+ if (min !== undefined && argCount < min)
11
+ return 'incompatible';
12
+ if (max !== undefined && argCount > max && !hasVararg)
13
+ return 'incompatible';
14
+ return 'compatible';
15
+ }
@@ -0,0 +1,7 @@
1
+ export declare function recordKotlinCacheHit(): void;
2
+ export declare function recordKotlinCacheMiss(): void;
3
+ export declare function getKotlinCaptureCacheStats(): {
4
+ readonly hits: number;
5
+ readonly misses: number;
6
+ };
7
+ export declare function resetKotlinCaptureCacheStats(): void;
@@ -0,0 +1,15 @@
1
+ let hits = 0;
2
+ let misses = 0;
3
+ export function recordKotlinCacheHit() {
4
+ hits += 1;
5
+ }
6
+ export function recordKotlinCacheMiss() {
7
+ misses += 1;
8
+ }
9
+ export function getKotlinCaptureCacheStats() {
10
+ return { hits, misses };
11
+ }
12
+ export function resetKotlinCaptureCacheStats() {
13
+ hits = 0;
14
+ misses = 0;
15
+ }
@@ -0,0 +1,2 @@
1
+ import type { CaptureMatch } from '../../../../_shared/index.js';
2
+ export declare function emitKotlinScopeCaptures(sourceText: string, _filePath: string, cachedTree?: unknown): readonly CaptureMatch[];
@@ -0,0 +1,376 @@
1
+ import { findNodeAtRange, nodeToCapture, syntheticCapture, } from '../../utils/ast-helpers.js';
2
+ import { getTreeSitterBufferSize } from '../../constants.js';
3
+ import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
4
+ import { computeKotlinArityMetadata } from './arity-metadata.js';
5
+ import { splitKotlinImportHeader } from './import-decomposer.js';
6
+ import { recordKotlinCacheHit, recordKotlinCacheMiss } from './cache-stats.js';
7
+ import { normalizeKotlinType } from './interpret.js';
8
+ import { synthesizeKotlinReceiverBinding } from './receiver-binding.js';
9
+ import { getKotlinParser, getKotlinScopeQuery } from './query.js';
10
+ const FUNCTION_DECL_TAGS = ['@declaration.function'];
11
+ export function emitKotlinScopeCaptures(sourceText, _filePath, cachedTree) {
12
+ let tree = cachedTree;
13
+ if (tree === undefined) {
14
+ tree = parseSourceSafe(getKotlinParser(), sourceText, undefined, {
15
+ bufferSize: getTreeSitterBufferSize(sourceText),
16
+ });
17
+ recordKotlinCacheMiss();
18
+ }
19
+ else {
20
+ recordKotlinCacheHit();
21
+ }
22
+ const out = [];
23
+ const returnTypes = collectKotlinReturnTypeTexts(tree.rootNode);
24
+ out.push(...synthesizeKotlinLocalAssignmentBindings(tree.rootNode, returnTypes));
25
+ out.push(...synthesizeKotlinLoopBindings(tree.rootNode, returnTypes));
26
+ for (const match of getKotlinScopeQuery().matches(tree.rootNode)) {
27
+ const grouped = {};
28
+ for (const capture of match.captures) {
29
+ const tag = '@' + capture.name;
30
+ grouped[tag] = nodeToCapture(tag, capture.node);
31
+ }
32
+ if (Object.keys(grouped).length === 0)
33
+ continue;
34
+ if (grouped['@import.statement'] !== undefined) {
35
+ const importNode = findNodeAtRange(tree.rootNode, grouped['@import.statement'].range, 'import_header');
36
+ if (importNode !== null) {
37
+ const decomposed = splitKotlinImportHeader(importNode);
38
+ if (decomposed !== null) {
39
+ out.push(decomposed);
40
+ continue;
41
+ }
42
+ }
43
+ }
44
+ if (grouped['@reference.call.free'] !== undefined &&
45
+ grouped['@reference.receiver'] !== undefined) {
46
+ continue;
47
+ }
48
+ if (grouped['@reference.read.member'] !== undefined) {
49
+ const anchor = grouped['@reference.read.member'];
50
+ const navNode = findNodeAtRange(tree.rootNode, anchor.range, 'navigation_expression');
51
+ if (navNode === null || !shouldEmitReadMember(navNode))
52
+ continue;
53
+ }
54
+ if (grouped['@scope.function'] !== undefined) {
55
+ out.push(grouped);
56
+ const fnNode = findNodeAtRange(tree.rootNode, grouped['@scope.function'].range, 'function_declaration');
57
+ if (fnNode !== null) {
58
+ out.push(...synthesizeKotlinReceiverBinding(fnNode));
59
+ }
60
+ continue;
61
+ }
62
+ const declTag = FUNCTION_DECL_TAGS.find((tag) => grouped[tag] !== undefined);
63
+ if (declTag !== undefined) {
64
+ const fnNode = findNodeAtRange(tree.rootNode, grouped[declTag].range, 'function_declaration');
65
+ if (fnNode !== null) {
66
+ const arity = computeKotlinArityMetadata(fnNode);
67
+ if (arity.parameterCount !== undefined) {
68
+ grouped['@declaration.parameter-count'] = syntheticCapture('@declaration.parameter-count', fnNode, String(arity.parameterCount));
69
+ }
70
+ if (arity.requiredParameterCount !== undefined) {
71
+ grouped['@declaration.required-parameter-count'] = syntheticCapture('@declaration.required-parameter-count', fnNode, String(arity.requiredParameterCount));
72
+ }
73
+ if (arity.parameterTypes !== undefined) {
74
+ grouped['@declaration.parameter-types'] = syntheticCapture('@declaration.parameter-types', fnNode, JSON.stringify(arity.parameterTypes));
75
+ }
76
+ }
77
+ }
78
+ const callTag = ['@reference.call.free', '@reference.call.member', '@reference.call.constructor'].find((tag) => grouped[tag] !== undefined);
79
+ if (callTag !== undefined && grouped['@reference.arity'] === undefined) {
80
+ const callNode = findNodeAtRange(tree.rootNode, grouped[callTag].range, 'call_expression');
81
+ if (callNode !== null) {
82
+ const args = callArguments(callNode);
83
+ grouped['@reference.arity'] = syntheticCapture('@reference.arity', callNode, String(args.length));
84
+ grouped['@reference.parameter-types'] = syntheticCapture('@reference.parameter-types', callNode, JSON.stringify(args.map(inferArgType)));
85
+ }
86
+ }
87
+ out.push(grouped);
88
+ const extensionFallback = extensionFreeCallFallback(grouped, tree.rootNode);
89
+ if (extensionFallback !== null)
90
+ out.push(extensionFallback);
91
+ }
92
+ return out;
93
+ }
94
+ function synthesizeKotlinLoopBindings(rootNode, returnTypes) {
95
+ const out = [];
96
+ for (const fnNode of descendantsOfType(rootNode, 'function_declaration')) {
97
+ const localTypes = collectKotlinLocalTypeTexts(fnNode, returnTypes);
98
+ for (const forNode of descendantsOfType(fnNode, 'for_statement')) {
99
+ const variable = forNode.namedChildren.find((child) => child.type === 'variable_declaration');
100
+ const name = variable?.namedChildren.find((child) => child.type === 'simple_identifier');
101
+ if (variable === undefined || name === undefined)
102
+ continue;
103
+ const explicitType = variable.namedChildren.find((child) => isKotlinTypeNode(child));
104
+ const iterable = forNode.namedChildren.find((child) => child.id !== variable.id && child.type !== 'control_structure_body');
105
+ const rawType = explicitType?.text ??
106
+ (iterable === undefined
107
+ ? null
108
+ : inferKotlinIterableElementType(iterable, localTypes, returnTypes));
109
+ if (rawType === null || rawType.trim() === '')
110
+ continue;
111
+ const anchor = forNode.namedChildren.find((child) => child.type === 'control_structure_body') ?? forNode;
112
+ out.push({
113
+ '@type-binding.annotation': nodeToCapture('@type-binding.annotation', anchor),
114
+ '@type-binding.name': syntheticCapture('@type-binding.name', name, name.text),
115
+ '@type-binding.type': syntheticCapture('@type-binding.type', explicitType ?? iterable ?? name, normalizeKotlinType(rawType)),
116
+ });
117
+ }
118
+ }
119
+ return out;
120
+ }
121
+ function synthesizeKotlinLocalAssignmentBindings(rootNode, returnTypes) {
122
+ const out = [];
123
+ for (const fnNode of descendantsOfType(rootNode, 'function_declaration')) {
124
+ const localTypes = new Map();
125
+ for (const prop of descendantsOfType(fnNode, 'property_declaration')) {
126
+ const inferred = inferKotlinPropertyType(prop, localTypes, returnTypes);
127
+ if (inferred === null)
128
+ continue;
129
+ localTypes.set(inferred.name.text, inferred.rawType);
130
+ if (inferred.synthetic) {
131
+ out.push({
132
+ '@type-binding.annotation': nodeToCapture('@type-binding.annotation', prop),
133
+ '@type-binding.name': syntheticCapture('@type-binding.name', inferred.name, inferred.name.text),
134
+ '@type-binding.type': syntheticCapture('@type-binding.type', inferred.source, normalizeKotlinType(inferred.rawType)),
135
+ });
136
+ }
137
+ }
138
+ }
139
+ return out;
140
+ }
141
+ function collectKotlinLocalTypeTexts(fnNode, returnTypes) {
142
+ const out = new Map();
143
+ for (const node of descendants(fnNode)) {
144
+ if (node.type === 'parameter') {
145
+ const name = descendantsOfType(node, 'simple_identifier')[0];
146
+ const type = node.namedChildren.find((child) => isKotlinTypeNode(child));
147
+ if (name !== undefined && type !== undefined)
148
+ out.set(name.text, type.text);
149
+ continue;
150
+ }
151
+ if (node.type === 'property_declaration') {
152
+ const inferred = inferKotlinPropertyType(node, out, returnTypes);
153
+ if (inferred !== null)
154
+ out.set(inferred.name.text, inferred.rawType);
155
+ }
156
+ }
157
+ return out;
158
+ }
159
+ function collectKotlinReturnTypeTexts(rootNode) {
160
+ const out = new Map();
161
+ for (const fnNode of descendantsOfType(rootNode, 'function_declaration')) {
162
+ const name = fnNode.namedChildren.find((child) => child.type === 'simple_identifier');
163
+ const paramsIndex = fnNode.namedChildren.findIndex((child) => child.type === 'function_value_parameters');
164
+ const type = paramsIndex < 0
165
+ ? undefined
166
+ : fnNode.namedChildren.slice(paramsIndex + 1).find((child) => isKotlinTypeNode(child));
167
+ if (name !== undefined && type !== undefined)
168
+ out.set(name.text, type.text);
169
+ }
170
+ return out;
171
+ }
172
+ function inferKotlinPropertyType(prop, localTypes, returnTypes) {
173
+ const variable = prop.namedChildren.find((child) => child.type === 'variable_declaration');
174
+ const name = variable?.namedChildren.find((child) => child.type === 'simple_identifier');
175
+ if (variable === undefined || name === undefined)
176
+ return null;
177
+ const explicitType = variable.namedChildren.find((child) => isKotlinTypeNode(child));
178
+ if (explicitType !== undefined) {
179
+ return { name, rawType: explicitType.text, source: explicitType, synthetic: false };
180
+ }
181
+ const value = prop.namedChildren.find((child) => child.id !== variable.id && child.type !== 'binding_pattern_kind');
182
+ if (value?.type === 'simple_identifier') {
183
+ const rawType = localTypes.get(value.text);
184
+ return rawType === undefined ? null : { name, rawType, source: value, synthetic: true };
185
+ }
186
+ if (value?.type === 'call_expression') {
187
+ const callee = value.namedChildren.find((child) => child.type === 'simple_identifier');
188
+ if (callee === undefined)
189
+ return null;
190
+ const rawType = returnTypes.get(callee.text) ?? (isUppercaseName(callee.text) ? callee.text : null);
191
+ if (rawType === null)
192
+ return null;
193
+ return { name, rawType, source: callee, synthetic: true };
194
+ }
195
+ return null;
196
+ }
197
+ function inferKotlinIterableElementType(iterable, localTypes, returnTypes) {
198
+ if (iterable.type === 'simple_identifier') {
199
+ const raw = localTypes.get(iterable.text);
200
+ return raw === undefined ? null : kotlinContainerElementType(raw, 'values');
201
+ }
202
+ if (iterable.type === 'navigation_expression') {
203
+ const receiver = iterable.namedChildren[0];
204
+ const member = iterable.namedChildren
205
+ .find((child) => child.type === 'navigation_suffix')
206
+ ?.namedChildren.find((child) => child.type === 'simple_identifier')?.text;
207
+ if (receiver?.type !== 'simple_identifier')
208
+ return null;
209
+ const raw = localTypes.get(receiver.text);
210
+ return raw === undefined ? null : kotlinContainerElementType(raw, member ?? 'values');
211
+ }
212
+ if (iterable.type === 'call_expression') {
213
+ const callee = iterable.namedChildren.find((child) => child.type === 'simple_identifier');
214
+ if (callee === undefined)
215
+ return null;
216
+ const raw = returnTypes.get(callee.text);
217
+ return raw === undefined ? null : kotlinContainerElementType(raw, 'values');
218
+ }
219
+ return null;
220
+ }
221
+ function isUppercaseName(text) {
222
+ return /^[A-Z]/.test(text);
223
+ }
224
+ function kotlinContainerElementType(rawType, member) {
225
+ const parsed = parseKotlinGeneric(rawType);
226
+ if (parsed === null)
227
+ return normalizeKotlinType(rawType);
228
+ const base = parsed.base.split('.').pop() ?? parsed.base;
229
+ if (isKotlinMapType(base)) {
230
+ if (member === 'keys')
231
+ return parsed.args[0] ?? null;
232
+ return parsed.args[1] ?? null;
233
+ }
234
+ if (isKotlinIterableType(base))
235
+ return parsed.args[0] ?? null;
236
+ return normalizeKotlinType(rawType);
237
+ }
238
+ function parseKotlinGeneric(text) {
239
+ const trimmed = text.trim().replace(/\?$/, '');
240
+ const open = trimmed.indexOf('<');
241
+ const close = trimmed.lastIndexOf('>');
242
+ if (open < 0 || close < open)
243
+ return null;
244
+ return {
245
+ base: trimmed.slice(0, open).trim(),
246
+ args: splitTopLevelKotlinArgs(trimmed.slice(open + 1, close)),
247
+ };
248
+ }
249
+ function splitTopLevelKotlinArgs(text) {
250
+ const out = [];
251
+ let depth = 0;
252
+ let start = 0;
253
+ for (let i = 0; i < text.length; i++) {
254
+ const ch = text[i];
255
+ if (ch === '<')
256
+ depth++;
257
+ else if (ch === '>')
258
+ depth--;
259
+ else if (ch === ',' && depth === 0) {
260
+ out.push(text.slice(start, i).trim());
261
+ start = i + 1;
262
+ }
263
+ }
264
+ out.push(text.slice(start).trim());
265
+ return out.filter((arg) => arg.length > 0);
266
+ }
267
+ function isKotlinMapType(base) {
268
+ return ['Map', 'MutableMap', 'HashMap', 'LinkedHashMap'].includes(base);
269
+ }
270
+ function isKotlinIterableType(base) {
271
+ return [
272
+ 'List',
273
+ 'MutableList',
274
+ 'ArrayList',
275
+ 'Set',
276
+ 'MutableSet',
277
+ 'Collection',
278
+ 'Iterable',
279
+ 'Sequence',
280
+ 'Array',
281
+ ].includes(base);
282
+ }
283
+ function isKotlinTypeNode(node) {
284
+ return (node.type === 'user_type' || node.type === 'nullable_type' || node.type === 'function_type');
285
+ }
286
+ function descendantsOfType(node, type) {
287
+ return descendants(node).filter((child) => child.type === type);
288
+ }
289
+ function descendants(node) {
290
+ const out = [];
291
+ for (let i = 0; i < node.namedChildCount; i++) {
292
+ const child = node.namedChild(i);
293
+ if (child === null)
294
+ continue;
295
+ out.push(child, ...descendants(child));
296
+ }
297
+ return out;
298
+ }
299
+ function shouldEmitReadMember(navNode) {
300
+ const parent = navNode.parent;
301
+ if (parent === null)
302
+ return true;
303
+ if (parent.type === 'call_expression')
304
+ return false;
305
+ if (parent.type === 'directly_assignable_expression')
306
+ return false;
307
+ return true;
308
+ }
309
+ function callArguments(callNode) {
310
+ const suffix = callNode.namedChildren.find((child) => child.type === 'call_suffix');
311
+ if (suffix === undefined)
312
+ return [];
313
+ const valueArgs = suffix?.namedChildren.find((child) => child.type === 'value_arguments');
314
+ const args = valueArgs?.namedChildren.filter((child) => child.type === 'value_argument') ?? [];
315
+ const trailingLambdas = suffix.namedChildren.filter((child) => child.type === 'annotated_lambda');
316
+ return [...args, ...trailingLambdas];
317
+ }
318
+ function inferArgType(argNode) {
319
+ const value = argNode.namedChild(0) ?? argNode;
320
+ switch (value.type) {
321
+ case 'integer_literal':
322
+ case 'long_literal':
323
+ return 'Int';
324
+ case 'real_literal':
325
+ return 'Double';
326
+ case 'string_literal':
327
+ case 'line_string_literal':
328
+ case 'multi_line_string_literal':
329
+ return 'String';
330
+ case 'character_literal':
331
+ return 'Char';
332
+ case 'boolean_literal':
333
+ return 'Boolean';
334
+ case 'call_expression': {
335
+ const first = value.namedChild(0);
336
+ return first?.type === 'simple_identifier' ? first.text : '';
337
+ }
338
+ default:
339
+ return '';
340
+ }
341
+ }
342
+ function extensionFreeCallFallback(grouped, rootNode) {
343
+ const member = grouped['@reference.call.member'];
344
+ const receiver = grouped['@reference.receiver'];
345
+ const name = grouped['@reference.name'];
346
+ if (member === undefined || receiver === undefined || name === undefined)
347
+ return null;
348
+ const callNode = findNodeAtRange(rootNode, member.range, 'call_expression');
349
+ if (callNode === null)
350
+ return null;
351
+ const receiverNode = findNodeAtRange(rootNode, receiver.range);
352
+ if (receiverNode === null || !isLiteralReceiver(receiverNode))
353
+ return null;
354
+ const out = {
355
+ '@reference.call.free': syntheticCapture('@reference.call.free', callNode, callNode.text),
356
+ '@reference.name': syntheticCapture('@reference.name', callNode, name.text),
357
+ };
358
+ if (grouped['@reference.arity'] !== undefined)
359
+ out['@reference.arity'] = grouped['@reference.arity'];
360
+ if (grouped['@reference.parameter-types'] !== undefined) {
361
+ out['@reference.parameter-types'] = grouped['@reference.parameter-types'];
362
+ }
363
+ return out;
364
+ }
365
+ function isLiteralReceiver(node) {
366
+ return [
367
+ 'integer_literal',
368
+ 'long_literal',
369
+ 'real_literal',
370
+ 'string_literal',
371
+ 'line_string_literal',
372
+ 'multi_line_string_literal',
373
+ 'character_literal',
374
+ 'boolean_literal',
375
+ ].includes(node.type);
376
+ }
@@ -0,0 +1,3 @@
1
+ import type { CaptureMatch } from '../../../../_shared/index.js';
2
+ import { type SyntaxNode } from '../../utils/ast-helpers.js';
3
+ export declare function splitKotlinImportHeader(importNode: SyntaxNode): CaptureMatch | null;