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,457 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* languages/utils.js - Shared tree-sitter AST utilities
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Traverse tree-sitter AST depth-first
|
|
7
|
+
* @param {object} node - Tree-sitter node
|
|
8
|
+
* @param {function} callback - Called with each node, return false to stop traversal of children
|
|
9
|
+
* @param {object} [options] - Optional traversal options
|
|
10
|
+
* @param {function} [options.onLeave] - Called when leaving each node (after children processed)
|
|
11
|
+
*/
|
|
12
|
+
function traverseTree(node, callback, options) {
|
|
13
|
+
if (callback(node) === false) return;
|
|
14
|
+
for (let i = 0; i < node.namedChildCount; i++) {
|
|
15
|
+
traverseTree(node.namedChild(i), callback, options);
|
|
16
|
+
}
|
|
17
|
+
if (options?.onLeave) {
|
|
18
|
+
options.onLeave(node);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Get line locations from tree-sitter node
|
|
24
|
+
* Returns 1-indexed lines to match UCN output format
|
|
25
|
+
* @param {object} node - Tree-sitter node
|
|
26
|
+
* @param {string} code - Original source code
|
|
27
|
+
* @returns {{ startLine: number, endLine: number, indent: number }}
|
|
28
|
+
*/
|
|
29
|
+
function nodeToLocation(node, code) {
|
|
30
|
+
const startLine = node.startPosition.row + 1; // tree-sitter is 0-indexed
|
|
31
|
+
const endLine = node.endPosition.row + 1;
|
|
32
|
+
|
|
33
|
+
// Calculate indent from start of line
|
|
34
|
+
const lines = code.split('\n');
|
|
35
|
+
const firstLine = lines[node.startPosition.row] || '';
|
|
36
|
+
const indentMatch = firstLine.match(/^(\s*)/);
|
|
37
|
+
const indent = indentMatch ? indentMatch[1].length : 0;
|
|
38
|
+
|
|
39
|
+
return { startLine, endLine, indent };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Extract parameter string from parameters node
|
|
44
|
+
* @param {object} paramsNode - Tree-sitter parameters node
|
|
45
|
+
* @returns {string}
|
|
46
|
+
*/
|
|
47
|
+
function extractParams(paramsNode) {
|
|
48
|
+
if (!paramsNode) return '...';
|
|
49
|
+
const text = paramsNode.text;
|
|
50
|
+
// Remove outer parens and trim
|
|
51
|
+
return text.replace(/^\(|\)$/g, '').trim() || '...';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Parse parameters into structured format
|
|
56
|
+
* @param {object} paramsNode - Tree-sitter parameters node
|
|
57
|
+
* @param {string} language - Language name
|
|
58
|
+
* @returns {Array<{name: string, type?: string, optional?: boolean, default?: string, rest?: boolean}>}
|
|
59
|
+
*/
|
|
60
|
+
function parseStructuredParams(paramsNode, language) {
|
|
61
|
+
if (!paramsNode) return [];
|
|
62
|
+
|
|
63
|
+
const params = [];
|
|
64
|
+
|
|
65
|
+
for (let i = 0; i < paramsNode.namedChildCount; i++) {
|
|
66
|
+
const param = paramsNode.namedChild(i);
|
|
67
|
+
const paramInfo = {};
|
|
68
|
+
|
|
69
|
+
// Different handling per language
|
|
70
|
+
if (language === 'javascript' || language === 'typescript' || language === 'tsx') {
|
|
71
|
+
parseJSParam(param, paramInfo);
|
|
72
|
+
} else if (language === 'python') {
|
|
73
|
+
parsePythonParam(param, paramInfo);
|
|
74
|
+
} else if (language === 'go') {
|
|
75
|
+
parseGoParam(param, paramInfo);
|
|
76
|
+
} else if (language === 'rust') {
|
|
77
|
+
parseRustParam(param, paramInfo);
|
|
78
|
+
} else if (language === 'java') {
|
|
79
|
+
parseJavaParam(param, paramInfo);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (paramInfo.name) {
|
|
83
|
+
params.push(paramInfo);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return params;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function parseJSParam(param, info) {
|
|
91
|
+
if (param.type === 'identifier') {
|
|
92
|
+
info.name = param.text;
|
|
93
|
+
} else if (param.type === 'required_parameter' || param.type === 'optional_parameter') {
|
|
94
|
+
const patternNode = param.childForFieldName('pattern');
|
|
95
|
+
const typeNode = param.childForFieldName('type');
|
|
96
|
+
if (patternNode) info.name = patternNode.text;
|
|
97
|
+
if (typeNode) info.type = typeNode.text.replace(/^:\s*/, '');
|
|
98
|
+
if (param.type === 'optional_parameter') info.optional = true;
|
|
99
|
+
} else if (param.type === 'rest_parameter') {
|
|
100
|
+
const patternNode = param.childForFieldName('pattern');
|
|
101
|
+
if (patternNode) info.name = patternNode.text;
|
|
102
|
+
info.rest = true;
|
|
103
|
+
} else if (param.type === 'assignment_pattern') {
|
|
104
|
+
const leftNode = param.childForFieldName('left');
|
|
105
|
+
const rightNode = param.childForFieldName('right');
|
|
106
|
+
if (leftNode) info.name = leftNode.text;
|
|
107
|
+
if (rightNode) info.default = rightNode.text;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function parsePythonParam(param, info) {
|
|
112
|
+
if (param.type === 'identifier') {
|
|
113
|
+
info.name = param.text;
|
|
114
|
+
} else if (param.type === 'typed_parameter') {
|
|
115
|
+
const nameNode = param.namedChild(0);
|
|
116
|
+
const typeNode = param.childForFieldName('type');
|
|
117
|
+
if (nameNode) info.name = nameNode.text;
|
|
118
|
+
if (typeNode) info.type = typeNode.text;
|
|
119
|
+
} else if (param.type === 'default_parameter' || param.type === 'typed_default_parameter') {
|
|
120
|
+
const nameNode = param.childForFieldName('name');
|
|
121
|
+
const valueNode = param.childForFieldName('value');
|
|
122
|
+
if (nameNode) info.name = nameNode.text;
|
|
123
|
+
if (valueNode) info.default = valueNode.text;
|
|
124
|
+
info.optional = true;
|
|
125
|
+
} else if (param.type === 'list_splat_pattern' || param.type === 'dictionary_splat_pattern') {
|
|
126
|
+
info.name = param.text;
|
|
127
|
+
info.rest = true;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function parseGoParam(param, info) {
|
|
132
|
+
if (param.type === 'parameter_declaration') {
|
|
133
|
+
const nameNode = param.childForFieldName('name');
|
|
134
|
+
const typeNode = param.childForFieldName('type');
|
|
135
|
+
if (nameNode) info.name = nameNode.text;
|
|
136
|
+
if (typeNode) info.type = typeNode.text;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function parseRustParam(param, info) {
|
|
141
|
+
if (param.type === 'parameter') {
|
|
142
|
+
const patternNode = param.childForFieldName('pattern');
|
|
143
|
+
const typeNode = param.childForFieldName('type');
|
|
144
|
+
if (patternNode) info.name = patternNode.text;
|
|
145
|
+
if (typeNode) info.type = typeNode.text;
|
|
146
|
+
} else if (param.type === 'self_parameter') {
|
|
147
|
+
info.name = param.text;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function parseJavaParam(param, info) {
|
|
152
|
+
if (param.type === 'formal_parameter' || param.type === 'spread_parameter') {
|
|
153
|
+
const nameNode = param.childForFieldName('name');
|
|
154
|
+
const typeNode = param.childForFieldName('type');
|
|
155
|
+
if (nameNode) info.name = nameNode.text;
|
|
156
|
+
if (typeNode) info.type = typeNode.text;
|
|
157
|
+
if (param.type === 'spread_parameter') info.rest = true;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Extract JSDoc docstring from JavaScript/TypeScript code
|
|
163
|
+
* Looks for /** ... *\/ comment block above the given line
|
|
164
|
+
* @param {string} code - Source code
|
|
165
|
+
* @param {number} startLine - 1-indexed line number of the function/class
|
|
166
|
+
* @returns {string|null} First line of docstring or null
|
|
167
|
+
*/
|
|
168
|
+
function extractJSDocstring(code, startLine) {
|
|
169
|
+
const lines = code.split('\n');
|
|
170
|
+
const lineIndex = startLine - 1;
|
|
171
|
+
if (lineIndex <= 0) return null;
|
|
172
|
+
|
|
173
|
+
// Scan upward, skipping empty lines and decorators
|
|
174
|
+
let i = lineIndex - 1;
|
|
175
|
+
while (i >= 0 && (lines[i].trim() === '' || lines[i].trim().startsWith('@'))) {
|
|
176
|
+
i--;
|
|
177
|
+
}
|
|
178
|
+
if (i < 0) return null;
|
|
179
|
+
|
|
180
|
+
// Check if this line ends with */ (end of JSDoc)
|
|
181
|
+
const trimmed = lines[i].trim();
|
|
182
|
+
if (trimmed.endsWith('*/')) {
|
|
183
|
+
// Find the start of the JSDoc block
|
|
184
|
+
let docEnd = i;
|
|
185
|
+
while (i >= 0 && !lines[i].includes('/**')) {
|
|
186
|
+
i--;
|
|
187
|
+
}
|
|
188
|
+
if (i < 0 || !lines[i].includes('/**')) return null;
|
|
189
|
+
|
|
190
|
+
// Extract the first meaningful line from the JSDoc
|
|
191
|
+
for (let j = i; j <= docEnd; j++) {
|
|
192
|
+
const line = lines[j]
|
|
193
|
+
.replace(/^\s*\/\*\*\s*/, '') // Remove /** at start
|
|
194
|
+
.replace(/\s*\*\/\s*$/, '') // Remove */ at end
|
|
195
|
+
.replace(/^\s*\*\s?/, '') // Remove leading *
|
|
196
|
+
.trim();
|
|
197
|
+
// Skip empty lines and @param/@returns tags
|
|
198
|
+
if (line && !line.startsWith('@')) {
|
|
199
|
+
return line;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return null;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Extract Python docstring from code
|
|
208
|
+
* Looks for """...""" or '''...''' as first statement after def/class
|
|
209
|
+
* @param {string} code - Source code
|
|
210
|
+
* @param {number} defLine - 1-indexed line number of the def/class (not decorator)
|
|
211
|
+
* @returns {string|null} First line of docstring or null
|
|
212
|
+
*/
|
|
213
|
+
function extractPythonDocstring(code, defLine) {
|
|
214
|
+
const lines = code.split('\n');
|
|
215
|
+
// Python docstring is INSIDE the function, on lines after the def:
|
|
216
|
+
let i = defLine; // Start after the def line (defLine is 1-indexed)
|
|
217
|
+
// Skip to find the first non-empty line inside the function
|
|
218
|
+
while (i < lines.length && lines[i].trim() === '') {
|
|
219
|
+
i++;
|
|
220
|
+
}
|
|
221
|
+
if (i >= lines.length) return null;
|
|
222
|
+
|
|
223
|
+
const trimmed = lines[i].trim();
|
|
224
|
+
// Check for triple-quoted string
|
|
225
|
+
if (trimmed.startsWith('"""') || trimmed.startsWith("'''")) {
|
|
226
|
+
const quote = trimmed.startsWith('"""') ? '"""' : "'''";
|
|
227
|
+
// Single-line docstring
|
|
228
|
+
if (trimmed.endsWith(quote) && trimmed.length > 6) {
|
|
229
|
+
return trimmed.slice(3, -3).trim();
|
|
230
|
+
}
|
|
231
|
+
// Multi-line docstring: return first line
|
|
232
|
+
const firstLine = trimmed.slice(3).trim();
|
|
233
|
+
if (firstLine) return firstLine;
|
|
234
|
+
// First line was just quotes, get next line
|
|
235
|
+
if (i + 1 < lines.length) {
|
|
236
|
+
return lines[i + 1].trim();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Extract Go documentation comment from code
|
|
244
|
+
* Looks for // comments directly above the func
|
|
245
|
+
* @param {string} code - Source code
|
|
246
|
+
* @param {number} startLine - 1-indexed line number of the function
|
|
247
|
+
* @returns {string|null} First line of doc comment or null
|
|
248
|
+
*/
|
|
249
|
+
function extractGoDocstring(code, startLine) {
|
|
250
|
+
const lines = code.split('\n');
|
|
251
|
+
const lineIndex = startLine - 1;
|
|
252
|
+
if (lineIndex <= 0) return null;
|
|
253
|
+
|
|
254
|
+
// Scan upward, skipping empty lines
|
|
255
|
+
let i = lineIndex - 1;
|
|
256
|
+
while (i >= 0 && lines[i].trim() === '') {
|
|
257
|
+
i--;
|
|
258
|
+
}
|
|
259
|
+
if (i < 0) return null;
|
|
260
|
+
|
|
261
|
+
const trimmed = lines[i].trim();
|
|
262
|
+
if (trimmed.startsWith('//')) {
|
|
263
|
+
// Find the start of the comment block
|
|
264
|
+
let commentStart = i;
|
|
265
|
+
while (commentStart > 0 && lines[commentStart - 1].trim().startsWith('//')) {
|
|
266
|
+
commentStart--;
|
|
267
|
+
}
|
|
268
|
+
// Return first line of comment block
|
|
269
|
+
const firstLine = lines[commentStart].trim().replace(/^\/\/\s?/, '');
|
|
270
|
+
if (firstLine) return firstLine;
|
|
271
|
+
}
|
|
272
|
+
return null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Extract Rust documentation comment from code
|
|
277
|
+
* Looks for /// or //! comments directly above the item
|
|
278
|
+
* @param {string} code - Source code
|
|
279
|
+
* @param {number} startLine - 1-indexed line number of the item
|
|
280
|
+
* @returns {string|null} First line of doc comment or null
|
|
281
|
+
*/
|
|
282
|
+
function extractRustDocstring(code, startLine) {
|
|
283
|
+
const lines = code.split('\n');
|
|
284
|
+
const lineIndex = startLine - 1;
|
|
285
|
+
if (lineIndex <= 0) return null;
|
|
286
|
+
|
|
287
|
+
// Scan upward, skipping empty lines and attributes (#[...])
|
|
288
|
+
let i = lineIndex - 1;
|
|
289
|
+
while (i >= 0 && (lines[i].trim() === '' || lines[i].trim().startsWith('#['))) {
|
|
290
|
+
i--;
|
|
291
|
+
}
|
|
292
|
+
if (i < 0) return null;
|
|
293
|
+
|
|
294
|
+
const trimmed = lines[i].trim();
|
|
295
|
+
if (trimmed.startsWith('///') || trimmed.startsWith('//!')) {
|
|
296
|
+
// Find the start of the doc comment block
|
|
297
|
+
let commentStart = i;
|
|
298
|
+
while (commentStart > 0 &&
|
|
299
|
+
(lines[commentStart - 1].trim().startsWith('///') ||
|
|
300
|
+
lines[commentStart - 1].trim().startsWith('//!'))) {
|
|
301
|
+
commentStart--;
|
|
302
|
+
}
|
|
303
|
+
// Return first line of comment block
|
|
304
|
+
const firstLine = lines[commentStart].trim().replace(/^\/\/[\/!]\s?/, '');
|
|
305
|
+
if (firstLine) return firstLine;
|
|
306
|
+
}
|
|
307
|
+
return null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Extract Java Javadoc comment from code
|
|
312
|
+
* Looks for /** ... *\/ comment block above the method/class
|
|
313
|
+
* @param {string} code - Source code
|
|
314
|
+
* @param {number} startLine - 1-indexed line number of the method/class
|
|
315
|
+
* @returns {string|null} First line of docstring or null
|
|
316
|
+
*/
|
|
317
|
+
function extractJavaDocstring(code, startLine) {
|
|
318
|
+
// Java uses same format as JS - /** ... */
|
|
319
|
+
return extractJSDocstring(code, startLine);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Get the token type at a specific position using AST
|
|
324
|
+
* @param {object} rootNode - Tree-sitter root node
|
|
325
|
+
* @param {number} line - 1-indexed line number
|
|
326
|
+
* @param {number} column - 0-indexed column number
|
|
327
|
+
* @returns {string} Token type: 'comment', 'string', or 'code'
|
|
328
|
+
*/
|
|
329
|
+
function getTokenTypeAtPosition(rootNode, line, column) {
|
|
330
|
+
// Convert to 0-indexed row for tree-sitter
|
|
331
|
+
const row = line - 1;
|
|
332
|
+
|
|
333
|
+
// Find the smallest node at this position
|
|
334
|
+
const node = rootNode.descendantForPosition({ row, column });
|
|
335
|
+
if (!node) return 'code';
|
|
336
|
+
|
|
337
|
+
// Walk up the tree to check if we're in a comment or string
|
|
338
|
+
let current = node;
|
|
339
|
+
while (current) {
|
|
340
|
+
const type = current.type;
|
|
341
|
+
|
|
342
|
+
// Comment types across languages
|
|
343
|
+
if (type === 'comment' ||
|
|
344
|
+
type === 'line_comment' ||
|
|
345
|
+
type === 'block_comment' ||
|
|
346
|
+
type === 'doc_comment' ||
|
|
347
|
+
type === 'documentation_comment') {
|
|
348
|
+
return 'comment';
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// String types across languages
|
|
352
|
+
if (type === 'string' ||
|
|
353
|
+
type === 'string_literal' ||
|
|
354
|
+
type === 'template_string' ||
|
|
355
|
+
type === 'template_literal' ||
|
|
356
|
+
type === 'raw_string_literal' ||
|
|
357
|
+
type === 'interpreted_string_literal' ||
|
|
358
|
+
type === 'concatenated_string') {
|
|
359
|
+
return 'string';
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// For template strings, check if we're in the literal part vs expression
|
|
363
|
+
if (type === 'template_substitution' || type === 'interpolation') {
|
|
364
|
+
// Inside ${...}, this is code
|
|
365
|
+
return 'code';
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
current = current.parent;
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
return 'code';
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Check if a match at a specific position is inside a comment or string
|
|
376
|
+
* @param {object} rootNode - Tree-sitter root node
|
|
377
|
+
* @param {number} line - 1-indexed line number
|
|
378
|
+
* @param {string} lineContent - The line content
|
|
379
|
+
* @param {string} term - The search term
|
|
380
|
+
* @returns {boolean} True if the match is in a comment or string
|
|
381
|
+
*/
|
|
382
|
+
function isMatchInCommentOrString(rootNode, line, lineContent, term) {
|
|
383
|
+
// Find where the term appears in the line
|
|
384
|
+
const termLower = term.toLowerCase();
|
|
385
|
+
const lineLower = lineContent.toLowerCase();
|
|
386
|
+
const index = lineLower.indexOf(termLower);
|
|
387
|
+
|
|
388
|
+
if (index === -1) return false;
|
|
389
|
+
|
|
390
|
+
// Check the token type at the match position
|
|
391
|
+
const tokenType = getTokenTypeAtPosition(rootNode, line, index);
|
|
392
|
+
return tokenType === 'comment' || tokenType === 'string';
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Find all matches of a term in a file, filtering by token type
|
|
397
|
+
* @param {string} content - File content
|
|
398
|
+
* @param {string} term - Search term
|
|
399
|
+
* @param {object} parser - Tree-sitter parser
|
|
400
|
+
* @param {object} options - { codeOnly: boolean }
|
|
401
|
+
* @returns {Array<{line: number, content: string, column: number}>}
|
|
402
|
+
*/
|
|
403
|
+
function findMatchesWithASTFilter(content, term, parser, options = {}) {
|
|
404
|
+
const { PARSE_OPTIONS } = require('./index');
|
|
405
|
+
const tree = parser.parse(content, undefined, PARSE_OPTIONS);
|
|
406
|
+
const lines = content.split('\n');
|
|
407
|
+
const matches = [];
|
|
408
|
+
|
|
409
|
+
// Escape special regex characters and create pattern
|
|
410
|
+
const escapedTerm = term.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
411
|
+
const regex = new RegExp(escapedTerm, 'gi');
|
|
412
|
+
|
|
413
|
+
lines.forEach((line, idx) => {
|
|
414
|
+
const lineNum = idx + 1;
|
|
415
|
+
let match;
|
|
416
|
+
|
|
417
|
+
// Reset regex for each line
|
|
418
|
+
regex.lastIndex = 0;
|
|
419
|
+
|
|
420
|
+
while ((match = regex.exec(line)) !== null) {
|
|
421
|
+
const column = match.index;
|
|
422
|
+
|
|
423
|
+
if (options.codeOnly) {
|
|
424
|
+
const tokenType = getTokenTypeAtPosition(tree.rootNode, lineNum, column);
|
|
425
|
+
if (tokenType !== 'code') {
|
|
426
|
+
continue; // Skip comments and strings
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Check if we already added this line (avoid duplicates)
|
|
431
|
+
if (!matches.some(m => m.line === lineNum)) {
|
|
432
|
+
matches.push({
|
|
433
|
+
line: lineNum,
|
|
434
|
+
content: line,
|
|
435
|
+
column
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
});
|
|
440
|
+
|
|
441
|
+
return matches;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
module.exports = {
|
|
445
|
+
traverseTree,
|
|
446
|
+
nodeToLocation,
|
|
447
|
+
extractParams,
|
|
448
|
+
parseStructuredParams,
|
|
449
|
+
extractJSDocstring,
|
|
450
|
+
extractPythonDocstring,
|
|
451
|
+
extractGoDocstring,
|
|
452
|
+
extractRustDocstring,
|
|
453
|
+
extractJavaDocstring,
|
|
454
|
+
getTokenTypeAtPosition,
|
|
455
|
+
isMatchInCommentOrString,
|
|
456
|
+
findMatchesWithASTFilter
|
|
457
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ucn",
|
|
3
|
+
"version": "3.0.0",
|
|
4
|
+
"description": "Code navigation built by AI, for AI. Reduces context usage by 90%+ when working with large codebases.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"ucn": "./cli/index.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test test/parser.test.js"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"code-navigation",
|
|
14
|
+
"ast",
|
|
15
|
+
"parser",
|
|
16
|
+
"tree-sitter",
|
|
17
|
+
"javascript",
|
|
18
|
+
"typescript",
|
|
19
|
+
"python",
|
|
20
|
+
"go",
|
|
21
|
+
"rust",
|
|
22
|
+
"java",
|
|
23
|
+
"ai",
|
|
24
|
+
"agent"
|
|
25
|
+
],
|
|
26
|
+
"author": "Constantin-Mihail Leoca (https://github.com/mleoca)",
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "git+https://github.com/mleoca/ucn.git"
|
|
30
|
+
},
|
|
31
|
+
"homepage": "https://github.com/mleoca/ucn#readme",
|
|
32
|
+
"license": "MIT",
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"tree-sitter": "^0.21.0",
|
|
35
|
+
"tree-sitter-go": "^0.21.0",
|
|
36
|
+
"tree-sitter-java": "^0.21.0",
|
|
37
|
+
"tree-sitter-javascript": "^0.21.0",
|
|
38
|
+
"tree-sitter-python": "^0.21.0",
|
|
39
|
+
"tree-sitter-rust": "^0.21.0",
|
|
40
|
+
"tree-sitter-typescript": "^0.21.0"
|
|
41
|
+
}
|
|
42
|
+
}
|