minovative-mind-cli 1.2.3 → 1.3.1
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/LICENSE.md +1 -1
- package/README.md +68 -110
- package/dist/services/agent.js +11 -7
- package/dist/services/proxyClient.d.ts +42 -0
- package/dist/services/proxyClient.js +20 -0
- package/dist/services/verificationService.d.ts +6 -1
- package/dist/services/verificationService.js +156 -9
- package/dist/utils/performanceAuditor.d.ts +42 -0
- package/dist/utils/performanceAuditor.js +615 -0
- package/dist/utils/symbolExtractor.js +193 -164
- package/dist/utils/systemPrompts.d.ts +1 -1
- package/dist/utils/systemPrompts.js +5 -0
- package/oclif.manifest.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,615 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import pc from 'picocolors';
|
|
3
|
+
const EXTENSION_MAP = {
|
|
4
|
+
'.ts': 'js', '.tsx': 'js', '.js': 'js', '.jsx': 'js', '.mjs': 'js', '.cjs': 'js',
|
|
5
|
+
'.py': 'python', '.pyw': 'python',
|
|
6
|
+
'.go': 'go',
|
|
7
|
+
'.rs': 'rust',
|
|
8
|
+
};
|
|
9
|
+
function detectLanguage(filePath) {
|
|
10
|
+
return EXTENSION_MAP[path.extname(filePath).toLowerCase()] ?? 'unknown';
|
|
11
|
+
}
|
|
12
|
+
// ─── Comment & String Stripping ──────────────────────────────────────
|
|
13
|
+
//
|
|
14
|
+
// Before running regex-based detection, we strip string literals and
|
|
15
|
+
// comments from the source to prevent false positives on patterns
|
|
16
|
+
// appearing inside strings or documentation.
|
|
17
|
+
/**
|
|
18
|
+
* Replaces the content of string literals and comments with whitespace,
|
|
19
|
+
* preserving line count so that line-number tracking remains accurate.
|
|
20
|
+
*/
|
|
21
|
+
function stripStringsAndComments(source, lang) {
|
|
22
|
+
if (lang === 'unknown')
|
|
23
|
+
return source;
|
|
24
|
+
let result = '';
|
|
25
|
+
let i = 0;
|
|
26
|
+
while (i < source.length) {
|
|
27
|
+
const ch = source[i];
|
|
28
|
+
const next = source[i + 1];
|
|
29
|
+
// ── JS/TS/Go/Rust: line comments (//) ─────────────────────
|
|
30
|
+
if ((lang === 'js' || lang === 'go' || lang === 'rust') && ch === '/' && next === '/') {
|
|
31
|
+
const eol = source.indexOf('\n', i);
|
|
32
|
+
const end = eol === -1 ? source.length : eol;
|
|
33
|
+
result += ' '.repeat(end - i);
|
|
34
|
+
i = end;
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
// ── JS/TS/Go/Rust: block comments (/* ... */) ─────────────
|
|
38
|
+
if ((lang === 'js' || lang === 'go' || lang === 'rust') && ch === '/' && next === '*') {
|
|
39
|
+
const end = source.indexOf('*/', i + 2);
|
|
40
|
+
const closePos = end === -1 ? source.length : end + 2;
|
|
41
|
+
// Preserve newlines for line counting
|
|
42
|
+
const segment = source.substring(i, closePos);
|
|
43
|
+
result += segment.replace(/[^\n]/g, ' ');
|
|
44
|
+
i = closePos;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
// ── Python: line comments (#) ─────────────────────────────
|
|
48
|
+
if (lang === 'python' && ch === '#') {
|
|
49
|
+
const eol = source.indexOf('\n', i);
|
|
50
|
+
const end = eol === -1 ? source.length : eol;
|
|
51
|
+
result += ' '.repeat(end - i);
|
|
52
|
+
i = end;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
// ── Python: triple-quoted strings ─────────────────────────
|
|
56
|
+
if (lang === 'python' && (source.substring(i, i + 3) === '"""' || source.substring(i, i + 3) === "'''")) {
|
|
57
|
+
const quote = source.substring(i, i + 3);
|
|
58
|
+
const end = source.indexOf(quote, i + 3);
|
|
59
|
+
const closePos = end === -1 ? source.length : end + 3;
|
|
60
|
+
const segment = source.substring(i, closePos);
|
|
61
|
+
result += segment.replace(/[^\n]/g, ' ');
|
|
62
|
+
i = closePos;
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
// ── All languages: string literals ────────────────────────
|
|
66
|
+
if (ch === '"' || ch === "'" || (lang === 'js' && ch === '`')) {
|
|
67
|
+
const quote = ch;
|
|
68
|
+
let j = i + 1;
|
|
69
|
+
while (j < source.length) {
|
|
70
|
+
if (source[j] === '\\') {
|
|
71
|
+
j += 2; // skip escaped character
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (source[j] === quote) {
|
|
75
|
+
j++;
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
// Template literals can span multiple lines
|
|
79
|
+
if (quote === '`' && source[j] === '\n') {
|
|
80
|
+
result += ' '.repeat(j - i);
|
|
81
|
+
result += '\n';
|
|
82
|
+
i = j + 1;
|
|
83
|
+
j = i;
|
|
84
|
+
// Reset — we'll pick up again from the new line
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
j++;
|
|
88
|
+
}
|
|
89
|
+
const segment = source.substring(i, j);
|
|
90
|
+
result += segment.replace(/[^\n]/g, ' ');
|
|
91
|
+
i = j;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
result += ch;
|
|
95
|
+
i++;
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
// ─── Utility: Line Number Lookup ─────────────────────────────────────
|
|
100
|
+
/**
|
|
101
|
+
* Given a character offset in the source, returns the 1-indexed line number.
|
|
102
|
+
*/
|
|
103
|
+
function getLineNumber(source, offset) {
|
|
104
|
+
let line = 1;
|
|
105
|
+
for (let i = 0; i < offset && i < source.length; i++) {
|
|
106
|
+
if (source[i] === '\n')
|
|
107
|
+
line++;
|
|
108
|
+
}
|
|
109
|
+
return line;
|
|
110
|
+
}
|
|
111
|
+
// ── PERF-001: Nested Loop Detection (O(n²) risk) ────────────────────
|
|
112
|
+
//
|
|
113
|
+
// Strategy: Track brace-depth to identify loop constructs that contain
|
|
114
|
+
// other loop constructs. Works across all C-family languages + Python.
|
|
115
|
+
const detectNestedLoops = (source, stripped, lang, findings) => {
|
|
116
|
+
// Python uses indentation, not braces — use a different strategy
|
|
117
|
+
if (lang === 'python') {
|
|
118
|
+
detectNestedLoopsPython(source, stripped, findings);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
// C-family: scan for loop keywords and track brace depth
|
|
122
|
+
const loopPattern = /\b(for|while|do)\s*[\s(]/g;
|
|
123
|
+
const lines = stripped.split('\n');
|
|
124
|
+
for (let i = 0; i < lines.length; i++) {
|
|
125
|
+
const line = lines[i];
|
|
126
|
+
if (!loopPattern.test(line)) {
|
|
127
|
+
loopPattern.lastIndex = 0;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
loopPattern.lastIndex = 0;
|
|
131
|
+
// Found a loop — scan forward tracking brace depth to find nested loops
|
|
132
|
+
let depth = 0;
|
|
133
|
+
let foundOpening = false;
|
|
134
|
+
for (let j = i; j < lines.length; j++) {
|
|
135
|
+
const innerLine = lines[j];
|
|
136
|
+
for (const ch of innerLine) {
|
|
137
|
+
if (ch === '{') {
|
|
138
|
+
depth++;
|
|
139
|
+
foundOpening = true;
|
|
140
|
+
}
|
|
141
|
+
else if (ch === '}') {
|
|
142
|
+
depth--;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Check lines inside the outer loop body for nested loops
|
|
146
|
+
if (j > i && foundOpening && depth > 0) {
|
|
147
|
+
if (/\b(for|while|do)\s*[\s(]/.test(innerLine)) {
|
|
148
|
+
findings.push({
|
|
149
|
+
severity: 'WARNING',
|
|
150
|
+
code: 'PERF-001',
|
|
151
|
+
line: j + 1,
|
|
152
|
+
message: `Nested loop detected (outer loop at line ${i + 1}) — O(n²) or worse complexity.`,
|
|
153
|
+
suggestion: 'Consider using a Map/Set for O(1) lookups, or restructure to avoid nested iteration.',
|
|
154
|
+
});
|
|
155
|
+
break; // One warning per outer loop is enough
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
// Outer loop body has closed
|
|
159
|
+
if (foundOpening && depth <= 0)
|
|
160
|
+
break;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
function detectNestedLoopsPython(_source, stripped, findings) {
|
|
165
|
+
const lines = stripped.split('\n');
|
|
166
|
+
const loopPattern = /^\s*(for\s+.+\s+in\s+|while\s+)/;
|
|
167
|
+
for (let i = 0; i < lines.length; i++) {
|
|
168
|
+
const outerMatch = lines[i].match(loopPattern);
|
|
169
|
+
if (!outerMatch)
|
|
170
|
+
continue;
|
|
171
|
+
const outerIndent = lines[i].search(/\S/);
|
|
172
|
+
if (outerIndent < 0)
|
|
173
|
+
continue;
|
|
174
|
+
// Scan forward for a nested loop with deeper indentation
|
|
175
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
176
|
+
const innerLine = lines[j];
|
|
177
|
+
if (innerLine.trim() === '')
|
|
178
|
+
continue;
|
|
179
|
+
const innerIndent = innerLine.search(/\S/);
|
|
180
|
+
// If we've returned to same or lesser indent, the outer loop body is over
|
|
181
|
+
if (innerIndent <= outerIndent)
|
|
182
|
+
break;
|
|
183
|
+
if (loopPattern.test(innerLine)) {
|
|
184
|
+
findings.push({
|
|
185
|
+
severity: 'WARNING',
|
|
186
|
+
code: 'PERF-001',
|
|
187
|
+
line: j + 1,
|
|
188
|
+
message: `Nested loop detected (outer loop at line ${i + 1}) — O(n²) or worse complexity.`,
|
|
189
|
+
suggestion: 'Consider using a dictionary for O(1) lookups, or restructure to avoid nested iteration.',
|
|
190
|
+
});
|
|
191
|
+
break;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
// ── PERF-002: Chained Array Methods (multiple intermediate allocations) ──
|
|
197
|
+
const detectChainedArrayMethods = (_source, stripped, lang, findings) => {
|
|
198
|
+
if (lang !== 'js')
|
|
199
|
+
return;
|
|
200
|
+
// Match chains like .map(...).filter(...).reduce(...) with 3+ methods
|
|
201
|
+
const chainPattern = /\.(map|filter|reduce|flatMap|flat|sort|slice|concat)\s*\([^)]*\)\s*\.(map|filter|reduce|flatMap|flat|sort|slice|concat)\s*\([^)]*\)\s*\.(map|filter|reduce|flatMap|flat|sort|slice|concat)/g;
|
|
202
|
+
const lines = stripped.split('\n');
|
|
203
|
+
// Search across joined lines since chains can span multiple lines
|
|
204
|
+
const joined = stripped;
|
|
205
|
+
let match;
|
|
206
|
+
while ((match = chainPattern.exec(joined)) !== null) {
|
|
207
|
+
const line = getLineNumber(stripped, match.index);
|
|
208
|
+
findings.push({
|
|
209
|
+
severity: 'WARNING',
|
|
210
|
+
code: 'PERF-002',
|
|
211
|
+
line,
|
|
212
|
+
message: `Chained array methods (.${match[1]}().${match[2]}().${match[3]}()) — creates 3+ intermediate arrays.`,
|
|
213
|
+
suggestion: 'Combine into a single .reduce() or use a for-loop to avoid intermediate allocations.',
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
// ── PERF-003: Synchronous I/O in Async Functions ─────────────────────
|
|
218
|
+
const detectSyncIOInAsync = (_source, stripped, lang, findings) => {
|
|
219
|
+
if (lang !== 'js')
|
|
220
|
+
return;
|
|
221
|
+
const syncAPIs = [
|
|
222
|
+
'readFileSync', 'writeFileSync', 'appendFileSync', 'mkdirSync',
|
|
223
|
+
'readdirSync', 'statSync', 'existsSync', 'copyFileSync',
|
|
224
|
+
'renameSync', 'unlinkSync', 'rmdirSync', 'accessSync',
|
|
225
|
+
];
|
|
226
|
+
const lines = stripped.split('\n');
|
|
227
|
+
let insideAsync = false;
|
|
228
|
+
let asyncBraceDepth = 0;
|
|
229
|
+
let braceDepth = 0;
|
|
230
|
+
for (let i = 0; i < lines.length; i++) {
|
|
231
|
+
const line = lines[i];
|
|
232
|
+
// Detect async function/method/arrow entry
|
|
233
|
+
if (/\basync\s+(function\b|\w+\s*\(|(\w+)\s*=>|\(.*\)\s*=>)/.test(line) || /\basync\s+\w+\s*\(/.test(line)) {
|
|
234
|
+
insideAsync = true;
|
|
235
|
+
asyncBraceDepth = braceDepth;
|
|
236
|
+
}
|
|
237
|
+
for (const ch of line) {
|
|
238
|
+
if (ch === '{')
|
|
239
|
+
braceDepth++;
|
|
240
|
+
else if (ch === '}') {
|
|
241
|
+
braceDepth--;
|
|
242
|
+
if (insideAsync && braceDepth <= asyncBraceDepth) {
|
|
243
|
+
insideAsync = false;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
if (insideAsync) {
|
|
248
|
+
for (const api of syncAPIs) {
|
|
249
|
+
if (line.includes(api)) {
|
|
250
|
+
findings.push({
|
|
251
|
+
severity: 'ERROR',
|
|
252
|
+
code: 'PERF-003',
|
|
253
|
+
line: i + 1,
|
|
254
|
+
message: `Synchronous I/O (${api}) inside async function — blocks the event loop.`,
|
|
255
|
+
suggestion: 'Use the async equivalent (e.g., fs.promises.readFile or fs/promises) instead.',
|
|
256
|
+
});
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
};
|
|
262
|
+
// ── PERF-004: Object/Array Spread Inside Loops ───────────────────────
|
|
263
|
+
const detectSpreadInLoops = (_source, stripped, lang, findings) => {
|
|
264
|
+
if (lang !== 'js')
|
|
265
|
+
return;
|
|
266
|
+
const lines = stripped.split('\n');
|
|
267
|
+
let loopDepth = 0;
|
|
268
|
+
let braceDepth = 0;
|
|
269
|
+
let inLoop = false;
|
|
270
|
+
let loopBraceStart = 0;
|
|
271
|
+
for (let i = 0; i < lines.length; i++) {
|
|
272
|
+
const line = lines[i];
|
|
273
|
+
if (/\b(for|while|do)\s*[\s(]/.test(line) && !inLoop) {
|
|
274
|
+
inLoop = true;
|
|
275
|
+
loopBraceStart = braceDepth;
|
|
276
|
+
loopDepth++;
|
|
277
|
+
}
|
|
278
|
+
for (const ch of line) {
|
|
279
|
+
if (ch === '{')
|
|
280
|
+
braceDepth++;
|
|
281
|
+
else if (ch === '}') {
|
|
282
|
+
braceDepth--;
|
|
283
|
+
if (inLoop && braceDepth <= loopBraceStart) {
|
|
284
|
+
inLoop = false;
|
|
285
|
+
loopDepth--;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (loopDepth > 0 && /\.\.\.[\w$]/.test(line)) {
|
|
290
|
+
findings.push({
|
|
291
|
+
severity: 'WARNING',
|
|
292
|
+
code: 'PERF-004',
|
|
293
|
+
line: i + 1,
|
|
294
|
+
message: 'Object/Array spread inside a loop — creates a full shallow copy on every iteration.',
|
|
295
|
+
suggestion: 'Mutate the object directly or accumulate into a pre-allocated structure.',
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
// ── PERF-005: Missing Resource Cleanup ───────────────────────────────
|
|
301
|
+
const detectMissingCleanup = (source, stripped, lang, findings) => {
|
|
302
|
+
if (lang !== 'js' && lang !== 'python')
|
|
303
|
+
return;
|
|
304
|
+
if (lang === 'js') {
|
|
305
|
+
// Look for createReadStream/createWriteStream/createServer assigned to a variable
|
|
306
|
+
const pattern = /\b(const|let|var)\s+(\w+)\s*=\s*.*\b(createReadStream|createWriteStream|createServer|createConnection|net\.connect|tls\.connect)\b/g;
|
|
307
|
+
let match;
|
|
308
|
+
while ((match = pattern.exec(stripped)) !== null) {
|
|
309
|
+
const varName = match[2];
|
|
310
|
+
const creator = match[3];
|
|
311
|
+
const line = getLineNumber(stripped, match.index);
|
|
312
|
+
// Check if .close(), .destroy(), .end(), or .disconnect() is called on this variable
|
|
313
|
+
// within a reasonable scope (the rest of the file)
|
|
314
|
+
const rest = stripped.substring(match.index);
|
|
315
|
+
const hasCleanup = rest.includes(`${varName}.close()`) ||
|
|
316
|
+
rest.includes(`${varName}.destroy()`) ||
|
|
317
|
+
rest.includes(`${varName}.end()`) ||
|
|
318
|
+
rest.includes(`${varName}.disconnect()`) ||
|
|
319
|
+
// Also accept piping (which handles cleanup) or 'using' declarations
|
|
320
|
+
rest.includes(`${varName}.pipe(`) ||
|
|
321
|
+
stripped.includes(`using ${varName}`);
|
|
322
|
+
if (!hasCleanup) {
|
|
323
|
+
findings.push({
|
|
324
|
+
severity: 'WARNING',
|
|
325
|
+
code: 'PERF-005',
|
|
326
|
+
line,
|
|
327
|
+
message: `Resource created via ${creator}() assigned to '${varName}' but no .close()/.destroy()/.end() found.`,
|
|
328
|
+
suggestion: "Use a try/finally block or 'using' declaration to ensure cleanup.",
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
if (lang === 'python') {
|
|
334
|
+
// Detect open() not inside a 'with' statement
|
|
335
|
+
const lines = stripped.split('\n');
|
|
336
|
+
for (let i = 0; i < lines.length; i++) {
|
|
337
|
+
const line = lines[i];
|
|
338
|
+
// Match: var = open(...) but NOT: with open(...) as var
|
|
339
|
+
if (/=\s*open\s*\(/.test(line) && !/\bwith\b/.test(line)) {
|
|
340
|
+
findings.push({
|
|
341
|
+
severity: 'WARNING',
|
|
342
|
+
code: 'PERF-005',
|
|
343
|
+
line: i + 1,
|
|
344
|
+
message: "File opened with open() without a 'with' statement — resource may not be properly closed.",
|
|
345
|
+
suggestion: "Use 'with open(...) as f:' to ensure the file handle is automatically closed.",
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
// ── PERF-006: eval() / new Function() / exec() ──────────────────────
|
|
352
|
+
const detectDangerousEval = (_source, stripped, lang, findings) => {
|
|
353
|
+
const lines = stripped.split('\n');
|
|
354
|
+
for (let i = 0; i < lines.length; i++) {
|
|
355
|
+
const line = lines[i];
|
|
356
|
+
if (lang === 'js') {
|
|
357
|
+
// Match eval(...) but not .addEventListener('...', eval)
|
|
358
|
+
if (/\beval\s*\(/.test(line)) {
|
|
359
|
+
findings.push({
|
|
360
|
+
severity: 'ERROR',
|
|
361
|
+
code: 'PERF-006',
|
|
362
|
+
line: i + 1,
|
|
363
|
+
message: 'eval() usage — prevents V8 optimizations and poses security risks.',
|
|
364
|
+
suggestion: 'Use JSON.parse(), a lookup table, or refactor the logic to avoid dynamic code evaluation.',
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
if (/\bnew\s+Function\s*\(/.test(line)) {
|
|
368
|
+
findings.push({
|
|
369
|
+
severity: 'ERROR',
|
|
370
|
+
code: 'PERF-006',
|
|
371
|
+
line: i + 1,
|
|
372
|
+
message: 'new Function() — dynamically compiled code bypasses V8 optimization pipeline.',
|
|
373
|
+
suggestion: 'Pre-compile or use a safer alternative (e.g., a lookup table or map).',
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
if (lang === 'python') {
|
|
378
|
+
if (/\beval\s*\(/.test(line)) {
|
|
379
|
+
findings.push({
|
|
380
|
+
severity: 'ERROR',
|
|
381
|
+
code: 'PERF-006',
|
|
382
|
+
line: i + 1,
|
|
383
|
+
message: 'eval() usage — arbitrary code execution risk and performance penalty.',
|
|
384
|
+
suggestion: 'Use ast.literal_eval() for safe evaluation, or refactor to avoid dynamic code.',
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
if (/\bexec\s*\(/.test(line)) {
|
|
388
|
+
findings.push({
|
|
389
|
+
severity: 'ERROR',
|
|
390
|
+
code: 'PERF-006',
|
|
391
|
+
line: i + 1,
|
|
392
|
+
message: 'exec() usage — arbitrary code execution risk.',
|
|
393
|
+
suggestion: 'Refactor to avoid dynamic code execution. Use importlib or a dispatch table.',
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
};
|
|
399
|
+
// ── PERF-007: JSON.parse on Unvalidated Input ────────────────────────
|
|
400
|
+
const detectUnsafeJsonParse = (_source, stripped, lang, findings) => {
|
|
401
|
+
if (lang !== 'js')
|
|
402
|
+
return;
|
|
403
|
+
const lines = stripped.split('\n');
|
|
404
|
+
let insideAsync = false;
|
|
405
|
+
let asyncBraceDepth = 0;
|
|
406
|
+
let braceDepth = 0;
|
|
407
|
+
for (let i = 0; i < lines.length; i++) {
|
|
408
|
+
const line = lines[i];
|
|
409
|
+
if (/\basync\s+/.test(line)) {
|
|
410
|
+
insideAsync = true;
|
|
411
|
+
asyncBraceDepth = braceDepth;
|
|
412
|
+
}
|
|
413
|
+
for (const ch of line) {
|
|
414
|
+
if (ch === '{')
|
|
415
|
+
braceDepth++;
|
|
416
|
+
else if (ch === '}') {
|
|
417
|
+
braceDepth--;
|
|
418
|
+
if (insideAsync && braceDepth <= asyncBraceDepth)
|
|
419
|
+
insideAsync = false;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
// Detect JSON.parse(req.body) / JSON.parse(body) / JSON.parse(data) etc.
|
|
423
|
+
// in what appears to be a request handler context
|
|
424
|
+
if (/JSON\.parse\s*\(/.test(line)) {
|
|
425
|
+
// Check if this is inside a try block — if so, it's handled
|
|
426
|
+
const prevLines = lines.slice(Math.max(0, i - 5), i).join('\n');
|
|
427
|
+
if (/\btry\s*\{/.test(prevLines))
|
|
428
|
+
continue;
|
|
429
|
+
// Check if the argument looks like an external input
|
|
430
|
+
const argMatch = line.match(/JSON\.parse\s*\(\s*(\w+)/);
|
|
431
|
+
if (argMatch) {
|
|
432
|
+
const argName = argMatch[1].toLowerCase();
|
|
433
|
+
const externalHints = ['body', 'data', 'payload', 'input', 'raw', 'text', 'chunk', 'buffer', 'message'];
|
|
434
|
+
if (externalHints.some((hint) => argName.includes(hint))) {
|
|
435
|
+
findings.push({
|
|
436
|
+
severity: 'INFO',
|
|
437
|
+
code: 'PERF-007',
|
|
438
|
+
line: i + 1,
|
|
439
|
+
message: `JSON.parse() on '${argMatch[1]}' without try/catch — may throw on malformed input or cause OOM on large payloads.`,
|
|
440
|
+
suggestion: 'Wrap in try/catch and consider validating payload size before parsing.',
|
|
441
|
+
});
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
};
|
|
447
|
+
// ── PERF-008: Unbounded Database/API Fetch ───────────────────────────
|
|
448
|
+
const detectUnboundedFetch = (_source, stripped, lang, findings) => {
|
|
449
|
+
const lines = stripped.split('\n');
|
|
450
|
+
for (let i = 0; i < lines.length; i++) {
|
|
451
|
+
const line = lines[i];
|
|
452
|
+
if (lang === 'js') {
|
|
453
|
+
// Detect .find() / .findMany() / .select() / .query() without .limit() / .take() / .paginate()
|
|
454
|
+
// Common in Prisma, Mongoose, Knex, Sequelize
|
|
455
|
+
if (/\.(find|findMany|findAll|select|query)\s*\(/.test(line)) {
|
|
456
|
+
// Look ahead a few lines for a .limit() / .take() / .skip() / .paginate() / .first()
|
|
457
|
+
const window = lines.slice(i, Math.min(i + 5, lines.length)).join('\n');
|
|
458
|
+
if (!/\.(limit|take|first|paginate|skip|offset|top)\s*\(/.test(window) &&
|
|
459
|
+
!/\bLIMIT\b/i.test(window)) {
|
|
460
|
+
findings.push({
|
|
461
|
+
severity: 'WARNING',
|
|
462
|
+
code: 'PERF-008',
|
|
463
|
+
line: i + 1,
|
|
464
|
+
message: 'Database/API query without .limit() or pagination — may fetch unbounded rows into memory.',
|
|
465
|
+
suggestion: 'Add .limit(), .take(), or cursor-based pagination to bound the result set.',
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
if (lang === 'python') {
|
|
471
|
+
// Detect .all() / .filter() on Django/SQLAlchemy querysets without [:N] or .limit()
|
|
472
|
+
if (/\.(all|filter|objects\.filter|objects\.all)\s*\(/.test(line)) {
|
|
473
|
+
const window = lines.slice(i, Math.min(i + 5, lines.length)).join('\n');
|
|
474
|
+
if (!/\.(limit|first|paginate|count)\s*\(/.test(window) &&
|
|
475
|
+
!/\[:?\d+\]/.test(window) &&
|
|
476
|
+
!/\bLIMIT\b/i.test(window)) {
|
|
477
|
+
findings.push({
|
|
478
|
+
severity: 'WARNING',
|
|
479
|
+
code: 'PERF-008',
|
|
480
|
+
line: i + 1,
|
|
481
|
+
message: 'Query without .limit() or slicing — may fetch unbounded rows into memory.',
|
|
482
|
+
suggestion: 'Add .limit(), slicing [:N], or cursor-based pagination to bound the result set.',
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
// Go / Rust: detect SQL queries without LIMIT
|
|
488
|
+
if (lang === 'go' || lang === 'rust') {
|
|
489
|
+
if (/SELECT\s+/i.test(line) && !/\bLIMIT\b/i.test(line)) {
|
|
490
|
+
// Look ahead for LIMIT on subsequent lines
|
|
491
|
+
const window = lines.slice(i, Math.min(i + 5, lines.length)).join('\n');
|
|
492
|
+
if (!/\bLIMIT\b/i.test(window)) {
|
|
493
|
+
findings.push({
|
|
494
|
+
severity: 'WARNING',
|
|
495
|
+
code: 'PERF-008',
|
|
496
|
+
line: i + 1,
|
|
497
|
+
message: 'SQL SELECT without LIMIT clause — may fetch unbounded rows into memory.',
|
|
498
|
+
suggestion: 'Add a LIMIT clause or use cursor-based pagination.',
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
};
|
|
505
|
+
// ─── Rule Registry ───────────────────────────────────────────────────
|
|
506
|
+
const ALL_RULES = [
|
|
507
|
+
detectNestedLoops,
|
|
508
|
+
detectChainedArrayMethods,
|
|
509
|
+
detectSyncIOInAsync,
|
|
510
|
+
detectSpreadInLoops,
|
|
511
|
+
detectMissingCleanup,
|
|
512
|
+
detectDangerousEval,
|
|
513
|
+
detectUnsafeJsonParse,
|
|
514
|
+
detectUnboundedFetch,
|
|
515
|
+
];
|
|
516
|
+
// ─── Supported Extensions for Auditing ───────────────────────────────
|
|
517
|
+
const AUDITABLE_EXTENSIONS = new Set(Object.keys(EXTENSION_MAP));
|
|
518
|
+
/**
|
|
519
|
+
* Returns true if the file extension is supported by the performance auditor.
|
|
520
|
+
*/
|
|
521
|
+
export function isAuditableFile(filePath) {
|
|
522
|
+
return AUDITABLE_EXTENSIONS.has(path.extname(filePath).toLowerCase());
|
|
523
|
+
}
|
|
524
|
+
// ─── Main Entry Point ────────────────────────────────────────────────
|
|
525
|
+
/**
|
|
526
|
+
* Scans source code for performance anti-patterns using language-aware
|
|
527
|
+
* regex-based heuristics. Returns structured findings grouped by severity.
|
|
528
|
+
*
|
|
529
|
+
* This function is designed to be called inline during the verification
|
|
530
|
+
* phase — it's synchronous, zero-dependency, and executes in <50ms on
|
|
531
|
+
* files up to 10,000 lines.
|
|
532
|
+
*
|
|
533
|
+
* @param content - The raw source code string to audit.
|
|
534
|
+
* @param filePath - The file path (used for language detection via extension).
|
|
535
|
+
* @returns A structured audit result with findings separated by severity.
|
|
536
|
+
*/
|
|
537
|
+
export function auditFilePerformance(content, filePath) {
|
|
538
|
+
const lang = detectLanguage(filePath);
|
|
539
|
+
const findings = [];
|
|
540
|
+
if (lang === 'unknown') {
|
|
541
|
+
return { filePath, findings: [], errors: [], warnings: [], infos: [] };
|
|
542
|
+
}
|
|
543
|
+
// Strip strings and comments to avoid false positives
|
|
544
|
+
const stripped = stripStringsAndComments(content, lang);
|
|
545
|
+
for (const rule of ALL_RULES) {
|
|
546
|
+
rule(content, stripped, lang, findings);
|
|
547
|
+
}
|
|
548
|
+
// Sort by severity (ERROR first), then by line number
|
|
549
|
+
const severityOrder = { ERROR: 0, WARNING: 1, INFO: 2 };
|
|
550
|
+
findings.sort((a, b) => {
|
|
551
|
+
const sevDiff = severityOrder[a.severity] - severityOrder[b.severity];
|
|
552
|
+
return sevDiff !== 0 ? sevDiff : a.line - b.line;
|
|
553
|
+
});
|
|
554
|
+
return {
|
|
555
|
+
filePath,
|
|
556
|
+
findings,
|
|
557
|
+
errors: findings.filter((f) => f.severity === 'ERROR'),
|
|
558
|
+
warnings: findings.filter((f) => f.severity === 'WARNING'),
|
|
559
|
+
infos: findings.filter((f) => f.severity === 'INFO'),
|
|
560
|
+
};
|
|
561
|
+
}
|
|
562
|
+
// ─── Output Formatters ───────────────────────────────────────────────
|
|
563
|
+
const SEVERITY_ICONS = {
|
|
564
|
+
ERROR: '🔴',
|
|
565
|
+
WARNING: '🟡',
|
|
566
|
+
INFO: '🔵',
|
|
567
|
+
};
|
|
568
|
+
/**
|
|
569
|
+
* Formats audit findings for beautiful terminal display using picocolors.
|
|
570
|
+
* Used for non-blocking warnings shown to the user after verification.
|
|
571
|
+
*/
|
|
572
|
+
export function formatAuditForTerminal(results) {
|
|
573
|
+
const allFindings = results.flatMap((r) => r.findings.map((f) => ({ ...f, file: r.filePath })));
|
|
574
|
+
if (allFindings.length === 0)
|
|
575
|
+
return '';
|
|
576
|
+
const lines = [
|
|
577
|
+
pc.bold('Performance Audit Results'),
|
|
578
|
+
pc.dim('─'.repeat(50)),
|
|
579
|
+
];
|
|
580
|
+
for (const f of allFindings) {
|
|
581
|
+
const icon = SEVERITY_ICONS[f.severity];
|
|
582
|
+
const sevColor = f.severity === 'ERROR' ? pc.red : f.severity === 'WARNING' ? pc.yellow : pc.blue;
|
|
583
|
+
lines.push(`${icon} ${sevColor(f.severity)} ${pc.dim(`[${f.code}]`)} ${pc.cyan(f.file)}${pc.dim(`:${f.line}`)}`);
|
|
584
|
+
lines.push(` ${f.message}`);
|
|
585
|
+
lines.push(` ${pc.dim('→')} ${pc.dim(f.suggestion)}`);
|
|
586
|
+
}
|
|
587
|
+
const errorCount = allFindings.filter((f) => f.severity === 'ERROR').length;
|
|
588
|
+
const warnCount = allFindings.filter((f) => f.severity === 'WARNING').length;
|
|
589
|
+
const infoCount = allFindings.filter((f) => f.severity === 'INFO').length;
|
|
590
|
+
lines.push(pc.dim('─'.repeat(50)));
|
|
591
|
+
lines.push(`${errorCount > 0 ? pc.red(`${errorCount} error(s)`) : '0 errors'}, ` +
|
|
592
|
+
`${warnCount > 0 ? pc.yellow(`${warnCount} warning(s)`) : '0 warnings'}, ` +
|
|
593
|
+
`${infoCount > 0 ? pc.blue(`${infoCount} info`) : '0 info'}`);
|
|
594
|
+
return lines.join('\n');
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Formats audit findings into a structured prompt for the AI auto-correction loop.
|
|
598
|
+
* Only includes ERROR-severity findings (warnings and info are non-blocking).
|
|
599
|
+
*/
|
|
600
|
+
export function formatAuditForModel(results) {
|
|
601
|
+
const errors = results.flatMap((r) => r.errors.map((f) => ({ ...f, file: r.filePath })));
|
|
602
|
+
if (errors.length === 0)
|
|
603
|
+
return '';
|
|
604
|
+
const lines = [
|
|
605
|
+
'The following performance anti-patterns were detected in your generated code:',
|
|
606
|
+
'',
|
|
607
|
+
];
|
|
608
|
+
for (const e of errors) {
|
|
609
|
+
lines.push(`[${e.code}] ${e.file}:${e.line} — ${e.message}`);
|
|
610
|
+
lines.push(` Fix: ${e.suggestion}`);
|
|
611
|
+
lines.push('');
|
|
612
|
+
}
|
|
613
|
+
lines.push('Please fix these performance issues using the modify_file tool.');
|
|
614
|
+
return lines.join('\n');
|
|
615
|
+
}
|