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.
- package/README.md +10 -0
- package/dist/cli/analyze.d.ts +2 -0
- package/dist/cli/analyze.js +75 -2
- package/dist/cli/index.js +6 -0
- package/dist/cli/wiki.js +19 -0
- package/dist/core/ingestion/languages/kotlin/arity-metadata.d.ts +7 -0
- package/dist/core/ingestion/languages/kotlin/arity-metadata.js +20 -0
- package/dist/core/ingestion/languages/kotlin/arity.d.ts +2 -0
- package/dist/core/ingestion/languages/kotlin/arity.js +15 -0
- package/dist/core/ingestion/languages/kotlin/cache-stats.d.ts +7 -0
- package/dist/core/ingestion/languages/kotlin/cache-stats.js +15 -0
- package/dist/core/ingestion/languages/kotlin/captures.d.ts +2 -0
- package/dist/core/ingestion/languages/kotlin/captures.js +376 -0
- package/dist/core/ingestion/languages/kotlin/import-decomposer.d.ts +3 -0
- package/dist/core/ingestion/languages/kotlin/import-decomposer.js +37 -0
- package/dist/core/ingestion/languages/kotlin/import-target.d.ts +6 -0
- package/dist/core/ingestion/languages/kotlin/import-target.js +60 -0
- package/dist/core/ingestion/languages/kotlin/index.d.ts +8 -0
- package/dist/core/ingestion/languages/kotlin/index.js +8 -0
- package/dist/core/ingestion/languages/kotlin/interpret.d.ts +4 -0
- package/dist/core/ingestion/languages/kotlin/interpret.js +70 -0
- package/dist/core/ingestion/languages/kotlin/merge-bindings.d.ts +2 -0
- package/dist/core/ingestion/languages/kotlin/merge-bindings.js +25 -0
- package/dist/core/ingestion/languages/kotlin/owners.d.ts +2 -0
- package/dist/core/ingestion/languages/kotlin/owners.js +50 -0
- package/dist/core/ingestion/languages/kotlin/query.d.ts +3 -0
- package/dist/core/ingestion/languages/kotlin/query.js +112 -0
- package/dist/core/ingestion/languages/kotlin/receiver-binding.d.ts +3 -0
- package/dist/core/ingestion/languages/kotlin/receiver-binding.js +100 -0
- package/dist/core/ingestion/languages/kotlin/scope-resolver.d.ts +17 -0
- package/dist/core/ingestion/languages/kotlin/scope-resolver.js +37 -0
- package/dist/core/ingestion/languages/kotlin/simple-hooks.d.ts +4 -0
- package/dist/core/ingestion/languages/kotlin/simple-hooks.js +19 -0
- package/dist/core/ingestion/languages/kotlin.js +10 -0
- package/dist/core/ingestion/languages/typescript/captures.js +59 -10
- package/dist/core/ingestion/parsing-processor.js +61 -7
- package/dist/core/ingestion/pipeline-phases/parse-impl.js +312 -74
- package/dist/core/ingestion/pipeline.d.ts +50 -0
- package/dist/core/ingestion/scope-resolution/pipeline/registry.js +2 -0
- package/dist/core/ingestion/workers/parse-worker.js +54 -11
- package/dist/core/ingestion/workers/quarantine.d.ts +45 -0
- package/dist/core/ingestion/workers/quarantine.js +38 -0
- package/dist/core/ingestion/workers/worker-pool.d.ts +184 -2
- package/dist/core/ingestion/workers/worker-pool.js +814 -72
- package/dist/core/run-analyze.d.ts +8 -0
- package/dist/core/run-analyze.js +1 -1
- 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
|
package/dist/cli/analyze.d.ts
CHANGED
|
@@ -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;
|
package/dist/cli/analyze.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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,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,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
|
+
}
|