ucn 3.0.0
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.
Potentially problematic release.
This version of ucn might be problematic. Click here for more details.
- package/.claude/skills/ucn/SKILL.md +77 -0
- package/LICENSE +21 -0
- package/README.md +135 -0
- package/cli/index.js +2437 -0
- package/core/discovery.js +513 -0
- package/core/imports.js +558 -0
- package/core/output.js +1274 -0
- package/core/parser.js +279 -0
- package/core/project.js +3261 -0
- package/index.js +52 -0
- package/languages/go.js +653 -0
- package/languages/index.js +267 -0
- package/languages/java.js +826 -0
- package/languages/javascript.js +1346 -0
- package/languages/python.js +667 -0
- package/languages/rust.js +950 -0
- package/languages/utils.js +457 -0
- package/package.json +42 -0
- package/test/fixtures/go/go.mod +3 -0
- package/test/fixtures/go/main.go +257 -0
- package/test/fixtures/go/service.go +187 -0
- package/test/fixtures/java/DataService.java +279 -0
- package/test/fixtures/java/Main.java +287 -0
- package/test/fixtures/java/Utils.java +199 -0
- package/test/fixtures/java/pom.xml +6 -0
- package/test/fixtures/javascript/main.js +109 -0
- package/test/fixtures/javascript/package.json +1 -0
- package/test/fixtures/javascript/service.js +88 -0
- package/test/fixtures/javascript/utils.js +67 -0
- package/test/fixtures/python/main.py +198 -0
- package/test/fixtures/python/pyproject.toml +3 -0
- package/test/fixtures/python/service.py +166 -0
- package/test/fixtures/python/utils.py +118 -0
- package/test/fixtures/rust/Cargo.toml +3 -0
- package/test/fixtures/rust/main.rs +253 -0
- package/test/fixtures/rust/service.rs +210 -0
- package/test/fixtures/rust/utils.rs +154 -0
- package/test/fixtures/typescript/main.ts +154 -0
- package/test/fixtures/typescript/package.json +1 -0
- package/test/fixtures/typescript/repository.ts +149 -0
- package/test/fixtures/typescript/types.ts +114 -0
- package/test/parser.test.js +3661 -0
- package/test/public-repos-test.js +477 -0
- package/test/systematic-test.js +619 -0
- package/ucn.js +8 -0
|
@@ -0,0 +1,950 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* languages/rust.js - Tree-sitter based Rust parsing
|
|
3
|
+
*
|
|
4
|
+
* Handles: function definitions, struct/enum/trait/impl blocks,
|
|
5
|
+
* modules, macros, and const/static declarations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const {
|
|
9
|
+
traverseTree,
|
|
10
|
+
nodeToLocation,
|
|
11
|
+
parseStructuredParams,
|
|
12
|
+
extractRustDocstring
|
|
13
|
+
} = require('./utils');
|
|
14
|
+
const { PARSE_OPTIONS } = require('./index');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extract return type from Rust function
|
|
18
|
+
*/
|
|
19
|
+
function extractReturnType(node) {
|
|
20
|
+
const returnTypeNode = node.childForFieldName('return_type');
|
|
21
|
+
if (returnTypeNode) {
|
|
22
|
+
let text = returnTypeNode.text.trim();
|
|
23
|
+
if (text.startsWith('->')) {
|
|
24
|
+
text = text.slice(2).trim();
|
|
25
|
+
}
|
|
26
|
+
return text || null;
|
|
27
|
+
}
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Extract Rust parameters
|
|
33
|
+
*/
|
|
34
|
+
function extractRustParams(paramsNode) {
|
|
35
|
+
if (!paramsNode) return '...';
|
|
36
|
+
const text = paramsNode.text;
|
|
37
|
+
let params = text.replace(/^\(|\)$/g, '').trim();
|
|
38
|
+
if (!params) return '...';
|
|
39
|
+
return params;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Extract visibility modifier
|
|
44
|
+
*/
|
|
45
|
+
function extractVisibility(text) {
|
|
46
|
+
const firstLine = text.split('\n')[0];
|
|
47
|
+
if (firstLine.includes('pub(crate)')) return 'pub(crate)';
|
|
48
|
+
if (firstLine.includes('pub(self)')) return 'pub(self)';
|
|
49
|
+
if (firstLine.includes('pub(super)')) return 'pub(super)';
|
|
50
|
+
if (firstLine.includes('pub ')) return 'pub';
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Find all functions in Rust code using tree-sitter
|
|
56
|
+
*/
|
|
57
|
+
function findFunctions(code, parser) {
|
|
58
|
+
const tree = parser.parse(code, undefined, PARSE_OPTIONS);
|
|
59
|
+
const functions = [];
|
|
60
|
+
const processedRanges = new Set();
|
|
61
|
+
|
|
62
|
+
traverseTree(tree.rootNode, (node) => {
|
|
63
|
+
const rangeKey = `${node.startIndex}-${node.endIndex}`;
|
|
64
|
+
|
|
65
|
+
if (node.type === 'function_item') {
|
|
66
|
+
if (processedRanges.has(rangeKey)) return true;
|
|
67
|
+
processedRanges.add(rangeKey);
|
|
68
|
+
|
|
69
|
+
const nameNode = node.childForFieldName('name');
|
|
70
|
+
const paramsNode = node.childForFieldName('parameters');
|
|
71
|
+
|
|
72
|
+
if (nameNode) {
|
|
73
|
+
const { startLine, endLine, indent } = nodeToLocation(node, code);
|
|
74
|
+
const text = node.text;
|
|
75
|
+
const firstLine = text.split('\n')[0];
|
|
76
|
+
|
|
77
|
+
const isAsync = firstLine.includes('async ');
|
|
78
|
+
const isUnsafe = firstLine.includes('unsafe ');
|
|
79
|
+
const isConst = firstLine.includes('const fn');
|
|
80
|
+
const visibility = extractVisibility(text);
|
|
81
|
+
const returnType = extractReturnType(node);
|
|
82
|
+
const docstring = extractRustDocstring(code, startLine);
|
|
83
|
+
const generics = extractGenerics(node);
|
|
84
|
+
|
|
85
|
+
const modifiers = [];
|
|
86
|
+
if (visibility) modifiers.push(visibility);
|
|
87
|
+
if (isAsync) modifiers.push('async');
|
|
88
|
+
if (isUnsafe) modifiers.push('unsafe');
|
|
89
|
+
if (isConst) modifiers.push('const');
|
|
90
|
+
|
|
91
|
+
functions.push({
|
|
92
|
+
name: nameNode.text,
|
|
93
|
+
params: extractRustParams(paramsNode),
|
|
94
|
+
paramsStructured: parseStructuredParams(paramsNode, 'rust'),
|
|
95
|
+
startLine,
|
|
96
|
+
endLine,
|
|
97
|
+
indent,
|
|
98
|
+
modifiers,
|
|
99
|
+
...(returnType && { returnType }),
|
|
100
|
+
...(docstring && { docstring }),
|
|
101
|
+
...(generics && { generics })
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return true;
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
functions.sort((a, b) => a.startLine - b.startLine);
|
|
111
|
+
return functions;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Extract generics from a node
|
|
116
|
+
*/
|
|
117
|
+
function extractGenerics(node) {
|
|
118
|
+
const typeParamsNode = node.childForFieldName('type_parameters');
|
|
119
|
+
if (typeParamsNode) {
|
|
120
|
+
return typeParamsNode.text;
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Find all types (structs, enums, traits, impls) in Rust code
|
|
127
|
+
*/
|
|
128
|
+
function findClasses(code, parser) {
|
|
129
|
+
const tree = parser.parse(code, undefined, PARSE_OPTIONS);
|
|
130
|
+
const types = [];
|
|
131
|
+
const processedRanges = new Set();
|
|
132
|
+
|
|
133
|
+
traverseTree(tree.rootNode, (node) => {
|
|
134
|
+
const rangeKey = `${node.startIndex}-${node.endIndex}`;
|
|
135
|
+
|
|
136
|
+
// Struct items
|
|
137
|
+
if (node.type === 'struct_item') {
|
|
138
|
+
if (processedRanges.has(rangeKey)) return true;
|
|
139
|
+
processedRanges.add(rangeKey);
|
|
140
|
+
|
|
141
|
+
const nameNode = node.childForFieldName('name');
|
|
142
|
+
if (nameNode) {
|
|
143
|
+
const { startLine, endLine } = nodeToLocation(node, code);
|
|
144
|
+
const docstring = extractRustDocstring(code, startLine);
|
|
145
|
+
const visibility = extractVisibility(node.text);
|
|
146
|
+
const generics = extractGenerics(node);
|
|
147
|
+
const members = extractStructFields(node, code);
|
|
148
|
+
|
|
149
|
+
types.push({
|
|
150
|
+
name: nameNode.text,
|
|
151
|
+
startLine,
|
|
152
|
+
endLine,
|
|
153
|
+
type: 'struct',
|
|
154
|
+
members,
|
|
155
|
+
modifiers: visibility ? [visibility] : [],
|
|
156
|
+
...(docstring && { docstring }),
|
|
157
|
+
...(generics && { generics })
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return true;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Enum items
|
|
164
|
+
if (node.type === 'enum_item') {
|
|
165
|
+
if (processedRanges.has(rangeKey)) return true;
|
|
166
|
+
processedRanges.add(rangeKey);
|
|
167
|
+
|
|
168
|
+
const nameNode = node.childForFieldName('name');
|
|
169
|
+
if (nameNode) {
|
|
170
|
+
const { startLine, endLine } = nodeToLocation(node, code);
|
|
171
|
+
const docstring = extractRustDocstring(code, startLine);
|
|
172
|
+
const visibility = extractVisibility(node.text);
|
|
173
|
+
const generics = extractGenerics(node);
|
|
174
|
+
|
|
175
|
+
types.push({
|
|
176
|
+
name: nameNode.text,
|
|
177
|
+
startLine,
|
|
178
|
+
endLine,
|
|
179
|
+
type: 'enum',
|
|
180
|
+
members: [],
|
|
181
|
+
modifiers: visibility ? [visibility] : [],
|
|
182
|
+
...(docstring && { docstring }),
|
|
183
|
+
...(generics && { generics })
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Trait items
|
|
190
|
+
if (node.type === 'trait_item') {
|
|
191
|
+
if (processedRanges.has(rangeKey)) return true;
|
|
192
|
+
processedRanges.add(rangeKey);
|
|
193
|
+
|
|
194
|
+
const nameNode = node.childForFieldName('name');
|
|
195
|
+
if (nameNode) {
|
|
196
|
+
const { startLine, endLine } = nodeToLocation(node, code);
|
|
197
|
+
const docstring = extractRustDocstring(code, startLine);
|
|
198
|
+
const visibility = extractVisibility(node.text);
|
|
199
|
+
const generics = extractGenerics(node);
|
|
200
|
+
|
|
201
|
+
types.push({
|
|
202
|
+
name: nameNode.text,
|
|
203
|
+
startLine,
|
|
204
|
+
endLine,
|
|
205
|
+
type: 'trait',
|
|
206
|
+
members: [],
|
|
207
|
+
modifiers: visibility ? [visibility] : [],
|
|
208
|
+
...(docstring && { docstring }),
|
|
209
|
+
...(generics && { generics })
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Impl items
|
|
216
|
+
if (node.type === 'impl_item') {
|
|
217
|
+
if (processedRanges.has(rangeKey)) return true;
|
|
218
|
+
processedRanges.add(rangeKey);
|
|
219
|
+
|
|
220
|
+
const { startLine, endLine } = nodeToLocation(node, code);
|
|
221
|
+
const implInfo = extractImplInfo(node);
|
|
222
|
+
const docstring = extractRustDocstring(code, startLine);
|
|
223
|
+
|
|
224
|
+
types.push({
|
|
225
|
+
name: implInfo.name,
|
|
226
|
+
startLine,
|
|
227
|
+
endLine,
|
|
228
|
+
type: 'impl',
|
|
229
|
+
traitName: implInfo.traitName,
|
|
230
|
+
typeName: implInfo.typeName,
|
|
231
|
+
members: extractImplMembers(node, code),
|
|
232
|
+
modifiers: [],
|
|
233
|
+
...(docstring && { docstring })
|
|
234
|
+
});
|
|
235
|
+
return false; // Don't traverse into impl body
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Module items
|
|
239
|
+
if (node.type === 'mod_item') {
|
|
240
|
+
if (processedRanges.has(rangeKey)) return true;
|
|
241
|
+
processedRanges.add(rangeKey);
|
|
242
|
+
|
|
243
|
+
const nameNode = node.childForFieldName('name');
|
|
244
|
+
if (nameNode) {
|
|
245
|
+
const { startLine, endLine } = nodeToLocation(node, code);
|
|
246
|
+
const docstring = extractRustDocstring(code, startLine);
|
|
247
|
+
const visibility = extractVisibility(node.text);
|
|
248
|
+
|
|
249
|
+
types.push({
|
|
250
|
+
name: nameNode.text,
|
|
251
|
+
startLine,
|
|
252
|
+
endLine,
|
|
253
|
+
type: 'module',
|
|
254
|
+
members: [],
|
|
255
|
+
modifiers: visibility ? [visibility] : [],
|
|
256
|
+
...(docstring && { docstring })
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
return true;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Macro definitions
|
|
263
|
+
if (node.type === 'macro_definition') {
|
|
264
|
+
if (processedRanges.has(rangeKey)) return true;
|
|
265
|
+
processedRanges.add(rangeKey);
|
|
266
|
+
|
|
267
|
+
const nameNode = node.childForFieldName('name');
|
|
268
|
+
if (nameNode) {
|
|
269
|
+
const { startLine, endLine } = nodeToLocation(node, code);
|
|
270
|
+
const docstring = extractRustDocstring(code, startLine);
|
|
271
|
+
|
|
272
|
+
types.push({
|
|
273
|
+
name: nameNode.text,
|
|
274
|
+
startLine,
|
|
275
|
+
endLine,
|
|
276
|
+
type: 'macro',
|
|
277
|
+
members: [],
|
|
278
|
+
modifiers: [],
|
|
279
|
+
...(docstring && { docstring })
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Type aliases (only top-level, not inside traits/impls)
|
|
286
|
+
if (node.type === 'type_item') {
|
|
287
|
+
if (processedRanges.has(rangeKey)) return true;
|
|
288
|
+
|
|
289
|
+
// Skip if inside trait or impl
|
|
290
|
+
let parent = node.parent;
|
|
291
|
+
while (parent) {
|
|
292
|
+
if (parent.type === 'trait_item' || parent.type === 'impl_item') {
|
|
293
|
+
return true; // Skip this one
|
|
294
|
+
}
|
|
295
|
+
parent = parent.parent;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
processedRanges.add(rangeKey);
|
|
299
|
+
|
|
300
|
+
const nameNode = node.childForFieldName('name');
|
|
301
|
+
if (nameNode) {
|
|
302
|
+
const { startLine, endLine } = nodeToLocation(node, code);
|
|
303
|
+
const docstring = extractRustDocstring(code, startLine);
|
|
304
|
+
const visibility = extractVisibility(node.text);
|
|
305
|
+
|
|
306
|
+
types.push({
|
|
307
|
+
name: nameNode.text,
|
|
308
|
+
startLine,
|
|
309
|
+
endLine,
|
|
310
|
+
type: 'type',
|
|
311
|
+
members: [],
|
|
312
|
+
modifiers: visibility ? [visibility] : [],
|
|
313
|
+
...(docstring && { docstring })
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
return true;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
return true;
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
types.sort((a, b) => a.startLine - b.startLine);
|
|
323
|
+
return types;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Extract struct fields
|
|
328
|
+
*/
|
|
329
|
+
function extractStructFields(structNode, code) {
|
|
330
|
+
const fields = [];
|
|
331
|
+
const bodyNode = structNode.childForFieldName('body');
|
|
332
|
+
if (!bodyNode) return fields;
|
|
333
|
+
|
|
334
|
+
for (let i = 0; i < bodyNode.namedChildCount; i++) {
|
|
335
|
+
const field = bodyNode.namedChild(i);
|
|
336
|
+
if (field.type === 'field_declaration') {
|
|
337
|
+
const { startLine, endLine } = nodeToLocation(field, code);
|
|
338
|
+
const nameNode = field.childForFieldName('name');
|
|
339
|
+
const typeNode = field.childForFieldName('type');
|
|
340
|
+
|
|
341
|
+
if (nameNode) {
|
|
342
|
+
fields.push({
|
|
343
|
+
name: nameNode.text,
|
|
344
|
+
startLine,
|
|
345
|
+
endLine,
|
|
346
|
+
memberType: 'field',
|
|
347
|
+
...(typeNode && { fieldType: typeNode.text })
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return fields;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Extract impl block info
|
|
358
|
+
*/
|
|
359
|
+
function extractImplInfo(implNode) {
|
|
360
|
+
let traitName = null;
|
|
361
|
+
let typeName = null;
|
|
362
|
+
const typeParamsNode = implNode.childForFieldName('type_parameters');
|
|
363
|
+
const typeParams = typeParamsNode ? typeParamsNode.text.trim() : '';
|
|
364
|
+
|
|
365
|
+
const traitNode = implNode.childForFieldName('trait');
|
|
366
|
+
const typeNode = implNode.childForFieldName('type');
|
|
367
|
+
|
|
368
|
+
if (traitNode) {
|
|
369
|
+
traitName = traitNode.text;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
if (typeNode) {
|
|
373
|
+
typeName = typeNode.text;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
const prefix = typeParams ? `${typeParams} ` : '';
|
|
377
|
+
let name;
|
|
378
|
+
if (traitName && typeName) {
|
|
379
|
+
name = `${prefix}${traitName} for ${typeName}`;
|
|
380
|
+
} else if (typeName) {
|
|
381
|
+
name = `${prefix}${typeName}`;
|
|
382
|
+
} else {
|
|
383
|
+
const text = implNode.text;
|
|
384
|
+
const match = text.match(/impl\s*(?:<[^>]+>\s*)?(\w+(?:\s+for\s+\w+)?)/);
|
|
385
|
+
name = match ? `${prefix}${match[1]}` : 'impl';
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return { name, traitName, typeName };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Extract impl block members (functions)
|
|
393
|
+
*/
|
|
394
|
+
function extractImplMembers(implNode, code) {
|
|
395
|
+
const members = [];
|
|
396
|
+
const bodyNode = implNode.childForFieldName('body');
|
|
397
|
+
if (!bodyNode) return members;
|
|
398
|
+
|
|
399
|
+
for (let i = 0; i < bodyNode.namedChildCount; i++) {
|
|
400
|
+
const child = bodyNode.namedChild(i);
|
|
401
|
+
|
|
402
|
+
if (child.type === 'function_item') {
|
|
403
|
+
const nameNode = child.childForFieldName('name');
|
|
404
|
+
const paramsNode = child.childForFieldName('parameters');
|
|
405
|
+
|
|
406
|
+
if (nameNode) {
|
|
407
|
+
const { startLine, endLine } = nodeToLocation(child, code);
|
|
408
|
+
const text = child.text;
|
|
409
|
+
const firstLine = text.split('\n')[0];
|
|
410
|
+
const returnType = extractReturnType(child);
|
|
411
|
+
const docstring = extractRustDocstring(code, startLine);
|
|
412
|
+
const visibility = extractVisibility(text);
|
|
413
|
+
|
|
414
|
+
members.push({
|
|
415
|
+
name: nameNode.text,
|
|
416
|
+
params: extractRustParams(paramsNode),
|
|
417
|
+
paramsStructured: parseStructuredParams(paramsNode, 'rust'),
|
|
418
|
+
startLine,
|
|
419
|
+
endLine,
|
|
420
|
+
memberType: visibility ? 'public' : 'method',
|
|
421
|
+
isAsync: firstLine.includes('async '),
|
|
422
|
+
...(returnType && { returnType }),
|
|
423
|
+
...(docstring && { docstring })
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
return members;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Find state objects (const/static) in Rust code
|
|
434
|
+
*/
|
|
435
|
+
function findStateObjects(code, parser) {
|
|
436
|
+
const tree = parser.parse(code, undefined, PARSE_OPTIONS);
|
|
437
|
+
const objects = [];
|
|
438
|
+
|
|
439
|
+
const statePattern = /^([A-Z][A-Z0-9_]+|DEFAULT_[A-Z_]+)$/;
|
|
440
|
+
|
|
441
|
+
traverseTree(tree.rootNode, (node) => {
|
|
442
|
+
// Handle const items (only top-level)
|
|
443
|
+
if (node.type === 'const_item') {
|
|
444
|
+
if (!node.parent || node.parent.type !== 'source_file') return true;
|
|
445
|
+
const nameNode = node.childForFieldName('name');
|
|
446
|
+
if (nameNode) {
|
|
447
|
+
const name = nameNode.text;
|
|
448
|
+
if (statePattern.test(name)) {
|
|
449
|
+
const { startLine, endLine } = nodeToLocation(node, code);
|
|
450
|
+
objects.push({ name, startLine, endLine });
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return true;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Handle static items (only top-level)
|
|
457
|
+
if (node.type === 'static_item') {
|
|
458
|
+
if (!node.parent || node.parent.type !== 'source_file') return true;
|
|
459
|
+
const nameNode = node.childForFieldName('name');
|
|
460
|
+
if (nameNode) {
|
|
461
|
+
const name = nameNode.text;
|
|
462
|
+
if (statePattern.test(name)) {
|
|
463
|
+
const { startLine, endLine } = nodeToLocation(node, code);
|
|
464
|
+
objects.push({ name, startLine, endLine });
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
return true;
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
objects.sort((a, b) => a.startLine - b.startLine);
|
|
474
|
+
return objects;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* Parse a Rust file completely
|
|
479
|
+
*/
|
|
480
|
+
function parse(code, parser) {
|
|
481
|
+
return {
|
|
482
|
+
language: 'rust',
|
|
483
|
+
totalLines: code.split('\n').length,
|
|
484
|
+
functions: findFunctions(code, parser),
|
|
485
|
+
classes: findClasses(code, parser),
|
|
486
|
+
stateObjects: findStateObjects(code, parser),
|
|
487
|
+
imports: [],
|
|
488
|
+
exports: []
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
/**
|
|
493
|
+
* Find all function calls in Rust code using tree-sitter AST
|
|
494
|
+
* @param {string} code - Source code to analyze
|
|
495
|
+
* @param {object} parser - Tree-sitter parser instance
|
|
496
|
+
* @returns {Array<{name: string, line: number, isMethod: boolean, receiver?: string, isMacro?: boolean}>}
|
|
497
|
+
*/
|
|
498
|
+
function findCallsInCode(code, parser) {
|
|
499
|
+
const tree = parser.parse(code, undefined, PARSE_OPTIONS);
|
|
500
|
+
const calls = [];
|
|
501
|
+
const functionStack = []; // Stack of { name, startLine, endLine }
|
|
502
|
+
|
|
503
|
+
// Helper to check if a node creates a function scope
|
|
504
|
+
const isFunctionNode = (node) => {
|
|
505
|
+
return ['function_item', 'closure_expression'].includes(node.type);
|
|
506
|
+
};
|
|
507
|
+
|
|
508
|
+
// Helper to extract function name from a function node
|
|
509
|
+
const extractFunctionName = (node) => {
|
|
510
|
+
if (node.type === 'function_item') {
|
|
511
|
+
const nameNode = node.childForFieldName('name');
|
|
512
|
+
return nameNode?.text || '<anonymous>';
|
|
513
|
+
}
|
|
514
|
+
if (node.type === 'closure_expression') {
|
|
515
|
+
return '<closure>';
|
|
516
|
+
}
|
|
517
|
+
return '<anonymous>';
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
// Helper to get current enclosing function
|
|
521
|
+
const getCurrentEnclosingFunction = () => {
|
|
522
|
+
return functionStack.length > 0
|
|
523
|
+
? { ...functionStack[functionStack.length - 1] }
|
|
524
|
+
: null;
|
|
525
|
+
};
|
|
526
|
+
|
|
527
|
+
traverseTree(tree.rootNode, (node) => {
|
|
528
|
+
// Track function entry
|
|
529
|
+
if (isFunctionNode(node)) {
|
|
530
|
+
functionStack.push({
|
|
531
|
+
name: extractFunctionName(node),
|
|
532
|
+
startLine: node.startPosition.row + 1,
|
|
533
|
+
endLine: node.endPosition.row + 1
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// Handle function calls: foo(), obj.method(), Type::func()
|
|
538
|
+
if (node.type === 'call_expression') {
|
|
539
|
+
const funcNode = node.childForFieldName('function');
|
|
540
|
+
if (!funcNode) return true;
|
|
541
|
+
|
|
542
|
+
const enclosingFunction = getCurrentEnclosingFunction();
|
|
543
|
+
|
|
544
|
+
if (funcNode.type === 'identifier') {
|
|
545
|
+
// Direct call: foo()
|
|
546
|
+
calls.push({
|
|
547
|
+
name: funcNode.text,
|
|
548
|
+
line: node.startPosition.row + 1,
|
|
549
|
+
isMethod: false,
|
|
550
|
+
enclosingFunction
|
|
551
|
+
});
|
|
552
|
+
} else if (funcNode.type === 'field_expression') {
|
|
553
|
+
// Method call: obj.method()
|
|
554
|
+
const fieldNode = funcNode.childForFieldName('field');
|
|
555
|
+
const valueNode = funcNode.childForFieldName('value');
|
|
556
|
+
|
|
557
|
+
if (fieldNode) {
|
|
558
|
+
calls.push({
|
|
559
|
+
name: fieldNode.text,
|
|
560
|
+
line: node.startPosition.row + 1,
|
|
561
|
+
isMethod: true,
|
|
562
|
+
receiver: valueNode?.type === 'identifier' ? valueNode.text : undefined,
|
|
563
|
+
enclosingFunction
|
|
564
|
+
});
|
|
565
|
+
}
|
|
566
|
+
} else if (funcNode.type === 'scoped_identifier') {
|
|
567
|
+
// Path call: Type::func() or module::func()
|
|
568
|
+
// Get the last segment of the path
|
|
569
|
+
const pathText = funcNode.text;
|
|
570
|
+
const segments = pathText.split('::');
|
|
571
|
+
const name = segments[segments.length - 1];
|
|
572
|
+
calls.push({
|
|
573
|
+
name: name,
|
|
574
|
+
line: node.startPosition.row + 1,
|
|
575
|
+
isMethod: segments.length > 1,
|
|
576
|
+
receiver: segments.length > 1 ? segments.slice(0, -1).join('::') : undefined,
|
|
577
|
+
enclosingFunction
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
return true;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
// Handle macro invocations: println!(), vec![]
|
|
584
|
+
if (node.type === 'macro_invocation') {
|
|
585
|
+
const macroNode = node.childForFieldName('macro');
|
|
586
|
+
if (macroNode) {
|
|
587
|
+
let macroName = macroNode.text;
|
|
588
|
+
// Remove the trailing ! if present in the name
|
|
589
|
+
if (macroName.endsWith('!')) {
|
|
590
|
+
macroName = macroName.slice(0, -1);
|
|
591
|
+
}
|
|
592
|
+
const enclosingFunction = getCurrentEnclosingFunction();
|
|
593
|
+
calls.push({
|
|
594
|
+
name: macroName,
|
|
595
|
+
line: node.startPosition.row + 1,
|
|
596
|
+
isMethod: false,
|
|
597
|
+
isMacro: true,
|
|
598
|
+
enclosingFunction
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
return true;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
return true;
|
|
605
|
+
}, {
|
|
606
|
+
onLeave: (node) => {
|
|
607
|
+
if (isFunctionNode(node)) {
|
|
608
|
+
functionStack.pop();
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
|
|
613
|
+
return calls;
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Find all imports in Rust code using tree-sitter AST
|
|
618
|
+
* @param {string} code - Source code to analyze
|
|
619
|
+
* @param {object} parser - Tree-sitter parser instance
|
|
620
|
+
* @returns {Array<{module: string, names: string[], type: string, line: number}>}
|
|
621
|
+
*/
|
|
622
|
+
function findImportsInCode(code, parser) {
|
|
623
|
+
const tree = parser.parse(code, undefined, PARSE_OPTIONS);
|
|
624
|
+
const imports = [];
|
|
625
|
+
|
|
626
|
+
traverseTree(tree.rootNode, (node) => {
|
|
627
|
+
// use declarations
|
|
628
|
+
if (node.type === 'use_declaration') {
|
|
629
|
+
const line = node.startPosition.row + 1;
|
|
630
|
+
|
|
631
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
632
|
+
const child = node.namedChild(i);
|
|
633
|
+
|
|
634
|
+
if (child.type === 'scoped_identifier' || child.type === 'identifier') {
|
|
635
|
+
// use std::io or use foo
|
|
636
|
+
const path = child.text;
|
|
637
|
+
const segments = path.split('::');
|
|
638
|
+
imports.push({
|
|
639
|
+
module: path,
|
|
640
|
+
names: [segments[segments.length - 1]],
|
|
641
|
+
type: 'use',
|
|
642
|
+
line
|
|
643
|
+
});
|
|
644
|
+
} else if (child.type === 'use_wildcard') {
|
|
645
|
+
// use std::collections::*
|
|
646
|
+
const scopedId = child.namedChild(0);
|
|
647
|
+
if (scopedId) {
|
|
648
|
+
imports.push({
|
|
649
|
+
module: scopedId.text,
|
|
650
|
+
names: ['*'],
|
|
651
|
+
type: 'use-glob',
|
|
652
|
+
line
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
} else if (child.type === 'use_list' || child.type === 'scoped_use_list') {
|
|
656
|
+
// use std::{io, fs} or use foo::{bar, baz}
|
|
657
|
+
// Extract the base path and names
|
|
658
|
+
const pathNode = child.childForFieldName('path');
|
|
659
|
+
const listNode = child.childForFieldName('list');
|
|
660
|
+
|
|
661
|
+
if (pathNode && listNode) {
|
|
662
|
+
const basePath = pathNode.text;
|
|
663
|
+
const names = [];
|
|
664
|
+
for (let j = 0; j < listNode.namedChildCount; j++) {
|
|
665
|
+
const item = listNode.namedChild(j);
|
|
666
|
+
if (item.type === 'identifier') {
|
|
667
|
+
names.push(item.text);
|
|
668
|
+
} else if (item.type === 'use_as_clause') {
|
|
669
|
+
const nameNode = item.namedChild(0);
|
|
670
|
+
if (nameNode) names.push(nameNode.text);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
imports.push({
|
|
674
|
+
module: basePath,
|
|
675
|
+
names,
|
|
676
|
+
type: 'use',
|
|
677
|
+
line
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
return true;
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// mod declarations (external module imports)
|
|
686
|
+
if (node.type === 'mod_item') {
|
|
687
|
+
const line = node.startPosition.row + 1;
|
|
688
|
+
const nameNode = node.childForFieldName('name');
|
|
689
|
+
|
|
690
|
+
// Only count mod declarations without body (file-based modules)
|
|
691
|
+
const hasBody = node.namedChildren.some(c => c.type === 'declaration_list');
|
|
692
|
+
|
|
693
|
+
if (nameNode && !hasBody) {
|
|
694
|
+
imports.push({
|
|
695
|
+
module: nameNode.text,
|
|
696
|
+
names: [nameNode.text],
|
|
697
|
+
type: 'mod',
|
|
698
|
+
line
|
|
699
|
+
});
|
|
700
|
+
}
|
|
701
|
+
return true;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
return true;
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
return imports;
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
/**
|
|
711
|
+
* Find all exports in Rust code using tree-sitter AST
|
|
712
|
+
* In Rust, exports are pub items
|
|
713
|
+
* @param {string} code - Source code to analyze
|
|
714
|
+
* @param {object} parser - Tree-sitter parser instance
|
|
715
|
+
* @returns {Array<{name: string, type: string, line: number}>}
|
|
716
|
+
*/
|
|
717
|
+
function findExportsInCode(code, parser) {
|
|
718
|
+
const tree = parser.parse(code, undefined, PARSE_OPTIONS);
|
|
719
|
+
const exports = [];
|
|
720
|
+
|
|
721
|
+
function hasVisibility(node) {
|
|
722
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
723
|
+
const child = node.namedChild(i);
|
|
724
|
+
if (child.type === 'visibility_modifier') {
|
|
725
|
+
return true;
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
return false;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
traverseTree(tree.rootNode, (node) => {
|
|
732
|
+
// Public functions
|
|
733
|
+
if (node.type === 'function_item' && hasVisibility(node)) {
|
|
734
|
+
const nameNode = node.childForFieldName('name');
|
|
735
|
+
if (nameNode) {
|
|
736
|
+
exports.push({
|
|
737
|
+
name: nameNode.text,
|
|
738
|
+
type: 'function',
|
|
739
|
+
line: node.startPosition.row + 1
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
return true;
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Public structs
|
|
746
|
+
if (node.type === 'struct_item' && hasVisibility(node)) {
|
|
747
|
+
const nameNode = node.childForFieldName('name');
|
|
748
|
+
if (nameNode) {
|
|
749
|
+
exports.push({
|
|
750
|
+
name: nameNode.text,
|
|
751
|
+
type: 'struct',
|
|
752
|
+
line: node.startPosition.row + 1
|
|
753
|
+
});
|
|
754
|
+
}
|
|
755
|
+
return true;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
// Public enums
|
|
759
|
+
if (node.type === 'enum_item' && hasVisibility(node)) {
|
|
760
|
+
const nameNode = node.childForFieldName('name');
|
|
761
|
+
if (nameNode) {
|
|
762
|
+
exports.push({
|
|
763
|
+
name: nameNode.text,
|
|
764
|
+
type: 'enum',
|
|
765
|
+
line: node.startPosition.row + 1
|
|
766
|
+
});
|
|
767
|
+
}
|
|
768
|
+
return true;
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
// Public traits
|
|
772
|
+
if (node.type === 'trait_item' && hasVisibility(node)) {
|
|
773
|
+
const nameNode = node.childForFieldName('name');
|
|
774
|
+
if (nameNode) {
|
|
775
|
+
exports.push({
|
|
776
|
+
name: nameNode.text,
|
|
777
|
+
type: 'trait',
|
|
778
|
+
line: node.startPosition.row + 1
|
|
779
|
+
});
|
|
780
|
+
}
|
|
781
|
+
return true;
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Public modules
|
|
785
|
+
if (node.type === 'mod_item' && hasVisibility(node)) {
|
|
786
|
+
const nameNode = node.childForFieldName('name');
|
|
787
|
+
if (nameNode) {
|
|
788
|
+
exports.push({
|
|
789
|
+
name: nameNode.text,
|
|
790
|
+
type: 'module',
|
|
791
|
+
line: node.startPosition.row + 1
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
return true;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Public type aliases
|
|
798
|
+
if (node.type === 'type_item' && hasVisibility(node)) {
|
|
799
|
+
const nameNode = node.childForFieldName('name');
|
|
800
|
+
if (nameNode) {
|
|
801
|
+
exports.push({
|
|
802
|
+
name: nameNode.text,
|
|
803
|
+
type: 'type',
|
|
804
|
+
line: node.startPosition.row + 1
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
return true;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// Public const
|
|
811
|
+
if (node.type === 'const_item' && hasVisibility(node)) {
|
|
812
|
+
const nameNode = node.childForFieldName('name');
|
|
813
|
+
if (nameNode) {
|
|
814
|
+
exports.push({
|
|
815
|
+
name: nameNode.text,
|
|
816
|
+
type: 'const',
|
|
817
|
+
line: node.startPosition.row + 1
|
|
818
|
+
});
|
|
819
|
+
}
|
|
820
|
+
return true;
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
// Public static
|
|
824
|
+
if (node.type === 'static_item' && hasVisibility(node)) {
|
|
825
|
+
const nameNode = node.childForFieldName('name');
|
|
826
|
+
if (nameNode) {
|
|
827
|
+
exports.push({
|
|
828
|
+
name: nameNode.text,
|
|
829
|
+
type: 'static',
|
|
830
|
+
line: node.startPosition.row + 1
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
return true;
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
return true;
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
return exports;
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Find all usages of a name in code using AST
|
|
844
|
+
* @param {string} code - Source code
|
|
845
|
+
* @param {string} name - Symbol name to find
|
|
846
|
+
* @param {object} parser - Tree-sitter parser instance
|
|
847
|
+
* @returns {Array<{line: number, column: number, usageType: string}>}
|
|
848
|
+
*/
|
|
849
|
+
function findUsagesInCode(code, name, parser) {
|
|
850
|
+
const tree = parser.parse(code, undefined, PARSE_OPTIONS);
|
|
851
|
+
const usages = [];
|
|
852
|
+
|
|
853
|
+
traverseTree(tree.rootNode, (node) => {
|
|
854
|
+
// Only look for identifiers with the matching name
|
|
855
|
+
if (node.type !== 'identifier' || node.text !== name) {
|
|
856
|
+
return true;
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
const line = node.startPosition.row + 1;
|
|
860
|
+
const column = node.startPosition.column;
|
|
861
|
+
const parent = node.parent;
|
|
862
|
+
|
|
863
|
+
let usageType = 'reference';
|
|
864
|
+
|
|
865
|
+
if (parent) {
|
|
866
|
+
// Import: use path::name
|
|
867
|
+
if (parent.type === 'use_declaration' ||
|
|
868
|
+
parent.type === 'use_as_clause' ||
|
|
869
|
+
parent.type === 'scoped_identifier' && parent.parent?.type === 'use_declaration') {
|
|
870
|
+
usageType = 'import';
|
|
871
|
+
}
|
|
872
|
+
// Call: name()
|
|
873
|
+
else if (parent.type === 'call_expression' &&
|
|
874
|
+
parent.childForFieldName('function') === node) {
|
|
875
|
+
usageType = 'call';
|
|
876
|
+
}
|
|
877
|
+
// Macro invocation: name!
|
|
878
|
+
else if (parent.type === 'macro_invocation') {
|
|
879
|
+
const macroNode = parent.childForFieldName('macro');
|
|
880
|
+
if (macroNode === node) {
|
|
881
|
+
usageType = 'call';
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
// Definition: fn name
|
|
885
|
+
else if (parent.type === 'function_item' &&
|
|
886
|
+
parent.childForFieldName('name') === node) {
|
|
887
|
+
usageType = 'definition';
|
|
888
|
+
}
|
|
889
|
+
// Definition: struct name
|
|
890
|
+
else if (parent.type === 'struct_item' &&
|
|
891
|
+
parent.childForFieldName('name') === node) {
|
|
892
|
+
usageType = 'definition';
|
|
893
|
+
}
|
|
894
|
+
// Definition: enum name
|
|
895
|
+
else if (parent.type === 'enum_item' &&
|
|
896
|
+
parent.childForFieldName('name') === node) {
|
|
897
|
+
usageType = 'definition';
|
|
898
|
+
}
|
|
899
|
+
// Definition: impl for Type
|
|
900
|
+
else if (parent.type === 'impl_item') {
|
|
901
|
+
usageType = 'definition';
|
|
902
|
+
}
|
|
903
|
+
// Definition: type alias
|
|
904
|
+
else if (parent.type === 'type_item' &&
|
|
905
|
+
parent.childForFieldName('name') === node) {
|
|
906
|
+
usageType = 'definition';
|
|
907
|
+
}
|
|
908
|
+
// Definition: let binding
|
|
909
|
+
else if (parent.type === 'let_declaration' &&
|
|
910
|
+
parent.childForFieldName('pattern')?.text === name) {
|
|
911
|
+
usageType = 'definition';
|
|
912
|
+
}
|
|
913
|
+
// Definition: const/static
|
|
914
|
+
else if ((parent.type === 'const_item' || parent.type === 'static_item') &&
|
|
915
|
+
parent.childForFieldName('name') === node) {
|
|
916
|
+
usageType = 'definition';
|
|
917
|
+
}
|
|
918
|
+
// Definition: parameter
|
|
919
|
+
else if (parent.type === 'parameter') {
|
|
920
|
+
usageType = 'definition';
|
|
921
|
+
}
|
|
922
|
+
// Method call: obj.name()
|
|
923
|
+
else if (parent.type === 'field_expression' &&
|
|
924
|
+
parent.childForFieldName('field') === node) {
|
|
925
|
+
const grandparent = parent.parent;
|
|
926
|
+
if (grandparent && grandparent.type === 'call_expression') {
|
|
927
|
+
usageType = 'call';
|
|
928
|
+
} else {
|
|
929
|
+
usageType = 'reference';
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
usages.push({ line, column, usageType });
|
|
935
|
+
return true;
|
|
936
|
+
});
|
|
937
|
+
|
|
938
|
+
return usages;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
module.exports = {
|
|
942
|
+
findFunctions,
|
|
943
|
+
findClasses,
|
|
944
|
+
findStateObjects,
|
|
945
|
+
findCallsInCode,
|
|
946
|
+
findImportsInCode,
|
|
947
|
+
findExportsInCode,
|
|
948
|
+
findUsagesInCode,
|
|
949
|
+
parse
|
|
950
|
+
};
|