ucn 3.7.24 → 3.7.26
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 +192 -463
- package/cli/index.js +285 -1054
- package/core/cache.js +193 -0
- package/core/callers.js +817 -0
- package/core/deadcode.js +320 -0
- package/core/discovery.js +1 -1
- package/core/execute.js +207 -10
- package/core/expand-cache.js +16 -5
- package/core/imports.js +21 -15
- package/core/output.js +370 -35
- package/core/project.js +365 -2272
- package/core/shared.js +11 -1
- package/core/stacktrace.js +313 -0
- package/core/verify.js +533 -0
- package/languages/go.js +57 -21
- package/languages/html.js +14 -3
- package/languages/java.js +4 -2
- package/languages/javascript.js +36 -9
- package/languages/rust.js +49 -17
- package/mcp/server.js +39 -172
- package/package.json +1 -1
package/core/shared.js
CHANGED
|
@@ -40,4 +40,14 @@ function addTestExclusions(exclude) {
|
|
|
40
40
|
return [...(exclude || []), ...additions];
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* Escape special regex characters
|
|
45
|
+
*/
|
|
46
|
+
function escapeRegExp(text) {
|
|
47
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Symbol types that are not callable (used to filter class/struct/type declarations from call analysis)
|
|
51
|
+
const NON_CALLABLE_TYPES = new Set(['class', 'struct', 'interface', 'type', 'enum', 'trait', 'state', 'impl']);
|
|
52
|
+
|
|
53
|
+
module.exports = { pickBestDefinition, addTestExclusions, escapeRegExp, NON_CALLABLE_TYPES };
|
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/stacktrace.js - Stack trace parsing and file matching
|
|
3
|
+
*
|
|
4
|
+
* Extracted from project.js. All functions take an `index` (ProjectIndex)
|
|
5
|
+
* as the first argument instead of using `this`.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Calculate path similarity score between two file paths
|
|
13
|
+
* Higher score = better match
|
|
14
|
+
* @param {string} query - The path from stack trace
|
|
15
|
+
* @param {string} candidate - The candidate file path
|
|
16
|
+
* @returns {number} Similarity score
|
|
17
|
+
*/
|
|
18
|
+
function calculatePathSimilarity(query, candidate) {
|
|
19
|
+
// Normalize paths for comparison
|
|
20
|
+
const queryParts = query.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
21
|
+
const candidateParts = candidate.replace(/\\/g, '/').split('/').filter(Boolean);
|
|
22
|
+
|
|
23
|
+
let score = 0;
|
|
24
|
+
|
|
25
|
+
// Exact match on full path
|
|
26
|
+
if (candidate.endsWith(query)) {
|
|
27
|
+
score += 100;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Compare from the end (most important part)
|
|
31
|
+
let matches = 0;
|
|
32
|
+
const minLen = Math.min(queryParts.length, candidateParts.length);
|
|
33
|
+
for (let i = 0; i < minLen; i++) {
|
|
34
|
+
const queryPart = queryParts[queryParts.length - 1 - i];
|
|
35
|
+
const candPart = candidateParts[candidateParts.length - 1 - i];
|
|
36
|
+
if (queryPart === candPart) {
|
|
37
|
+
matches++;
|
|
38
|
+
// Earlier parts (closer to filename) score more
|
|
39
|
+
score += (10 - i) * 5;
|
|
40
|
+
} else {
|
|
41
|
+
break; // Stop at first mismatch
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Bonus for matching most of the query path
|
|
46
|
+
if (matches === queryParts.length) {
|
|
47
|
+
score += 50;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Filename match is essential
|
|
51
|
+
const queryFile = queryParts[queryParts.length - 1];
|
|
52
|
+
const candFile = candidateParts[candidateParts.length - 1];
|
|
53
|
+
if (queryFile !== candFile) {
|
|
54
|
+
score = 0; // No match if filename doesn't match
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return score;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Find the best matching file for a stack trace path
|
|
62
|
+
* @param {object} index - ProjectIndex instance
|
|
63
|
+
* @param {string} filePath - Path from stack trace
|
|
64
|
+
* @param {string|null} funcName - Function name for verification
|
|
65
|
+
* @param {number} lineNum - Line number for verification
|
|
66
|
+
* @returns {{path: string, relativePath: string, confidence: number}|null}
|
|
67
|
+
*/
|
|
68
|
+
function findBestMatchingFile(index, filePath, funcName, lineNum) {
|
|
69
|
+
const candidates = [];
|
|
70
|
+
|
|
71
|
+
// Collect all potential matches with scores
|
|
72
|
+
for (const [absPath, fileEntry] of index.files) {
|
|
73
|
+
const score = calculatePathSimilarity(filePath, absPath);
|
|
74
|
+
const relScore = calculatePathSimilarity(filePath, fileEntry.relativePath);
|
|
75
|
+
const bestScore = Math.max(score, relScore);
|
|
76
|
+
|
|
77
|
+
if (bestScore > 0) {
|
|
78
|
+
candidates.push({
|
|
79
|
+
absPath,
|
|
80
|
+
relativePath: fileEntry.relativePath,
|
|
81
|
+
score: bestScore,
|
|
82
|
+
fileEntry
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (candidates.length === 0) {
|
|
88
|
+
// Try absolute path
|
|
89
|
+
const absPath = path.isAbsolute(filePath) ? filePath : path.join(index.root, filePath);
|
|
90
|
+
if (fs.existsSync(absPath)) {
|
|
91
|
+
return {
|
|
92
|
+
path: absPath,
|
|
93
|
+
relativePath: path.relative(index.root, absPath),
|
|
94
|
+
confidence: 0.5 // Low confidence for unindexed files
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// Sort by score descending
|
|
101
|
+
candidates.sort((a, b) => b.score - a.score);
|
|
102
|
+
|
|
103
|
+
// If there's a function name, verify it exists at the line
|
|
104
|
+
if (funcName && candidates.length > 1) {
|
|
105
|
+
for (const cand of candidates) {
|
|
106
|
+
const symbols = index.symbols.get(funcName);
|
|
107
|
+
if (symbols) {
|
|
108
|
+
const match = symbols.find(s =>
|
|
109
|
+
s.file === cand.absPath &&
|
|
110
|
+
s.startLine <= lineNum && s.endLine >= lineNum
|
|
111
|
+
);
|
|
112
|
+
if (match) {
|
|
113
|
+
// This candidate has the function at the right line - strong match
|
|
114
|
+
return {
|
|
115
|
+
path: cand.absPath,
|
|
116
|
+
relativePath: cand.relativePath,
|
|
117
|
+
confidence: 1.0,
|
|
118
|
+
verifiedFunction: true
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Return best scoring candidate
|
|
126
|
+
const best = candidates[0];
|
|
127
|
+
const confidence = candidates.length === 1 ? 0.9 :
|
|
128
|
+
(best.score > 100 ? 0.8 : 0.6);
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
path: best.absPath,
|
|
132
|
+
relativePath: best.relativePath,
|
|
133
|
+
confidence
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Create a stack frame with code context
|
|
139
|
+
* @param {object} index - ProjectIndex instance
|
|
140
|
+
* @param {string} filePath - File path from stack trace
|
|
141
|
+
* @param {number} lineNum - Line number
|
|
142
|
+
* @param {string|null} funcName - Function name
|
|
143
|
+
* @param {number|null} col - Column number
|
|
144
|
+
* @param {string} rawLine - Raw stack trace line
|
|
145
|
+
* @returns {object} Stack frame with code context
|
|
146
|
+
*/
|
|
147
|
+
function createStackFrame(index, filePath, lineNum, funcName, col, rawLine) {
|
|
148
|
+
const frame = {
|
|
149
|
+
file: filePath,
|
|
150
|
+
line: lineNum,
|
|
151
|
+
function: funcName,
|
|
152
|
+
column: col,
|
|
153
|
+
raw: rawLine,
|
|
154
|
+
found: false,
|
|
155
|
+
code: null,
|
|
156
|
+
context: null,
|
|
157
|
+
confidence: 0
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Find the best matching file using improved algorithm
|
|
161
|
+
const match = findBestMatchingFile(index, filePath, funcName, lineNum);
|
|
162
|
+
|
|
163
|
+
if (match) {
|
|
164
|
+
const resolvedPath = match.path;
|
|
165
|
+
frame.found = true;
|
|
166
|
+
frame.resolvedFile = match.relativePath;
|
|
167
|
+
frame.confidence = match.confidence;
|
|
168
|
+
if (match.verifiedFunction) {
|
|
169
|
+
frame.verifiedFunction = true;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
const content = index._readFile(resolvedPath);
|
|
174
|
+
const lines = content.split('\n');
|
|
175
|
+
|
|
176
|
+
// Get the exact line
|
|
177
|
+
if (lineNum > 0 && lineNum <= lines.length) {
|
|
178
|
+
frame.code = lines[lineNum - 1];
|
|
179
|
+
|
|
180
|
+
// Get context (2 lines before, 2 after)
|
|
181
|
+
const contextLines = [];
|
|
182
|
+
for (let i = Math.max(0, lineNum - 3); i < Math.min(lines.length, lineNum + 2); i++) {
|
|
183
|
+
contextLines.push({
|
|
184
|
+
line: i + 1,
|
|
185
|
+
code: lines[i],
|
|
186
|
+
isCurrent: i + 1 === lineNum
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
frame.context = contextLines;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// Try to find function info (verify it contains the line)
|
|
193
|
+
if (funcName) {
|
|
194
|
+
const symbols = index.symbols.get(funcName);
|
|
195
|
+
if (symbols) {
|
|
196
|
+
const funcMatch = symbols.find(s =>
|
|
197
|
+
s.file === resolvedPath &&
|
|
198
|
+
s.startLine <= lineNum && s.endLine >= lineNum
|
|
199
|
+
);
|
|
200
|
+
if (funcMatch) {
|
|
201
|
+
frame.functionInfo = {
|
|
202
|
+
name: funcMatch.name,
|
|
203
|
+
startLine: funcMatch.startLine,
|
|
204
|
+
endLine: funcMatch.endLine,
|
|
205
|
+
params: funcMatch.params
|
|
206
|
+
};
|
|
207
|
+
frame.confidence = 1.0; // High confidence when function verified
|
|
208
|
+
} else {
|
|
209
|
+
// Function exists but line doesn't match - lower confidence
|
|
210
|
+
const anyMatch = symbols.find(s => s.file === resolvedPath);
|
|
211
|
+
if (anyMatch) {
|
|
212
|
+
frame.functionInfo = {
|
|
213
|
+
name: anyMatch.name,
|
|
214
|
+
startLine: anyMatch.startLine,
|
|
215
|
+
endLine: anyMatch.endLine,
|
|
216
|
+
params: anyMatch.params,
|
|
217
|
+
lineMismatch: true
|
|
218
|
+
};
|
|
219
|
+
frame.confidence = Math.min(frame.confidence, 0.5);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
} else {
|
|
224
|
+
// No function name in stack - find enclosing function
|
|
225
|
+
const enclosing = index.findEnclosingFunction(resolvedPath, lineNum, true);
|
|
226
|
+
if (enclosing) {
|
|
227
|
+
frame.functionInfo = {
|
|
228
|
+
name: enclosing.name,
|
|
229
|
+
startLine: enclosing.startLine,
|
|
230
|
+
endLine: enclosing.endLine,
|
|
231
|
+
params: enclosing.params,
|
|
232
|
+
inferred: true
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
} catch (e) {
|
|
237
|
+
frame.error = e.message;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return frame;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Parse a stack trace and show code for each frame
|
|
246
|
+
* @param {object} index - ProjectIndex instance
|
|
247
|
+
* @param {string} stackText - Stack trace text
|
|
248
|
+
* @returns {object} Parsed frames with code context
|
|
249
|
+
*/
|
|
250
|
+
function parseStackTrace(index, stackText) {
|
|
251
|
+
const frames = [];
|
|
252
|
+
const lines = stackText.split(/\\n|\n/);
|
|
253
|
+
|
|
254
|
+
// Stack trace patterns for different languages/runtimes
|
|
255
|
+
// Order matters - more specific patterns first
|
|
256
|
+
const patterns = [
|
|
257
|
+
// JavaScript Node.js: "at functionName (file.js:line:col)" or "at file.js:line:col"
|
|
258
|
+
{ regex: /at\s+(?:async\s+)?(?:(.+?)\s+\()?([^():]+):(\d+)(?::(\d+))?\)?/, extract: (m) => ({ funcName: m[1] || null, file: m[2], line: parseInt(m[3]), col: m[4] ? parseInt(m[4]) : null }) },
|
|
259
|
+
// Deno: "at functionName (file:///path/to/file.ts:line:col)"
|
|
260
|
+
{ regex: /at\s+(?:async\s+)?(?:(.+?)\s+\()?file:\/\/([^:]+):(\d+)(?::(\d+))?\)?/, extract: (m) => ({ funcName: m[1] || null, file: m[2], line: parseInt(m[3]), col: m[4] ? parseInt(m[4]) : null }) },
|
|
261
|
+
// Bun: "at functionName (file.js:line:col)" - similar to Node but may have different formatting
|
|
262
|
+
{ regex: /^\s+at\s+(.+?)\s+\[as\s+\w+\]\s+\(([^:]+):(\d+):(\d+)\)/, extract: (m) => ({ funcName: m[1], file: m[2], line: parseInt(m[3]), col: parseInt(m[4]) }) },
|
|
263
|
+
// Browser Chrome/V8: "at functionName (http://... or file:// ...)"
|
|
264
|
+
{ regex: /at\s+(?:async\s+)?(?:(.+?)\s+\()?(?:https?:\/\/[^/]+)?([^():]+):(\d+)(?::(\d+))?\)?/, extract: (m) => ({ funcName: m[1] || null, file: m[2], line: parseInt(m[3]), col: m[4] ? parseInt(m[4]) : null }) },
|
|
265
|
+
// Firefox: "functionName@file:line:col"
|
|
266
|
+
{ regex: /^(.+)@(.+):(\d+):(\d+)$/, extract: (m) => ({ funcName: m[1] || null, file: m[2], line: parseInt(m[3]), col: parseInt(m[4]) }) },
|
|
267
|
+
// Safari: "functionName@file:line:col" (similar to Firefox)
|
|
268
|
+
{ regex: /^(.+)@(?:https?:\/\/[^/]+)?([^:]+):(\d+)(?::(\d+))?$/, extract: (m) => ({ funcName: m[1] || null, file: m[2], line: parseInt(m[3]), col: m[4] ? parseInt(m[4]) : null }) },
|
|
269
|
+
// Python: "File \"file.py\", line N, in function"
|
|
270
|
+
{ regex: /File\s+"([^"]+)",\s+line\s+(\d+)(?:,\s+in\s+(.+))?/, extract: (m) => ({ file: m[1], line: parseInt(m[2]), funcName: m[3] || null, col: null }) },
|
|
271
|
+
// Go: "file.go:line" or "package/file.go:line +0x..."
|
|
272
|
+
{ regex: /^\s*([^\s:]+\.go):(\d+)(?:\s|$)/, extract: (m) => ({ file: m[1], line: parseInt(m[2]), funcName: null, col: null }) },
|
|
273
|
+
// Go with function: "package.FunctionName()\n\tfile.go:line"
|
|
274
|
+
{ regex: /^\s*([^\s(]+)\(\)$/, extract: null }, // Skip function-only lines
|
|
275
|
+
// Java: "at package.Class.method(File.java:line)"
|
|
276
|
+
{ regex: /at\s+([^\(]+)\(([^:]+):(\d+)\)/, extract: (m) => ({ funcName: m[1].split('.').pop(), file: m[2], line: parseInt(m[3]), col: null }) },
|
|
277
|
+
// Rust: "at src/main.rs:line:col" or panic location
|
|
278
|
+
{ regex: /(?:at\s+)?([^\s:]+\.rs):(\d+)(?::(\d+))?/, extract: (m) => ({ file: m[1], line: parseInt(m[2]), col: m[3] ? parseInt(m[3]) : null, funcName: null }) },
|
|
279
|
+
// Generic: "file:line" as last resort
|
|
280
|
+
{ regex: /([^\s:]+\.\w+):(\d+)(?::(\d+))?/, extract: (m) => ({ file: m[1], line: parseInt(m[2]), col: m[3] ? parseInt(m[3]) : null, funcName: null }) }
|
|
281
|
+
];
|
|
282
|
+
|
|
283
|
+
for (const line of lines) {
|
|
284
|
+
const trimmed = line.trim();
|
|
285
|
+
if (!trimmed) continue;
|
|
286
|
+
|
|
287
|
+
// Try each pattern until one matches
|
|
288
|
+
for (const pattern of patterns) {
|
|
289
|
+
const match = pattern.regex.exec(trimmed);
|
|
290
|
+
if (match && pattern.extract) {
|
|
291
|
+
const extracted = pattern.extract(match);
|
|
292
|
+
if (extracted && extracted.file && extracted.line) {
|
|
293
|
+
frames.push(createStackFrame(
|
|
294
|
+
index,
|
|
295
|
+
extracted.file,
|
|
296
|
+
extracted.line,
|
|
297
|
+
extracted.funcName,
|
|
298
|
+
extracted.col,
|
|
299
|
+
trimmed
|
|
300
|
+
));
|
|
301
|
+
break; // Move to next line
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return {
|
|
308
|
+
frameCount: frames.length,
|
|
309
|
+
frames
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
module.exports = { parseStackTrace, findBestMatchingFile, createStackFrame, calculatePathSimilarity };
|