gitnexus 1.6.6-rc.59 → 1.6.6-rc.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/i18n/en.d.ts +1 -1
- package/dist/cli/i18n/en.js +1 -1
- package/dist/cli/i18n/resources.d.ts +1 -1
- package/dist/cli/i18n/zh-CN.js +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/cli/wiki.js +57 -19
- package/dist/core/ingestion/languages/rust/arity.d.ts +2 -0
- package/dist/core/ingestion/languages/rust/arity.js +13 -0
- package/dist/core/ingestion/languages/rust/cache-stats.d.ts +7 -0
- package/dist/core/ingestion/languages/rust/cache-stats.js +15 -0
- package/dist/core/ingestion/languages/rust/captures.d.ts +2 -0
- package/dist/core/ingestion/languages/rust/captures.js +179 -0
- package/dist/core/ingestion/languages/rust/import-decomposer.d.ts +8 -0
- package/dist/core/ingestion/languages/rust/import-decomposer.js +164 -0
- package/dist/core/ingestion/languages/rust/import-target.d.ts +14 -0
- package/dist/core/ingestion/languages/rust/import-target.js +105 -0
- package/dist/core/ingestion/languages/rust/index.d.ts +12 -0
- package/dist/core/ingestion/languages/rust/index.js +12 -0
- package/dist/core/ingestion/languages/rust/interpret.d.ts +4 -0
- package/dist/core/ingestion/languages/rust/interpret.js +178 -0
- package/dist/core/ingestion/languages/rust/merge-bindings.d.ts +2 -0
- package/dist/core/ingestion/languages/rust/merge-bindings.js +18 -0
- package/dist/core/ingestion/languages/rust/method-owners.d.ts +19 -0
- package/dist/core/ingestion/languages/rust/method-owners.js +63 -0
- package/dist/core/ingestion/languages/rust/query.d.ts +3 -0
- package/dist/core/ingestion/languages/rust/query.js +145 -0
- package/dist/core/ingestion/languages/rust/range-binding.d.ts +18 -0
- package/dist/core/ingestion/languages/rust/range-binding.js +570 -0
- package/dist/core/ingestion/languages/rust/receiver-binding.d.ts +20 -0
- package/dist/core/ingestion/languages/rust/receiver-binding.js +134 -0
- package/dist/core/ingestion/languages/rust/scope-resolver.d.ts +2 -0
- package/dist/core/ingestion/languages/rust/scope-resolver.js +62 -0
- package/dist/core/ingestion/languages/rust/simple-hooks.d.ts +8 -0
- package/dist/core/ingestion/languages/rust/simple-hooks.js +27 -0
- package/dist/core/ingestion/languages/rust.js +9 -0
- package/dist/core/ingestion/registry-primary-flag.js +1 -0
- package/dist/core/ingestion/scope-resolution/pipeline/registry.js +2 -0
- package/dist/core/wiki/generator.d.ts +1 -1
- package/dist/core/wiki/generator.js +12 -1
- package/dist/core/wiki/llm-client.d.ts +2 -2
- package/dist/core/wiki/llm-client.js +13 -5
- package/dist/core/wiki/local-cli-client.d.ts +17 -0
- package/dist/core/wiki/local-cli-client.js +270 -0
- package/dist/storage/repo-manager.d.ts +3 -1
- package/package.json +1 -1
- package/scripts/cross-platform-tests.ts +1 -0
package/dist/cli/i18n/en.d.ts
CHANGED
|
@@ -155,7 +155,7 @@ export declare const en: {
|
|
|
155
155
|
readonly 'help.option.clean.all': "Clean all indexed repos";
|
|
156
156
|
readonly 'help.option.clean.lbugSidecars': "Clean quarantined LadybugDB missing-shadow WAL sidecars";
|
|
157
157
|
readonly 'help.option.wiki.force': "Force full regeneration even if up to date";
|
|
158
|
-
readonly 'help.option.wiki.provider': "LLM provider: openai
|
|
158
|
+
readonly 'help.option.wiki.provider': "LLM provider: openai, openrouter, azure, custom, cursor, claude, or codex (default: openai)";
|
|
159
159
|
readonly 'help.option.wiki.model': "LLM model or Azure deployment name (default: minimax/minimax-m2.5)";
|
|
160
160
|
readonly 'help.option.wiki.baseUrl': "LLM API base URL. Azure v1: https://{resource}.openai.azure.com/openai/v1";
|
|
161
161
|
readonly 'help.option.wiki.apiKey': "LLM API key or Azure api-key (saved to ~/.gitnexus/config.json)";
|
package/dist/cli/i18n/en.js
CHANGED
|
@@ -155,7 +155,7 @@ export const en = {
|
|
|
155
155
|
'help.option.clean.all': 'Clean all indexed repos',
|
|
156
156
|
'help.option.clean.lbugSidecars': 'Clean quarantined LadybugDB missing-shadow WAL sidecars',
|
|
157
157
|
'help.option.wiki.force': 'Force full regeneration even if up to date',
|
|
158
|
-
'help.option.wiki.provider': 'LLM provider: openai
|
|
158
|
+
'help.option.wiki.provider': 'LLM provider: openai, openrouter, azure, custom, cursor, claude, or codex (default: openai)',
|
|
159
159
|
'help.option.wiki.model': 'LLM model or Azure deployment name (default: minimax/minimax-m2.5)',
|
|
160
160
|
'help.option.wiki.baseUrl': 'LLM API base URL. Azure v1: https://{resource}.openai.azure.com/openai/v1',
|
|
161
161
|
'help.option.wiki.apiKey': 'LLM API key or Azure api-key (saved to ~/.gitnexus/config.json)',
|
|
@@ -156,7 +156,7 @@ export declare const cliResources: {
|
|
|
156
156
|
readonly 'help.option.clean.all': "Clean all indexed repos";
|
|
157
157
|
readonly 'help.option.clean.lbugSidecars': "Clean quarantined LadybugDB missing-shadow WAL sidecars";
|
|
158
158
|
readonly 'help.option.wiki.force': "Force full regeneration even if up to date";
|
|
159
|
-
readonly 'help.option.wiki.provider': "LLM provider: openai
|
|
159
|
+
readonly 'help.option.wiki.provider': "LLM provider: openai, openrouter, azure, custom, cursor, claude, or codex (default: openai)";
|
|
160
160
|
readonly 'help.option.wiki.model': "LLM model or Azure deployment name (default: minimax/minimax-m2.5)";
|
|
161
161
|
readonly 'help.option.wiki.baseUrl': "LLM API base URL. Azure v1: https://{resource}.openai.azure.com/openai/v1";
|
|
162
162
|
readonly 'help.option.wiki.apiKey': "LLM API key or Azure api-key (saved to ~/.gitnexus/config.json)";
|
package/dist/cli/i18n/zh-CN.js
CHANGED
|
@@ -155,7 +155,7 @@ export const zhCN = {
|
|
|
155
155
|
'help.option.clean.all': '清理所有已索引仓库',
|
|
156
156
|
'help.option.clean.lbugSidecars': '清理已隔离的 LadybugDB missing-shadow WAL sidecar',
|
|
157
157
|
'help.option.wiki.force': '即使已是最新也强制完整重新生成',
|
|
158
|
-
'help.option.wiki.provider': 'LLM 提供商:openai 或
|
|
158
|
+
'help.option.wiki.provider': 'LLM 提供商:openai、openrouter、azure、custom、cursor、claude 或 codex(默认:openai)',
|
|
159
159
|
'help.option.wiki.model': 'LLM 模型或 Azure deployment 名称(默认:minimax/minimax-m2.5)',
|
|
160
160
|
'help.option.wiki.baseUrl': 'LLM API base URL。Azure v1:https://{resource}.openai.azure.com/openai/v1',
|
|
161
161
|
'help.option.wiki.apiKey': 'LLM API key 或 Azure api-key(保存到 ~/.gitnexus/config.json)',
|
package/dist/cli/index.js
CHANGED
|
@@ -94,7 +94,7 @@ program
|
|
|
94
94
|
.command('wiki [path]')
|
|
95
95
|
.description('Generate repository wiki from knowledge graph')
|
|
96
96
|
.option('-f, --force', 'Force full regeneration even if up to date')
|
|
97
|
-
.option('--provider <provider>', 'LLM provider: openai
|
|
97
|
+
.option('--provider <provider>', 'LLM provider: openai, openrouter, azure, custom, cursor, claude, or codex (default: openai)')
|
|
98
98
|
.option('--model <model>', 'LLM model or Azure deployment name (default: minimax/minimax-m2.5)')
|
|
99
99
|
.option('--base-url <url>', 'LLM API base URL. Azure v1: https://{resource}.openai.azure.com/openai/v1')
|
|
100
100
|
.option('--api-key <key>', 'LLM API key or Azure api-key (saved to ~/.gitnexus/config.json)')
|
package/dist/cli/wiki.js
CHANGED
|
@@ -13,6 +13,7 @@ import { getStoragePaths, loadMeta, loadCLIConfig, saveCLIConfig, } from '../sto
|
|
|
13
13
|
import { WikiGenerator } from '../core/wiki/generator.js';
|
|
14
14
|
import { resolveLLMConfig } from '../core/wiki/llm-client.js';
|
|
15
15
|
import { detectCursorCLI } from '../core/wiki/cursor-client.js';
|
|
16
|
+
import { detectLocalCLI } from '../core/wiki/local-cli-client.js';
|
|
16
17
|
import { logger } from '../core/logger.js';
|
|
17
18
|
function parsePositiveIntegerOption(value, flag, multiplier = 1) {
|
|
18
19
|
if (value === undefined)
|
|
@@ -27,6 +28,16 @@ function parsePositiveIntegerOption(value, flag, multiplier = 1) {
|
|
|
27
28
|
}
|
|
28
29
|
return parsed;
|
|
29
30
|
}
|
|
31
|
+
function isLocalProvider(provider) {
|
|
32
|
+
return provider === 'cursor' || provider === 'claude' || provider === 'codex';
|
|
33
|
+
}
|
|
34
|
+
function localModelConfigKey(provider) {
|
|
35
|
+
if (provider === 'cursor')
|
|
36
|
+
return 'cursorModel';
|
|
37
|
+
if (provider === 'claude')
|
|
38
|
+
return 'claudeModel';
|
|
39
|
+
return 'codexModel';
|
|
40
|
+
}
|
|
30
41
|
/**
|
|
31
42
|
* Prompt the user for input via stdin.
|
|
32
43
|
*/
|
|
@@ -163,10 +174,11 @@ const wikiCommandImpl = async (inputPath, options) => {
|
|
|
163
174
|
updates.apiVersion = options.apiVersion;
|
|
164
175
|
if (options.reasoningModel !== undefined)
|
|
165
176
|
updates.isReasoningModel = options.reasoningModel;
|
|
166
|
-
// Save model to appropriate field based on provider
|
|
177
|
+
// Save model to appropriate field based on provider.
|
|
167
178
|
if (options.model) {
|
|
168
|
-
|
|
169
|
-
|
|
179
|
+
const targetProvider = options.provider ?? existing.provider;
|
|
180
|
+
if (isLocalProvider(targetProvider)) {
|
|
181
|
+
updates[localModelConfigKey(targetProvider)] = options.model;
|
|
170
182
|
}
|
|
171
183
|
else {
|
|
172
184
|
updates.model = options.model;
|
|
@@ -176,7 +188,7 @@ const wikiCommandImpl = async (inputPath, options) => {
|
|
|
176
188
|
console.log(' Config saved to ~/.gitnexus/config.json\n');
|
|
177
189
|
}
|
|
178
190
|
const savedConfig = await loadCLIConfig();
|
|
179
|
-
const hasSavedConfig = !!(savedConfig.provider
|
|
191
|
+
const hasSavedConfig = !!(isLocalProvider(savedConfig.provider) ||
|
|
180
192
|
(savedConfig.apiKey && savedConfig.baseUrl));
|
|
181
193
|
const hasCLIOverrides = !!(options?.apiKey ||
|
|
182
194
|
options?.model ||
|
|
@@ -197,10 +209,10 @@ const wikiCommandImpl = async (inputPath, options) => {
|
|
|
197
209
|
if (!hasSavedConfig && !hasCLIOverrides) {
|
|
198
210
|
if (!process.stdin.isTTY) {
|
|
199
211
|
// Non-interactive mode — need either API key or Cursor CLI
|
|
200
|
-
if (!llmConfig.apiKey && llmConfig.provider
|
|
212
|
+
if (!llmConfig.apiKey && !isLocalProvider(llmConfig.provider)) {
|
|
201
213
|
console.log(' Error: No LLM API key found.');
|
|
202
214
|
console.log(' Set OPENAI_API_KEY or GITNEXUS_API_KEY environment variable,');
|
|
203
|
-
console.log(' or pass --api-key <key>, or use --provider cursor.\n');
|
|
215
|
+
console.log(' or pass --api-key <key>, or use --provider cursor|claude|codex.\n');
|
|
204
216
|
process.exitCode = 1;
|
|
205
217
|
return;
|
|
206
218
|
}
|
|
@@ -208,37 +220,62 @@ const wikiCommandImpl = async (inputPath, options) => {
|
|
|
208
220
|
}
|
|
209
221
|
else {
|
|
210
222
|
console.log(" No LLM configured. Let's set it up.\n");
|
|
211
|
-
console.log(' Supports OpenAI, OpenRouter, Azure, any OpenAI-compatible API, or
|
|
212
|
-
// Check if
|
|
223
|
+
console.log(' Supports OpenAI, OpenRouter, Azure, any OpenAI-compatible API, Cursor CLI, Claude CLI, or Codex CLI.\n');
|
|
224
|
+
// Check if local agent CLIs are available.
|
|
213
225
|
const hasCursor = detectCursorCLI();
|
|
226
|
+
const hasClaude = detectLocalCLI('claude');
|
|
227
|
+
const hasCodex = detectLocalCLI('codex');
|
|
228
|
+
const localChoices = [];
|
|
214
229
|
// Provider selection
|
|
215
230
|
console.log(' [1] OpenAI (api.openai.com)');
|
|
216
231
|
console.log(' [2] OpenRouter (openrouter.ai)');
|
|
217
232
|
console.log(' [3] Azure OpenAI');
|
|
218
233
|
console.log(' [4] Custom endpoint');
|
|
234
|
+
let nextChoice = 5;
|
|
219
235
|
if (hasCursor) {
|
|
220
|
-
|
|
236
|
+
const choice = String(nextChoice++);
|
|
237
|
+
localChoices.push({
|
|
238
|
+
choice,
|
|
239
|
+
provider: 'cursor',
|
|
240
|
+
});
|
|
241
|
+
console.log(` [${choice}] Cursor CLI (local, uses your Cursor subscription)`);
|
|
242
|
+
}
|
|
243
|
+
if (hasClaude) {
|
|
244
|
+
const choice = String(nextChoice++);
|
|
245
|
+
localChoices.push({
|
|
246
|
+
choice,
|
|
247
|
+
provider: 'claude',
|
|
248
|
+
});
|
|
249
|
+
console.log(` [${choice}] Claude CLI (local, uses your Claude Code login)`);
|
|
250
|
+
}
|
|
251
|
+
if (hasCodex) {
|
|
252
|
+
const choice = String(nextChoice++);
|
|
253
|
+
localChoices.push({
|
|
254
|
+
choice,
|
|
255
|
+
provider: 'codex',
|
|
256
|
+
});
|
|
257
|
+
console.log(` [${choice}] Codex CLI (local, uses your Codex login)`);
|
|
221
258
|
}
|
|
222
259
|
console.log('');
|
|
223
|
-
const maxChoice =
|
|
260
|
+
const maxChoice = String(nextChoice - 1);
|
|
224
261
|
const choice = await prompt(` Select provider (1/${maxChoice}): `);
|
|
225
262
|
let baseUrl;
|
|
226
263
|
let defaultModel;
|
|
227
264
|
let provider = 'openai';
|
|
228
265
|
let key = '';
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
266
|
+
const selectedLocal = localChoices.find((item) => item.choice === choice);
|
|
267
|
+
if (selectedLocal) {
|
|
268
|
+
// Local CLI selected - model defaults to the CLI's configured default.
|
|
269
|
+
provider = selectedLocal.provider;
|
|
232
270
|
baseUrl = '';
|
|
233
|
-
const modelInput = await prompt(' Model (leave empty for
|
|
271
|
+
const modelInput = await prompt(' Model (leave empty for CLI default): ');
|
|
234
272
|
const model = modelInput || '';
|
|
235
|
-
|
|
236
|
-
const cursorConfig = { provider: 'cursor' };
|
|
273
|
+
const localConfig = { ...savedConfig, provider };
|
|
237
274
|
if (model)
|
|
238
|
-
|
|
239
|
-
await saveCLIConfig(
|
|
275
|
+
localConfig[localModelConfigKey(provider)] = model;
|
|
276
|
+
await saveCLIConfig(localConfig);
|
|
240
277
|
console.log(' Config saved to ~/.gitnexus/config.json\n');
|
|
241
|
-
llmConfig = { ...llmConfig, provider
|
|
278
|
+
llmConfig = { ...llmConfig, provider, model, apiKey: '', baseUrl: '' };
|
|
242
279
|
}
|
|
243
280
|
else if (choice === '3') {
|
|
244
281
|
// Azure OpenAI guided setup — minimal prompts
|
|
@@ -281,6 +318,7 @@ const wikiCommandImpl = async (inputPath, options) => {
|
|
|
281
318
|
// Always use v1 API format — no need for api-version
|
|
282
319
|
const azureBaseUrl = `${endpoint}/openai/v1`;
|
|
283
320
|
await saveCLIConfig({
|
|
321
|
+
...savedConfig,
|
|
284
322
|
apiKey: azureKey,
|
|
285
323
|
baseUrl: azureBaseUrl,
|
|
286
324
|
model: deploymentName,
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export function rustArityCompatibility(def, callsite) {
|
|
2
|
+
const max = def.parameterCount;
|
|
3
|
+
const min = def.requiredParameterCount;
|
|
4
|
+
if (max === undefined && min === undefined)
|
|
5
|
+
return 'unknown';
|
|
6
|
+
if (!Number.isFinite(callsite.arity) || callsite.arity < 0)
|
|
7
|
+
return 'unknown';
|
|
8
|
+
if (min !== undefined && callsite.arity < min)
|
|
9
|
+
return 'incompatible';
|
|
10
|
+
if (max !== undefined && callsite.arity > max)
|
|
11
|
+
return 'incompatible';
|
|
12
|
+
return 'compatible';
|
|
13
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare function recordRustCacheHit(): void;
|
|
2
|
+
export declare function recordRustCacheMiss(): void;
|
|
3
|
+
export declare function getRustCaptureCacheStats(): {
|
|
4
|
+
readonly hits: number;
|
|
5
|
+
readonly misses: number;
|
|
6
|
+
};
|
|
7
|
+
export declare function resetRustCaptureCacheStats(): void;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
let hits = 0;
|
|
2
|
+
let misses = 0;
|
|
3
|
+
export function recordRustCacheHit() {
|
|
4
|
+
hits++;
|
|
5
|
+
}
|
|
6
|
+
export function recordRustCacheMiss() {
|
|
7
|
+
misses++;
|
|
8
|
+
}
|
|
9
|
+
export function getRustCaptureCacheStats() {
|
|
10
|
+
return { hits, misses };
|
|
11
|
+
}
|
|
12
|
+
export function resetRustCaptureCacheStats() {
|
|
13
|
+
hits = 0;
|
|
14
|
+
misses = 0;
|
|
15
|
+
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { findNodeAtRange, nodeToCapture, syntheticCapture, } from '../../utils/ast-helpers.js';
|
|
2
|
+
import { getRustParser, getRustScopeQuery } from './query.js';
|
|
3
|
+
import { recordRustCacheHit, recordRustCacheMiss } from './cache-stats.js';
|
|
4
|
+
import { splitRustUseDeclaration } from './import-decomposer.js';
|
|
5
|
+
import { synthesizeRustReceiverBinding } from './receiver-binding.js';
|
|
6
|
+
import { getTreeSitterBufferSize } from '../../constants.js';
|
|
7
|
+
import { parseSourceSafe } from '../../../tree-sitter/safe-parse.js';
|
|
8
|
+
export function emitRustScopeCaptures(sourceText, _filePath, cachedTree) {
|
|
9
|
+
let tree = cachedTree;
|
|
10
|
+
if (tree === undefined) {
|
|
11
|
+
tree = parseSourceSafe(getRustParser(), sourceText, undefined, {
|
|
12
|
+
bufferSize: getTreeSitterBufferSize(sourceText),
|
|
13
|
+
});
|
|
14
|
+
recordRustCacheMiss();
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
recordRustCacheHit();
|
|
18
|
+
}
|
|
19
|
+
const rawMatches = getRustScopeQuery().matches(tree.rootNode);
|
|
20
|
+
const out = [];
|
|
21
|
+
for (const m of rawMatches) {
|
|
22
|
+
const grouped = {};
|
|
23
|
+
for (const c of m.captures) {
|
|
24
|
+
const tag = '@' + c.name;
|
|
25
|
+
if (tag.startsWith('@_'))
|
|
26
|
+
continue;
|
|
27
|
+
grouped[tag] = nodeToCapture(tag, c.node);
|
|
28
|
+
}
|
|
29
|
+
if (Object.keys(grouped).length === 0)
|
|
30
|
+
continue;
|
|
31
|
+
// Decompose use declarations into individual import captures
|
|
32
|
+
if (grouped['@import.statement'] !== undefined) {
|
|
33
|
+
const anchor = grouped['@import.statement'];
|
|
34
|
+
const useNode = findNodeAtRange(tree.rootNode, anchor.range, 'use_declaration');
|
|
35
|
+
if (useNode !== null) {
|
|
36
|
+
out.push(...splitRustUseDeclaration(useNode));
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
// Synthesize self receiver bindings for methods inside impl blocks
|
|
41
|
+
let cachedImplLookup;
|
|
42
|
+
if (grouped['@scope.function'] !== undefined) {
|
|
43
|
+
const scopeCap = grouped['@scope.function'];
|
|
44
|
+
const fnNode = findNodeAtRange(tree.rootNode, scopeCap.range, 'function_item');
|
|
45
|
+
if (fnNode !== null) {
|
|
46
|
+
const implNode = findEnclosingImpl(fnNode);
|
|
47
|
+
cachedImplLookup = { fnNode, implNode };
|
|
48
|
+
const receiver = synthesizeRustReceiverBinding(fnNode, implNode);
|
|
49
|
+
if (receiver !== null)
|
|
50
|
+
out.push(receiver);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
// Attach declaration arity for functions/methods
|
|
54
|
+
const declAnchor = grouped['@declaration.function'];
|
|
55
|
+
if (declAnchor !== undefined) {
|
|
56
|
+
const fnNode = findNodeAtRange(tree.rootNode, declAnchor.range, 'function_item');
|
|
57
|
+
if (fnNode !== null) {
|
|
58
|
+
const implNode = cachedImplLookup?.fnNode === fnNode
|
|
59
|
+
? cachedImplLookup.implNode
|
|
60
|
+
: findEnclosingImpl(fnNode);
|
|
61
|
+
const traitNode = implNode === null ? findEnclosingTrait(fnNode) : null;
|
|
62
|
+
// Reclassify as method if inside an impl block or trait definition
|
|
63
|
+
if (implNode !== null || traitNode !== null) {
|
|
64
|
+
const nameCap = grouped['@declaration.name'];
|
|
65
|
+
delete grouped['@declaration.function'];
|
|
66
|
+
grouped['@declaration.method'] = syntheticCapture('@declaration.method', fnNode, fnNode.text);
|
|
67
|
+
if (nameCap !== undefined) {
|
|
68
|
+
grouped['@declaration.name'] = nameCap;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const arity = computeRustDeclarationArity(fnNode);
|
|
72
|
+
if (arity.parameterCount !== undefined) {
|
|
73
|
+
grouped['@declaration.parameter-count'] = syntheticCapture('@declaration.parameter-count', fnNode, String(arity.parameterCount));
|
|
74
|
+
}
|
|
75
|
+
if (arity.requiredParameterCount !== undefined) {
|
|
76
|
+
grouped['@declaration.required-parameter-count'] = syntheticCapture('@declaration.required-parameter-count', fnNode, String(arity.requiredParameterCount));
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// Hoist return-type bindings from impl block functions to module level.
|
|
81
|
+
// The auto-hoist in the scope-extractor places a type binding whose
|
|
82
|
+
// anchor matches its innermost scope on the parent scope. By using the
|
|
83
|
+
// impl_item node as the anchor (which matches the impl's Class scope),
|
|
84
|
+
// the binding lands on the Module scope — making it visible to the
|
|
85
|
+
// compound receiver's hoistTypeBindingsToModule walk.
|
|
86
|
+
if (grouped['@type-binding.return'] !== undefined &&
|
|
87
|
+
grouped['@type-binding.name'] !== undefined) {
|
|
88
|
+
const tbReturnAnchor = grouped['@type-binding.return'];
|
|
89
|
+
const fnNode = findNodeAtRange(tree.rootNode, tbReturnAnchor.range, 'function_item');
|
|
90
|
+
if (fnNode !== null) {
|
|
91
|
+
const implNode = findEnclosingImpl(fnNode);
|
|
92
|
+
if (implNode !== null) {
|
|
93
|
+
out.push({
|
|
94
|
+
'@type-binding.name': syntheticCapture('@type-binding.name', implNode, grouped['@type-binding.name'].text),
|
|
95
|
+
'@type-binding.type': syntheticCapture('@type-binding.type', implNode, grouped['@type-binding.type'].text),
|
|
96
|
+
'@type-binding.return': syntheticCapture('@type-binding.return', implNode, tbReturnAnchor.text),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Attach call arity for call expressions
|
|
102
|
+
const callAnchor = grouped['@reference.call.free'] ??
|
|
103
|
+
grouped['@reference.call.member'] ??
|
|
104
|
+
grouped['@reference.call.constructor'];
|
|
105
|
+
if (callAnchor !== undefined) {
|
|
106
|
+
const callNode = findNodeAtRange(tree.rootNode, callAnchor.range, 'call_expression') ??
|
|
107
|
+
findNodeAtRange(tree.rootNode, callAnchor.range, 'struct_expression');
|
|
108
|
+
if (callNode !== null) {
|
|
109
|
+
const arity = computeRustCallArity(callNode);
|
|
110
|
+
grouped['@reference.arity'] = syntheticCapture('@reference.arity', callNode, String(arity));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
out.push(grouped);
|
|
114
|
+
}
|
|
115
|
+
return out;
|
|
116
|
+
}
|
|
117
|
+
function findEnclosingImpl(node) {
|
|
118
|
+
let current = node.parent;
|
|
119
|
+
while (current !== null) {
|
|
120
|
+
if (current.type === 'impl_item')
|
|
121
|
+
return current;
|
|
122
|
+
if (current.type === 'source_file' || current.type === 'mod_item')
|
|
123
|
+
return null;
|
|
124
|
+
current = current.parent;
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
function findEnclosingTrait(node) {
|
|
129
|
+
let current = node.parent;
|
|
130
|
+
while (current !== null) {
|
|
131
|
+
if (current.type === 'trait_item')
|
|
132
|
+
return current;
|
|
133
|
+
if (current.type === 'source_file' || current.type === 'mod_item')
|
|
134
|
+
return null;
|
|
135
|
+
current = current.parent;
|
|
136
|
+
}
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
function computeRustDeclarationArity(fnNode) {
|
|
140
|
+
const params = fnNode.childForFieldName('parameters');
|
|
141
|
+
if (params === null)
|
|
142
|
+
return {};
|
|
143
|
+
let count = 0;
|
|
144
|
+
for (let i = 0; i < params.namedChildCount; i++) {
|
|
145
|
+
const child = params.namedChild(i);
|
|
146
|
+
if (child === null)
|
|
147
|
+
continue;
|
|
148
|
+
if (child.type === 'self_parameter')
|
|
149
|
+
continue;
|
|
150
|
+
if (child.type === 'parameter')
|
|
151
|
+
count++;
|
|
152
|
+
}
|
|
153
|
+
// Rust has no default parameters or overloading
|
|
154
|
+
return { parameterCount: count, requiredParameterCount: count };
|
|
155
|
+
}
|
|
156
|
+
function computeRustCallArity(callNode) {
|
|
157
|
+
if (callNode.type === 'struct_expression') {
|
|
158
|
+
const body = callNode.childForFieldName('body');
|
|
159
|
+
if (body === null)
|
|
160
|
+
return 0;
|
|
161
|
+
let count = 0;
|
|
162
|
+
for (let i = 0; i < body.namedChildCount; i++) {
|
|
163
|
+
const t = body.namedChild(i)?.type;
|
|
164
|
+
if (t === 'field_initializer' || t === 'shorthand_field_initializer')
|
|
165
|
+
count++;
|
|
166
|
+
}
|
|
167
|
+
return count;
|
|
168
|
+
}
|
|
169
|
+
const args = callNode.childForFieldName('arguments');
|
|
170
|
+
if (args === null)
|
|
171
|
+
return 0;
|
|
172
|
+
let count = 0;
|
|
173
|
+
for (let i = 0; i < args.namedChildCount; i++) {
|
|
174
|
+
const child = args.namedChild(i);
|
|
175
|
+
if (child !== null)
|
|
176
|
+
count++;
|
|
177
|
+
}
|
|
178
|
+
return count;
|
|
179
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { CaptureMatch } from '../../../../_shared/index.js';
|
|
2
|
+
import type { SyntaxNode } from '../../utils/ast-helpers.js';
|
|
3
|
+
/**
|
|
4
|
+
* Decompose a Rust `use_declaration` into individual import captures.
|
|
5
|
+
* Handles simple paths, grouped imports ({A, B}), wildcards (*),
|
|
6
|
+
* renames (as), and `pub use` re-exports.
|
|
7
|
+
*/
|
|
8
|
+
export declare function splitRustUseDeclaration(node: SyntaxNode): CaptureMatch[];
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { syntheticCapture } from '../../utils/ast-helpers.js';
|
|
2
|
+
/**
|
|
3
|
+
* Decompose a Rust `use_declaration` into individual import captures.
|
|
4
|
+
* Handles simple paths, grouped imports ({A, B}), wildcards (*),
|
|
5
|
+
* renames (as), and `pub use` re-exports.
|
|
6
|
+
*/
|
|
7
|
+
export function splitRustUseDeclaration(node) {
|
|
8
|
+
if (node.type !== 'use_declaration')
|
|
9
|
+
return [];
|
|
10
|
+
const isReexport = hasVisibilityModifier(node);
|
|
11
|
+
const argument = getUseArgument(node);
|
|
12
|
+
if (argument === null)
|
|
13
|
+
return [];
|
|
14
|
+
return decomposeUseArgument(argument, '', isReexport, node);
|
|
15
|
+
}
|
|
16
|
+
function hasVisibilityModifier(node) {
|
|
17
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
18
|
+
if (node.child(i)?.type === 'visibility_modifier')
|
|
19
|
+
return true;
|
|
20
|
+
}
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
function getUseArgument(node) {
|
|
24
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
25
|
+
const child = node.child(i);
|
|
26
|
+
if (child === null)
|
|
27
|
+
continue;
|
|
28
|
+
if (child.type === 'scoped_identifier' ||
|
|
29
|
+
child.type === 'scoped_use_list' ||
|
|
30
|
+
child.type === 'use_wildcard' ||
|
|
31
|
+
child.type === 'use_as_clause' ||
|
|
32
|
+
child.type === 'identifier' ||
|
|
33
|
+
child.type === 'use_list') {
|
|
34
|
+
return child;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
function decomposeUseArgument(node, prefixPath, isReexport, anchor) {
|
|
40
|
+
switch (node.type) {
|
|
41
|
+
case 'scoped_identifier': {
|
|
42
|
+
const path = buildScopedPath(node);
|
|
43
|
+
const segments = path.split('::');
|
|
44
|
+
const name = segments[segments.length - 1];
|
|
45
|
+
return [
|
|
46
|
+
makeImportCapture(anchor, isReexport ? 'reexport' : 'named', joinPaths(prefixPath, path), name, undefined),
|
|
47
|
+
];
|
|
48
|
+
}
|
|
49
|
+
case 'scoped_use_list': {
|
|
50
|
+
const pathNode = node.childForFieldName('path');
|
|
51
|
+
const listNode = node.childForFieldName('list');
|
|
52
|
+
const pathStr = pathNode ? buildNodePath(pathNode) : '';
|
|
53
|
+
const fullPrefix = joinPaths(prefixPath, pathStr);
|
|
54
|
+
if (listNode === null)
|
|
55
|
+
return [];
|
|
56
|
+
return decomposeUseList(listNode, fullPrefix, isReexport, anchor);
|
|
57
|
+
}
|
|
58
|
+
case 'use_list': {
|
|
59
|
+
return decomposeUseList(node, prefixPath, isReexport, anchor);
|
|
60
|
+
}
|
|
61
|
+
case 'use_wildcard': {
|
|
62
|
+
const wcPath = buildWildcardPath(node);
|
|
63
|
+
return [makeImportCapture(anchor, 'wildcard', joinPaths(prefixPath, wcPath), '*', undefined)];
|
|
64
|
+
}
|
|
65
|
+
case 'use_as_clause': {
|
|
66
|
+
const pathChild = node.childForFieldName('path');
|
|
67
|
+
const aliasChild = node.childForFieldName('alias');
|
|
68
|
+
if (pathChild === null || aliasChild === null)
|
|
69
|
+
return [];
|
|
70
|
+
const originalName = pathChild.type === 'scoped_identifier' ? buildScopedPath(pathChild) : pathChild.text;
|
|
71
|
+
const aliasName = aliasChild.text;
|
|
72
|
+
const segments = originalName.split('::');
|
|
73
|
+
const importedName = segments[segments.length - 1];
|
|
74
|
+
return [
|
|
75
|
+
makeImportCapture(anchor, isReexport ? 'reexport' : 'named', joinPaths(prefixPath, originalName), importedName, aliasName),
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
case 'identifier': {
|
|
79
|
+
return [
|
|
80
|
+
makeImportCapture(anchor, isReexport ? 'reexport' : 'named', joinPaths(prefixPath, node.text), node.text, undefined),
|
|
81
|
+
];
|
|
82
|
+
}
|
|
83
|
+
default:
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
function decomposeUseList(listNode, prefix, isReexport, anchor) {
|
|
88
|
+
const out = [];
|
|
89
|
+
for (let i = 0; i < listNode.namedChildCount; i++) {
|
|
90
|
+
const child = listNode.namedChild(i);
|
|
91
|
+
if (child === null)
|
|
92
|
+
continue;
|
|
93
|
+
if (child.type === 'self') {
|
|
94
|
+
// `use crate::models::{self}` — imports the module itself
|
|
95
|
+
const segments = prefix.split('::').filter(Boolean);
|
|
96
|
+
const name = segments[segments.length - 1] ?? 'self';
|
|
97
|
+
out.push(makeImportCapture(anchor, 'namespace', prefix, name, undefined));
|
|
98
|
+
}
|
|
99
|
+
else {
|
|
100
|
+
out.push(...decomposeUseArgument(child, prefix, isReexport, anchor));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return out;
|
|
104
|
+
}
|
|
105
|
+
function buildScopedPath(node) {
|
|
106
|
+
if (node.type === 'scoped_identifier') {
|
|
107
|
+
const parts = [];
|
|
108
|
+
collectScopedParts(node, parts);
|
|
109
|
+
return parts.join('::');
|
|
110
|
+
}
|
|
111
|
+
return node.text;
|
|
112
|
+
}
|
|
113
|
+
function collectScopedParts(node, parts) {
|
|
114
|
+
if (node.type === 'scoped_identifier') {
|
|
115
|
+
const pathNode = node.childForFieldName('path');
|
|
116
|
+
const nameNode = node.childForFieldName('name');
|
|
117
|
+
if (pathNode)
|
|
118
|
+
collectScopedParts(pathNode, parts);
|
|
119
|
+
if (nameNode)
|
|
120
|
+
parts.push(nameNode.text);
|
|
121
|
+
}
|
|
122
|
+
else {
|
|
123
|
+
parts.push(node.text);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function buildNodePath(node) {
|
|
127
|
+
if (node.type === 'scoped_identifier') {
|
|
128
|
+
return buildScopedPath(node);
|
|
129
|
+
}
|
|
130
|
+
return node.text;
|
|
131
|
+
}
|
|
132
|
+
function buildWildcardPath(node) {
|
|
133
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
134
|
+
const child = node.child(i);
|
|
135
|
+
if (child === null)
|
|
136
|
+
continue;
|
|
137
|
+
if (child.type === 'scoped_identifier')
|
|
138
|
+
return buildScopedPath(child);
|
|
139
|
+
if (child.type === 'identifier')
|
|
140
|
+
return child.text;
|
|
141
|
+
}
|
|
142
|
+
return '';
|
|
143
|
+
}
|
|
144
|
+
function joinPaths(prefix, suffix) {
|
|
145
|
+
if (!prefix)
|
|
146
|
+
return suffix;
|
|
147
|
+
if (!suffix)
|
|
148
|
+
return prefix;
|
|
149
|
+
return `${prefix}::${suffix}`;
|
|
150
|
+
}
|
|
151
|
+
function makeImportCapture(anchor, kind, source, name, alias) {
|
|
152
|
+
return {
|
|
153
|
+
'@import.statement': syntheticCapture('@import.statement', anchor, anchor.text),
|
|
154
|
+
'@import.kind': syntheticCapture('@import.kind', anchor, kind),
|
|
155
|
+
'@import.source': syntheticCapture('@import.source', anchor, source),
|
|
156
|
+
'@import.name': syntheticCapture('@import.name', anchor, alias ?? name),
|
|
157
|
+
...(alias !== undefined
|
|
158
|
+
? {
|
|
159
|
+
'@import.alias': syntheticCapture('@import.alias', anchor, alias),
|
|
160
|
+
'@import.original-name': syntheticCapture('@import.original-name', anchor, name),
|
|
161
|
+
}
|
|
162
|
+
: {}),
|
|
163
|
+
};
|
|
164
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve a Rust `use` import path to a repo-relative file path.
|
|
3
|
+
*
|
|
4
|
+
* Rust module resolution rules:
|
|
5
|
+
* - `crate::foo::bar` → `src/foo/bar.rs` or `src/foo/bar/mod.rs`
|
|
6
|
+
* - `super::foo` → parent directory's `foo.rs` or `foo/mod.rs`
|
|
7
|
+
* - `self::foo` → same directory's `foo.rs` or `foo/mod.rs`
|
|
8
|
+
* - External crate imports (no `crate::`/`super::`/`self::`) → null
|
|
9
|
+
*/
|
|
10
|
+
export declare function resolveRustImportTarget(targetRaw: string, fromFile: string, allFilePaths: ReadonlySet<string>, _resolutionConfig?: unknown): string | readonly string[] | null;
|
|
11
|
+
export interface RustResolveContext {
|
|
12
|
+
readonly fromFile: string;
|
|
13
|
+
readonly allFilePaths: ReadonlySet<string>;
|
|
14
|
+
}
|