ucn 3.8.22 → 3.8.25
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/.claude/skills/ucn/SKILL.md +114 -11
- package/README.md +152 -156
- package/cli/index.js +363 -37
- package/core/analysis.js +960 -37
- package/core/bridge.js +1111 -0
- package/core/brief.js +408 -0
- package/core/cache.js +213 -59
- package/core/callers.js +117 -41
- package/core/check.js +200 -0
- package/core/deadcode.js +31 -2
- package/core/discovery.js +57 -34
- package/core/entrypoints.js +638 -4
- package/core/execute.js +304 -5
- package/core/git-enrich.js +130 -0
- package/core/graph-build.js +4 -4
- package/core/graph.js +31 -12
- package/core/output/analysis.js +157 -25
- package/core/output/brief.js +100 -0
- package/core/output/check.js +79 -0
- package/core/output/doctor.js +85 -0
- package/core/output/endpoints.js +239 -0
- package/core/output/extraction.js +2 -0
- package/core/output/find.js +126 -39
- package/core/output/graph.js +48 -15
- package/core/output/refactoring.js +103 -5
- package/core/output/reporting.js +63 -23
- package/core/output/search.js +110 -17
- package/core/output/shared.js +56 -2
- package/core/output.js +4 -0
- package/core/parallel-build.js +10 -7
- package/core/parser.js +8 -2
- package/core/project.js +147 -41
- package/core/registry.js +30 -14
- package/core/reporting.js +465 -2
- package/core/search.js +139 -15
- package/core/shared.js +101 -5
- package/core/tracing.js +31 -12
- package/core/verify.js +982 -95
- package/languages/go.js +91 -6
- package/languages/html.js +10 -0
- package/languages/java.js +151 -35
- package/languages/javascript.js +290 -33
- package/languages/python.js +78 -11
- package/languages/rust.js +267 -12
- package/languages/utils.js +315 -3
- package/mcp/server.js +91 -16
- package/package.json +10 -2
package/core/brief.js
ADDED
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* core/brief.js — Brief: AST-only one-screen summary for a symbol.
|
|
3
|
+
*
|
|
4
|
+
* Returns a compact "before-I-touch-this" snapshot:
|
|
5
|
+
* - typed signature
|
|
6
|
+
* - first-sentence docstring
|
|
7
|
+
* - side-effect classification (fs/network/global mutation/process)
|
|
8
|
+
* - complexity (branches, maxDepth, lineCount)
|
|
9
|
+
* - async/generator flags
|
|
10
|
+
*
|
|
11
|
+
* No LLM, no heuristics that pretend to "summarize" intent.
|
|
12
|
+
* Everything here is derivable from the AST and existing symbol fields.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
const fs = require('fs');
|
|
18
|
+
const path = require('path');
|
|
19
|
+
const { parse } = require('./parser');
|
|
20
|
+
const { detectLanguage, langTraits } = require('../languages');
|
|
21
|
+
const { formatSymbolHandle } = require('./shared');
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Side-effect signal sets (per-language, conservative)
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
// Module/import names that signal a category.
|
|
28
|
+
// Keys are language; values are { fs: Set, network: Set, process: Set }.
|
|
29
|
+
const SIDE_EFFECT_IMPORTS = {
|
|
30
|
+
javascript: {
|
|
31
|
+
fs: new Set(['fs', 'fs/promises', 'graceful-fs', 'node:fs', 'node:fs/promises']),
|
|
32
|
+
network: new Set(['http', 'https', 'net', 'tls', 'dgram', 'axios', 'node-fetch', 'got', 'undici', 'ws', 'node:http', 'node:https', 'node:net']),
|
|
33
|
+
process: new Set(['child_process', 'cluster', 'worker_threads', 'os', 'node:child_process', 'node:cluster', 'node:worker_threads', 'node:os']),
|
|
34
|
+
},
|
|
35
|
+
typescript: {
|
|
36
|
+
fs: new Set(['fs', 'fs/promises', 'graceful-fs', 'node:fs', 'node:fs/promises']),
|
|
37
|
+
network: new Set(['http', 'https', 'net', 'tls', 'dgram', 'axios', 'node-fetch', 'got', 'undici', 'ws', 'node:http', 'node:https', 'node:net']),
|
|
38
|
+
process: new Set(['child_process', 'cluster', 'worker_threads', 'os', 'node:child_process', 'node:cluster', 'node:worker_threads', 'node:os']),
|
|
39
|
+
},
|
|
40
|
+
python: {
|
|
41
|
+
fs: new Set(['os', 'os.path', 'pathlib', 'shutil', 'tempfile', 'io']),
|
|
42
|
+
network: new Set(['urllib', 'urllib.request', 'http', 'http.client', 'socket', 'requests', 'httpx', 'aiohttp']),
|
|
43
|
+
process: new Set(['subprocess', 'multiprocessing', 'os', 'signal', 'threading']),
|
|
44
|
+
},
|
|
45
|
+
go: {
|
|
46
|
+
fs: new Set(['os', 'io', 'io/ioutil', 'path/filepath', 'embed']),
|
|
47
|
+
network: new Set(['net', 'net/http', 'net/url', 'net/rpc']),
|
|
48
|
+
process: new Set(['os/exec', 'syscall', 'runtime']),
|
|
49
|
+
},
|
|
50
|
+
java: {
|
|
51
|
+
fs: new Set(['java.io', 'java.nio', 'java.nio.file']),
|
|
52
|
+
network: new Set(['java.net', 'java.net.http']),
|
|
53
|
+
process: new Set(['java.lang.Runtime', 'java.lang.ProcessBuilder']),
|
|
54
|
+
},
|
|
55
|
+
rust: {
|
|
56
|
+
fs: new Set(['std::fs', 'std::path']),
|
|
57
|
+
network: new Set(['std::net', 'reqwest', 'hyper', 'tokio::net']),
|
|
58
|
+
process: new Set(['std::process']),
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Identifier names that signal side effects when called or referenced.
|
|
63
|
+
// Plain identifier match — not regex. We require the call to be the receiver-less form
|
|
64
|
+
// (e.g. `fetch(...)`) OR a member of a recognized object (`fs.readFile`).
|
|
65
|
+
const SIDE_EFFECT_CALLS_BY_LANG = {
|
|
66
|
+
javascript: {
|
|
67
|
+
network: new Set(['fetch', 'XMLHttpRequest']),
|
|
68
|
+
// Top-level browser globals that mutate state
|
|
69
|
+
process: new Set(['exit']),
|
|
70
|
+
},
|
|
71
|
+
typescript: {
|
|
72
|
+
network: new Set(['fetch', 'XMLHttpRequest']),
|
|
73
|
+
process: new Set(['exit']),
|
|
74
|
+
},
|
|
75
|
+
python: {
|
|
76
|
+
fs: new Set(['open']),
|
|
77
|
+
process: new Set(['exit', 'system']),
|
|
78
|
+
},
|
|
79
|
+
// Nominal languages: imports already give a strong signal (e.g. `java.io`),
|
|
80
|
+
// and direct-call tokens like `exit` would cause false positives across
|
|
81
|
+
// generic identifiers. We deliberately keep this empty.
|
|
82
|
+
go: {},
|
|
83
|
+
java: {},
|
|
84
|
+
rust: {},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
// ============================================================================
|
|
88
|
+
// brief()
|
|
89
|
+
// ============================================================================
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Compute a brief AST summary for a symbol.
|
|
93
|
+
*
|
|
94
|
+
* @param {object} index - ProjectIndex
|
|
95
|
+
* @param {string} name - Symbol name (function/method/class)
|
|
96
|
+
* @param {object} options - { file, className, git }
|
|
97
|
+
* @returns {object|null}
|
|
98
|
+
*/
|
|
99
|
+
function brief(index, name, options = {}) {
|
|
100
|
+
index._beginOp();
|
|
101
|
+
try {
|
|
102
|
+
const { def } = index.resolveSymbol(name, { file: options.file, className: options.className, line: options.line });
|
|
103
|
+
if (!def) return null;
|
|
104
|
+
|
|
105
|
+
const language = detectLanguage(def.relativePath || def.file);
|
|
106
|
+
const symbol = {
|
|
107
|
+
name: def.name,
|
|
108
|
+
type: def.type,
|
|
109
|
+
file: def.relativePath || def.file,
|
|
110
|
+
startLine: def.startLine,
|
|
111
|
+
endLine: def.endLine,
|
|
112
|
+
handle: formatSymbolHandle(def),
|
|
113
|
+
language,
|
|
114
|
+
...(def.params != null && { params: def.params }),
|
|
115
|
+
...(def.paramsStructured && { paramsStructured: def.paramsStructured }),
|
|
116
|
+
...(def.paramTypes && { paramTypes: def.paramTypes }),
|
|
117
|
+
...(def.returnType && { returnType: def.returnType }),
|
|
118
|
+
...(def.modifiers && def.modifiers.length && { modifiers: def.modifiers }),
|
|
119
|
+
...(def.decorators && def.decorators.length && { decorators: def.decorators }),
|
|
120
|
+
...(def.docstring && { docstring: firstSentence(def.docstring) }),
|
|
121
|
+
...(def.className && { className: def.className }),
|
|
122
|
+
...(def.isAsync && { isAsync: true }),
|
|
123
|
+
...(def.isGenerator && { isGenerator: true }),
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
// Optional git enrichment for the primary symbol's file.
|
|
127
|
+
// Skipped silently outside git repos; formatters check `git.available`.
|
|
128
|
+
let gitInfo = null;
|
|
129
|
+
if (options.git) {
|
|
130
|
+
const { getGitInfo } = require('./git-enrich');
|
|
131
|
+
gitInfo = getGitInfo(index.root, def.relativePath || def.file);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// For non-callable types (class/struct/interface/type), most fields don't apply
|
|
135
|
+
if (['class', 'struct', 'interface', 'type', 'enum'].includes(def.type)) {
|
|
136
|
+
return {
|
|
137
|
+
symbol,
|
|
138
|
+
kind: 'type',
|
|
139
|
+
lineCount: (def.endLine || def.startLine) - def.startLine + 1,
|
|
140
|
+
memberCount: countMembers(index, def),
|
|
141
|
+
...(gitInfo && { git: gitInfo }),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// For callable symbols, scan the body
|
|
146
|
+
const filePath = path.isAbsolute(def.file) ? def.file : path.join(index.root, def.file);
|
|
147
|
+
let bodyText = '';
|
|
148
|
+
try {
|
|
149
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
150
|
+
const lines = content.split('\n');
|
|
151
|
+
const start = Math.max(0, (def.startLine || 1) - 1);
|
|
152
|
+
const end = Math.min(lines.length, def.endLine || def.startLine || 1);
|
|
153
|
+
bodyText = lines.slice(start, end).join('\n');
|
|
154
|
+
} catch (e) {
|
|
155
|
+
return {
|
|
156
|
+
symbol,
|
|
157
|
+
kind: 'function',
|
|
158
|
+
lineCount: 0,
|
|
159
|
+
sideEffects: [],
|
|
160
|
+
complexity: { branches: 0, maxDepth: 0, lineCount: 0 },
|
|
161
|
+
isAsync: !!def.isAsync,
|
|
162
|
+
error: 'Could not read source',
|
|
163
|
+
...(gitInfo && { git: gitInfo }),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const fileEntry = index.files.get(def.file);
|
|
168
|
+
const fileImports = collectImportNames(fileEntry);
|
|
169
|
+
|
|
170
|
+
const sideEffects = classifySideEffects(bodyText, language, fileImports);
|
|
171
|
+
const complexity = computeComplexity(bodyText, language);
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
symbol,
|
|
175
|
+
kind: 'function',
|
|
176
|
+
lineCount: complexity.lineCount,
|
|
177
|
+
sideEffects,
|
|
178
|
+
complexity,
|
|
179
|
+
isAsync: !!def.isAsync,
|
|
180
|
+
isGenerator: !!def.isGenerator,
|
|
181
|
+
...(gitInfo && { git: gitInfo }),
|
|
182
|
+
};
|
|
183
|
+
} finally {
|
|
184
|
+
index._endOp();
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// ============================================================================
|
|
189
|
+
// Helpers
|
|
190
|
+
// ============================================================================
|
|
191
|
+
|
|
192
|
+
function firstSentence(text) {
|
|
193
|
+
if (!text) return null;
|
|
194
|
+
const trimmed = text.trim();
|
|
195
|
+
// Cut on first sentence terminator. Cap at 200 chars to avoid runaway.
|
|
196
|
+
const m = trimmed.match(/^(.+?[.!?])\s/);
|
|
197
|
+
let s = m ? m[1] : trimmed;
|
|
198
|
+
if (s.length > 200) s = s.slice(0, 197) + '...';
|
|
199
|
+
return s;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function countMembers(index, def) {
|
|
203
|
+
if (!def || !def.file) return 0;
|
|
204
|
+
let count = 0;
|
|
205
|
+
for (const arr of index.symbols.values()) {
|
|
206
|
+
for (const s of arr) {
|
|
207
|
+
if (s.file === def.file && s.className === def.name && s.isMethod) count++;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return count;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function collectImportNames(fileEntry) {
|
|
214
|
+
if (!fileEntry) return new Set();
|
|
215
|
+
const names = new Set();
|
|
216
|
+
if (fileEntry.exportDetails) {
|
|
217
|
+
// exportDetails are exports — skip
|
|
218
|
+
}
|
|
219
|
+
// imports map: importName → modulePath (or { source, ... })
|
|
220
|
+
if (fileEntry.imports && typeof fileEntry.imports === 'object') {
|
|
221
|
+
for (const v of Object.values(fileEntry.imports)) {
|
|
222
|
+
if (typeof v === 'string') names.add(v);
|
|
223
|
+
else if (v && v.source) names.add(v.source);
|
|
224
|
+
else if (v && v.from) names.add(v.from);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (fileEntry.importDetails && Array.isArray(fileEntry.importDetails)) {
|
|
228
|
+
for (const imp of fileEntry.importDetails) {
|
|
229
|
+
if (imp && imp.source) names.add(imp.source);
|
|
230
|
+
if (imp && imp.from) names.add(imp.from);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return names;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Classify side-effects from a function body using string scans.
|
|
238
|
+
*
|
|
239
|
+
* Returns: array of categories the function appears to touch:
|
|
240
|
+
* 'fs' — filesystem reads/writes
|
|
241
|
+
* 'network' — outbound network calls
|
|
242
|
+
* 'process' — child processes / OS-level effects
|
|
243
|
+
* 'global_mutation' — assignments to module-level identifiers (heuristic)
|
|
244
|
+
*
|
|
245
|
+
* NOTE: We use textual scanning over the function body — tree-sitter is great
|
|
246
|
+
* for top-level structure but reparsing every function body for the full AST
|
|
247
|
+
* just to detect well-known names is overkill. The signal sets are tight.
|
|
248
|
+
*/
|
|
249
|
+
function classifySideEffects(bodyText, language, fileImports) {
|
|
250
|
+
const out = new Set();
|
|
251
|
+
if (!bodyText) return [];
|
|
252
|
+
|
|
253
|
+
const importsBuckets = SIDE_EFFECT_IMPORTS[language] || {};
|
|
254
|
+
const callsBuckets = SIDE_EFFECT_CALLS_BY_LANG[language] || {};
|
|
255
|
+
|
|
256
|
+
// Resolve which categories the file's imports touch (file-level signal).
|
|
257
|
+
// E.g. if the file imports `fs`, ANY function in the file *could* use it.
|
|
258
|
+
// We confirm by looking for the import-binding name being used in the body.
|
|
259
|
+
// For now, surface category as a "potential" signal if the body references
|
|
260
|
+
// ANY imported binding from a category.
|
|
261
|
+
const fileImportLower = new Set([...fileImports].map(s => s.toLowerCase()));
|
|
262
|
+
for (const [cat, modSet] of Object.entries(importsBuckets)) {
|
|
263
|
+
for (const m of modSet) {
|
|
264
|
+
if (fileImportLower.has(m.toLowerCase())) {
|
|
265
|
+
// Also confirm the body references the module name as an identifier
|
|
266
|
+
// (very common: `fs.readFile`, `requests.get(`, etc.).
|
|
267
|
+
const baseName = m.split(/[./]/).pop();
|
|
268
|
+
if (baseName && new RegExp(`\\b${escapeRegExp(baseName)}\\b`).test(bodyText)) {
|
|
269
|
+
out.add(cat);
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Direct-call signals (no import context needed): `fetch(`, `open(`, etc.
|
|
277
|
+
for (const [cat, callSet] of Object.entries(callsBuckets)) {
|
|
278
|
+
for (const fn of callSet) {
|
|
279
|
+
const re = new RegExp(`\\b${escapeRegExp(fn)}\\s*\\(`);
|
|
280
|
+
if (re.test(bodyText)) {
|
|
281
|
+
out.add(cat);
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Process category for JS console.* (informational, not flagged)
|
|
288
|
+
// Skip — too noisy for "side effect" semantics.
|
|
289
|
+
|
|
290
|
+
// Global-mutation heuristic (cheap):
|
|
291
|
+
// - JS/TS: `module.exports.X = ` / `exports.X = ` / global identifier reassignment at top-level of function body
|
|
292
|
+
// - Python: `global X`
|
|
293
|
+
// - Go: package-level ident on lhs (hard without full AST — skip)
|
|
294
|
+
if (language === 'javascript' || language === 'typescript') {
|
|
295
|
+
if (/\b(module\.exports|exports)\.[A-Za-z_]\w*\s*=/.test(bodyText)) {
|
|
296
|
+
out.add('global_mutation');
|
|
297
|
+
}
|
|
298
|
+
} else if (language === 'python') {
|
|
299
|
+
if (/^\s*global\s+\w/m.test(bodyText)) {
|
|
300
|
+
out.add('global_mutation');
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return [...out].sort();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function escapeRegExp(s) {
|
|
308
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
/**
|
|
312
|
+
* Compute complexity metrics from a function body.
|
|
313
|
+
* Cheap, AST-free counts on tokenized source.
|
|
314
|
+
*/
|
|
315
|
+
function computeComplexity(bodyText, language) {
|
|
316
|
+
const lines = bodyText.split('\n');
|
|
317
|
+
const lineCount = lines.length;
|
|
318
|
+
|
|
319
|
+
// Branch count: count keywords that introduce a new branching path.
|
|
320
|
+
// We deliberately ignore final `else` (it's just the alternate of an `if`).
|
|
321
|
+
const branchPatterns = [
|
|
322
|
+
/\bif\s*\(/g, // JS/TS/Java/Rust/Go/C-like
|
|
323
|
+
/\bif\s+/g, // Python (if x:)
|
|
324
|
+
/\belif\b/g, // Python
|
|
325
|
+
/\belse\s+if\b/g, // JS/Java/etc.
|
|
326
|
+
/\bcase\b/g, // switch case
|
|
327
|
+
/\bwhen\b/g, // Rust match arms (and Kotlin/Scala but we don't support those)
|
|
328
|
+
/\bfor\s*\(/g, // C-like for
|
|
329
|
+
/\bfor\s+\w/g, // Python for x in
|
|
330
|
+
/\bwhile\s*\(/g, // C-like while
|
|
331
|
+
/\bwhile\s+/g, // Python while x:
|
|
332
|
+
/\?[^?]/g, // ternary (rough)
|
|
333
|
+
/\bcatch\s*\(/g, // catch
|
|
334
|
+
/\bexcept\b/g, // Python except
|
|
335
|
+
];
|
|
336
|
+
let branches = 0;
|
|
337
|
+
for (const re of branchPatterns) branches += (bodyText.match(re) || []).length;
|
|
338
|
+
|
|
339
|
+
// maxDepth: indent-based proxy. Fast, language-agnostic, off-by-one safe.
|
|
340
|
+
let maxDepth = 0;
|
|
341
|
+
let firstNonBlankIndent = -1;
|
|
342
|
+
for (const line of lines) {
|
|
343
|
+
if (!line.trim()) continue;
|
|
344
|
+
const m = line.match(/^(\s*)/);
|
|
345
|
+
const spaces = m ? expandIndent(m[1]) : 0;
|
|
346
|
+
if (firstNonBlankIndent === -1) firstNonBlankIndent = spaces;
|
|
347
|
+
// depth = (current - first) / unit; we don't know "unit", so just track
|
|
348
|
+
// raw delta and divide by 2 (conservative — most code is 2 or 4 space indented).
|
|
349
|
+
const rawDepth = Math.max(0, spaces - firstNonBlankIndent);
|
|
350
|
+
if (rawDepth > maxDepth) maxDepth = rawDepth;
|
|
351
|
+
}
|
|
352
|
+
// Translate raw spaces to depth levels (assume 2-space indent baseline)
|
|
353
|
+
const depth = Math.round(maxDepth / 2);
|
|
354
|
+
|
|
355
|
+
return { branches, maxDepth: depth, lineCount };
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function expandIndent(s) {
|
|
359
|
+
let n = 0;
|
|
360
|
+
for (const c of s) n += (c === '\t') ? 4 : 1;
|
|
361
|
+
return n;
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Lazy classifier: side-effect tags for an arbitrary symbol record.
|
|
366
|
+
* Used by callee output (`context`, `about`) to surface [fs]/[net]/[proc] tags
|
|
367
|
+
* inline. Cached on the index in `_sideEffectCache` (key: file:startLine).
|
|
368
|
+
*
|
|
369
|
+
* Cheap on cache hit; first hit reads + scans the symbol's body. Returns
|
|
370
|
+
* `null` for non-callable types or unreadable files.
|
|
371
|
+
*/
|
|
372
|
+
function sideEffectsFor(index, symbol) {
|
|
373
|
+
if (!index || !symbol) return null;
|
|
374
|
+
if (NON_CALLABLE_KIND.has(symbol.type)) return null;
|
|
375
|
+
const key = `${symbol.file || symbol.relativePath}:${symbol.startLine || 0}`;
|
|
376
|
+
if (!index._sideEffectCache) index._sideEffectCache = new Map();
|
|
377
|
+
if (index._sideEffectCache.has(key)) return index._sideEffectCache.get(key);
|
|
378
|
+
|
|
379
|
+
const filePath = path.isAbsolute(symbol.file || '') ? symbol.file : path.join(index.root, symbol.file || symbol.relativePath || '');
|
|
380
|
+
let bodyText = '';
|
|
381
|
+
try {
|
|
382
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
383
|
+
const lines = content.split('\n');
|
|
384
|
+
const start = Math.max(0, (symbol.startLine || 1) - 1);
|
|
385
|
+
const end = Math.min(lines.length, symbol.endLine || symbol.startLine || 1);
|
|
386
|
+
bodyText = lines.slice(start, end).join('\n');
|
|
387
|
+
} catch (e) {
|
|
388
|
+
index._sideEffectCache.set(key, null);
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
const language = detectLanguage(symbol.relativePath || symbol.file);
|
|
392
|
+
const fileEntry = index.files.get(symbol.file);
|
|
393
|
+
const fileImports = collectImportNames(fileEntry);
|
|
394
|
+
const tags = classifySideEffects(bodyText, language, fileImports);
|
|
395
|
+
index._sideEffectCache.set(key, tags);
|
|
396
|
+
return tags;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const NON_CALLABLE_KIND = new Set(['class', 'struct', 'interface', 'type', 'enum', 'trait', 'impl', 'state', 'field']);
|
|
400
|
+
|
|
401
|
+
module.exports = {
|
|
402
|
+
brief,
|
|
403
|
+
sideEffectsFor,
|
|
404
|
+
// exposed for tests
|
|
405
|
+
classifySideEffects,
|
|
406
|
+
computeComplexity,
|
|
407
|
+
firstSentence,
|
|
408
|
+
};
|