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/core/shared.js CHANGED
@@ -40,4 +40,14 @@ function addTestExclusions(exclude) {
40
40
  return [...(exclude || []), ...additions];
41
41
  }
42
42
 
43
- module.exports = { pickBestDefinition, addTestExclusions };
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 };