minovative-mind-cli 2.5.1 → 2.6.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.
- package/README.md +28 -25
- package/dist/commands/chat.js +1 -1
- package/dist/services/agent/inputHandler.d.ts +9 -0
- package/dist/services/agent/inputHandler.js +34 -0
- package/dist/services/agent/slashCommands.js +158 -37
- package/dist/services/agent/syntaxAgent.d.ts +40 -0
- package/dist/services/agent/syntaxAgent.js +237 -23
- package/dist/services/agent/toolLoop.js +10 -1
- package/dist/services/agent/types.d.ts +1 -0
- package/dist/services/agent-tools.d.ts +156 -1
- package/dist/services/agent-tools.js +259 -67
- package/dist/services/agent.d.ts +74 -0
- package/dist/services/agent.js +192 -30
- package/dist/services/ai.d.ts +5 -0
- package/dist/services/ai.js +80 -87
- package/dist/services/chatHistoryService.d.ts +11 -0
- package/dist/services/chatHistoryService.js +20 -1
- package/dist/services/contextAgent.d.ts +1 -1
- package/dist/services/contextAgent.js +9 -29
- package/dist/services/orchestration/investigationAgent.d.ts +2 -1
- package/dist/services/orchestration/investigationAgent.js +7 -2
- package/dist/services/orchestration/investigationOrchestrator.d.ts +1 -1
- package/dist/services/orchestration/investigationOrchestrator.js +13 -2
- package/dist/services/orchestration/orchestrator.js +21 -4
- package/dist/services/orchestration/subAgent.d.ts +2 -1
- package/dist/services/orchestration/subAgent.js +12 -6
- package/dist/services/workspaceRegistry.d.ts +7 -0
- package/dist/services/workspaceRegistry.js +22 -0
- package/dist/utils/analysisRunner.d.ts +27 -4
- package/dist/utils/analysisRunner.js +100 -20
- package/dist/utils/config.d.ts +2 -0
- package/dist/utils/config.js +2 -0
- package/dist/utils/fuzzyMatch.d.ts +32 -0
- package/dist/utils/fuzzyMatch.js +215 -27
- package/dist/utils/localSyntaxValidator.d.ts +2 -2
- package/dist/utils/localSyntaxValidator.js +280 -81
- package/dist/utils/performanceAuditor.d.ts +2 -7
- package/dist/utils/performanceAuditor.js +541 -89
- package/dist/utils/projectStorage.js +9 -0
- package/dist/utils/systemPrompts.d.ts +3 -2
- package/dist/utils/systemPrompts.js +29 -5
- package/oclif.manifest.json +2 -2
- package/package.json +1 -1
|
@@ -1,20 +1,31 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import pc from 'picocolors';
|
|
3
3
|
const EXTENSION_MAP = {
|
|
4
|
-
'.ts': 'js',
|
|
5
|
-
'.
|
|
4
|
+
'.ts': 'js',
|
|
5
|
+
'.tsx': 'js',
|
|
6
|
+
'.js': 'js',
|
|
7
|
+
'.jsx': 'js',
|
|
8
|
+
'.mjs': 'js',
|
|
9
|
+
'.cjs': 'js',
|
|
10
|
+
'.py': 'python',
|
|
11
|
+
'.pyw': 'python',
|
|
6
12
|
'.go': 'go',
|
|
7
13
|
'.rs': 'rust',
|
|
8
14
|
'.php': 'php',
|
|
15
|
+
'.cs': 'csharp',
|
|
16
|
+
'.java': 'java',
|
|
17
|
+
'.cpp': 'cpp',
|
|
18
|
+
'.hpp': 'cpp',
|
|
19
|
+
'.cc': 'cpp',
|
|
20
|
+
'.cxx': 'cpp',
|
|
21
|
+
'.c': 'cpp',
|
|
22
|
+
'.h': 'cpp',
|
|
23
|
+
'.rb': 'ruby',
|
|
9
24
|
};
|
|
10
25
|
function detectLanguage(filePath) {
|
|
11
26
|
return EXTENSION_MAP[path.extname(filePath).toLowerCase()] ?? 'unknown';
|
|
12
27
|
}
|
|
13
28
|
// ─── Comment & String Stripping ──────────────────────────────────────
|
|
14
|
-
//
|
|
15
|
-
// Before running regex-based detection, we strip string literals and
|
|
16
|
-
// comments from the source to prevent false positives on patterns
|
|
17
|
-
// appearing inside strings or documentation.
|
|
18
29
|
/**
|
|
19
30
|
* Replaces the content of string literals and comments with whitespace,
|
|
20
31
|
* preserving line count so that line-number tracking remains accurate.
|
|
@@ -27,32 +38,77 @@ function stripStringsAndComments(source, lang) {
|
|
|
27
38
|
while (i < source.length) {
|
|
28
39
|
const ch = source[i];
|
|
29
40
|
const next = source[i + 1];
|
|
30
|
-
// ──
|
|
31
|
-
if (
|
|
41
|
+
// ── C-family: line comments (//) ──────────────────────────────
|
|
42
|
+
if (['js', 'go', 'rust', 'php', 'csharp', 'java', 'cpp'].includes(lang) && ch === '/' && next === '/') {
|
|
32
43
|
const eol = source.indexOf('\n', i);
|
|
33
44
|
const end = eol === -1 ? source.length : eol;
|
|
34
45
|
result += ' '.repeat(end - i);
|
|
35
46
|
i = end;
|
|
36
47
|
continue;
|
|
37
48
|
}
|
|
38
|
-
// ──
|
|
39
|
-
if (
|
|
49
|
+
// ── C-family: block comments (/* ... */) ─────────────────────
|
|
50
|
+
if (['js', 'go', 'rust', 'php', 'csharp', 'java', 'cpp'].includes(lang) && ch === '/' && next === '*') {
|
|
40
51
|
const end = source.indexOf('*/', i + 2);
|
|
41
52
|
const closePos = end === -1 ? source.length : end + 2;
|
|
42
|
-
// Preserve newlines for line counting
|
|
43
53
|
const segment = source.substring(i, closePos);
|
|
44
54
|
result += segment.replace(/[^\n]/g, ' ');
|
|
45
55
|
i = closePos;
|
|
46
56
|
continue;
|
|
47
57
|
}
|
|
48
|
-
// ──
|
|
49
|
-
if (
|
|
58
|
+
// ── Hash comment languages: (#) ──────────────────────────────
|
|
59
|
+
if (['python', 'php', 'ruby'].includes(lang) && ch === '#') {
|
|
50
60
|
const eol = source.indexOf('\n', i);
|
|
51
61
|
const end = eol === -1 ? source.length : eol;
|
|
52
62
|
result += ' '.repeat(end - i);
|
|
53
63
|
i = end;
|
|
54
64
|
continue;
|
|
55
65
|
}
|
|
66
|
+
// ── Ruby: block comments (=begin ... =end) ────────────────────
|
|
67
|
+
if (lang === 'ruby' && (i === 0 || source[i - 1] === '\n') && source.substring(i, i + 6) === '=begin') {
|
|
68
|
+
const end = source.indexOf('\n=end', i + 6);
|
|
69
|
+
const closePos = end === -1 ? source.length : end + 5;
|
|
70
|
+
const segment = source.substring(i, closePos);
|
|
71
|
+
result += segment.replace(/[^\n]/g, ' ');
|
|
72
|
+
i = closePos;
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
// ── C#: Raw string literals ("""...""") ────────────────────────
|
|
76
|
+
if (lang === 'csharp' && source.substring(i, i + 3) === '"""') {
|
|
77
|
+
const end = source.indexOf('"""', i + 3);
|
|
78
|
+
const closePos = end === -1 ? source.length : end + 3;
|
|
79
|
+
const segment = source.substring(i, closePos);
|
|
80
|
+
result += segment.replace(/[^\n]/g, ' ');
|
|
81
|
+
i = closePos;
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
// ── C#: Verbatim string literals (@"...") ─────────────────────
|
|
85
|
+
if (lang === 'csharp' && ch === '@' && next === '"') {
|
|
86
|
+
let j = i + 2;
|
|
87
|
+
while (j < source.length) {
|
|
88
|
+
if (source[j] === '"' && source[j + 1] === '"') {
|
|
89
|
+
j += 2;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (source[j] === '"') {
|
|
93
|
+
j++;
|
|
94
|
+
break;
|
|
95
|
+
}
|
|
96
|
+
j++;
|
|
97
|
+
}
|
|
98
|
+
const segment = source.substring(i, j);
|
|
99
|
+
result += segment.replace(/[^\n]/g, ' ');
|
|
100
|
+
i = j;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
// ── C++: Raw string literals R"(...)" ─────────────────────────
|
|
104
|
+
if (lang === 'cpp' && ch === 'R' && next === '"') {
|
|
105
|
+
const end = source.indexOf(')"', i + 2);
|
|
106
|
+
const closePos = end === -1 ? source.length : end + 2;
|
|
107
|
+
const segment = source.substring(i, closePos);
|
|
108
|
+
result += segment.replace(/[^\n]/g, ' ');
|
|
109
|
+
i = closePos;
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
56
112
|
// ── Python: triple-quoted strings ─────────────────────────
|
|
57
113
|
if (lang === 'python' && (source.substring(i, i + 3) === '"""' || source.substring(i, i + 3) === "'''")) {
|
|
58
114
|
const quote = source.substring(i, i + 3);
|
|
@@ -67,7 +123,6 @@ function stripStringsAndComments(source, lang) {
|
|
|
67
123
|
if (lang === 'php' && ch === '<' && next === '<' && source[i + 2] === '<') {
|
|
68
124
|
const eol = source.indexOf('\n', i);
|
|
69
125
|
if (eol !== -1) {
|
|
70
|
-
// e.g. <<<EOF or <<<'EOF'
|
|
71
126
|
const idMatch = source.substring(i + 3, eol).match(/[A-Za-z0-9_]+/);
|
|
72
127
|
if (idMatch) {
|
|
73
128
|
const identifier = idMatch[0];
|
|
@@ -83,26 +138,24 @@ function stripStringsAndComments(source, lang) {
|
|
|
83
138
|
}
|
|
84
139
|
}
|
|
85
140
|
}
|
|
86
|
-
// ── All languages: string literals
|
|
141
|
+
// ── All languages: generic string literals ────────────────────
|
|
87
142
|
if (ch === '"' || ch === "'" || (lang === 'js' && ch === '`')) {
|
|
88
143
|
const quote = ch;
|
|
89
144
|
let j = i + 1;
|
|
90
145
|
while (j < source.length) {
|
|
91
146
|
if (source[j] === '\\') {
|
|
92
|
-
j += 2;
|
|
147
|
+
j += 2;
|
|
93
148
|
continue;
|
|
94
149
|
}
|
|
95
150
|
if (source[j] === quote) {
|
|
96
151
|
j++;
|
|
97
152
|
break;
|
|
98
153
|
}
|
|
99
|
-
// Template literals can span multiple lines
|
|
100
154
|
if (quote === '`' && source[j] === '\n') {
|
|
101
155
|
result += ' '.repeat(j - i);
|
|
102
156
|
result += '\n';
|
|
103
157
|
i = j + 1;
|
|
104
158
|
j = i;
|
|
105
|
-
// Reset — we'll pick up again from the new line
|
|
106
159
|
continue;
|
|
107
160
|
}
|
|
108
161
|
j++;
|
|
@@ -117,10 +170,83 @@ function stripStringsAndComments(source, lang) {
|
|
|
117
170
|
}
|
|
118
171
|
return result;
|
|
119
172
|
}
|
|
120
|
-
// ───
|
|
173
|
+
// ─── Directives Handling (perf-ignore) ────────────────────────────────
|
|
121
174
|
/**
|
|
122
|
-
*
|
|
175
|
+
* Filters out findings that are suppressed via comment directives in raw source.
|
|
176
|
+
* Supported directives:
|
|
177
|
+
* // perf-ignore
|
|
178
|
+
* // perf-ignore: PERF-001, PERF-009
|
|
179
|
+
* // perf-ignore-file
|
|
180
|
+
* // perf-ignore-file: PERF-003
|
|
181
|
+
* Works with //, #, /* ... * /, -- comment styles across languages.
|
|
123
182
|
*/
|
|
183
|
+
function applyDirectiveSuppression(findings, rawSource) {
|
|
184
|
+
if (findings.length === 0)
|
|
185
|
+
return findings;
|
|
186
|
+
const lines = rawSource.split('\n');
|
|
187
|
+
const fileIgnoredCodes = new Set();
|
|
188
|
+
let ignoreAllFile = false;
|
|
189
|
+
const lineIgnoredCodesMap = new Map();
|
|
190
|
+
const directiveRegex = /(?:\/\/|#|\/\*|--)\s*perf-ignore(-file)?(?:[:\s]+([A-Za-z0-9_,\s-]+))?/i;
|
|
191
|
+
for (let idx = 0; idx < lines.length; idx++) {
|
|
192
|
+
const lineNum = idx + 1;
|
|
193
|
+
const lineStr = lines[idx];
|
|
194
|
+
const match = lineStr.match(directiveRegex);
|
|
195
|
+
if (!match)
|
|
196
|
+
continue;
|
|
197
|
+
const isFileLevel = Boolean(match[1]);
|
|
198
|
+
const codesRaw = match[2]?.trim();
|
|
199
|
+
const parsedCodes = codesRaw
|
|
200
|
+
? codesRaw
|
|
201
|
+
.split(/[\s,]+/)
|
|
202
|
+
.map((c) => c.trim().toUpperCase())
|
|
203
|
+
.filter(Boolean)
|
|
204
|
+
: [];
|
|
205
|
+
if (isFileLevel) {
|
|
206
|
+
if (parsedCodes.length === 0) {
|
|
207
|
+
ignoreAllFile = true;
|
|
208
|
+
}
|
|
209
|
+
else {
|
|
210
|
+
for (const code of parsedCodes) {
|
|
211
|
+
fileIgnoredCodes.add(code);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
// Line-level directive suppresses findings on the directive line AND the following line
|
|
217
|
+
const targetLines = [lineNum, lineNum + 1];
|
|
218
|
+
for (const targetLine of targetLines) {
|
|
219
|
+
let lineCodes = lineIgnoredCodesMap.get(targetLine);
|
|
220
|
+
if (!lineCodes) {
|
|
221
|
+
lineCodes = new Set();
|
|
222
|
+
lineIgnoredCodesMap.set(targetLine, lineCodes);
|
|
223
|
+
}
|
|
224
|
+
if (parsedCodes.length === 0) {
|
|
225
|
+
lineCodes.add('*');
|
|
226
|
+
}
|
|
227
|
+
else {
|
|
228
|
+
for (const code of parsedCodes) {
|
|
229
|
+
lineCodes.add(code);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
if (ignoreAllFile)
|
|
236
|
+
return [];
|
|
237
|
+
return findings.filter((finding) => {
|
|
238
|
+
const code = finding.code.toUpperCase();
|
|
239
|
+
if (fileIgnoredCodes.has(code))
|
|
240
|
+
return false;
|
|
241
|
+
const lineCodes = lineIgnoredCodesMap.get(finding.line);
|
|
242
|
+
if (lineCodes) {
|
|
243
|
+
if (lineCodes.has('*') || lineCodes.has(code))
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
return true;
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
// ─── Utility: Line Number Lookup ─────────────────────────────────────
|
|
124
250
|
function getLineNumber(source, offset) {
|
|
125
251
|
let line = 1;
|
|
126
252
|
for (let i = 0; i < offset && i < source.length; i++) {
|
|
@@ -130,16 +256,15 @@ function getLineNumber(source, offset) {
|
|
|
130
256
|
return line;
|
|
131
257
|
}
|
|
132
258
|
// ── PERF-001: Nested Loop Detection (O(n²) risk) ────────────────────
|
|
133
|
-
//
|
|
134
|
-
// Strategy: Track brace-depth to identify loop constructs that contain
|
|
135
|
-
// other loop constructs. Works across all C-family languages + Python.
|
|
136
259
|
const detectNestedLoops = (source, stripped, lang, findings) => {
|
|
137
|
-
// Python uses indentation, not braces — use a different strategy
|
|
138
260
|
if (lang === 'python') {
|
|
139
261
|
detectNestedLoopsPython(source, stripped, findings);
|
|
140
262
|
return;
|
|
141
263
|
}
|
|
142
|
-
|
|
264
|
+
if (lang === 'ruby') {
|
|
265
|
+
detectNestedLoopsRuby(source, stripped, findings);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
143
268
|
const loopPattern = /\b(for|foreach|while|do)\s*[\s(]|\.(forEach|map|filter|reduce|flatMap)\s*\(/g;
|
|
144
269
|
const lines = stripped.split('\n');
|
|
145
270
|
for (let i = 0; i < lines.length; i++) {
|
|
@@ -149,7 +274,6 @@ const detectNestedLoops = (source, stripped, lang, findings) => {
|
|
|
149
274
|
continue;
|
|
150
275
|
}
|
|
151
276
|
loopPattern.lastIndex = 0;
|
|
152
|
-
// Found a loop — scan forward tracking brace depth to find nested loops
|
|
153
277
|
let depth = 0;
|
|
154
278
|
let foundOpening = false;
|
|
155
279
|
for (let j = i; j < lines.length; j++) {
|
|
@@ -163,20 +287,24 @@ const detectNestedLoops = (source, stripped, lang, findings) => {
|
|
|
163
287
|
depth--;
|
|
164
288
|
}
|
|
165
289
|
}
|
|
166
|
-
// Check lines inside the outer loop body for nested loops
|
|
167
290
|
if (j > i && foundOpening && depth > 0) {
|
|
291
|
+
// Context-aware heuristic: Ignore nested loops over small constant literals/bounds (e.g. i < 2, ['a', 'b'])
|
|
168
292
|
if (/\b(for|foreach|while|do)\s*[\s(]|\.(forEach|map|filter|reduce|flatMap)\s*\(/.test(innerLine)) {
|
|
293
|
+
if (/for\s*\([^;]*;\s*[\w$]+\s*<=\s*([0-9]|10)\b/.test(innerLine) ||
|
|
294
|
+
/for\s*\([^;]*;\s*[\w$]+\s*<\s*([0-9]|10)\b/.test(innerLine) ||
|
|
295
|
+
/\[\s*(?:\d+|'[^']+'|"[^"]+")\s*,\s*(?:\d+|'[^']+'|"[^"]+")\s*\]/.test(innerLine)) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
169
298
|
findings.push({
|
|
170
299
|
severity: 'WARNING',
|
|
171
300
|
code: 'PERF-001',
|
|
172
301
|
line: j + 1,
|
|
173
302
|
message: `Nested loop detected (outer loop at line ${i + 1}) — O(n²) or worse complexity.`,
|
|
174
|
-
suggestion: 'Consider using a Map/Set for O(1) lookups, or restructure to avoid nested iteration.',
|
|
303
|
+
suggestion: 'Consider using a Map/Set/Hash for O(1) lookups, or restructure to avoid nested iteration.',
|
|
175
304
|
});
|
|
176
|
-
break;
|
|
305
|
+
break;
|
|
177
306
|
}
|
|
178
307
|
}
|
|
179
|
-
// Outer loop body has closed
|
|
180
308
|
if (foundOpening && depth <= 0)
|
|
181
309
|
break;
|
|
182
310
|
}
|
|
@@ -192,22 +320,52 @@ function detectNestedLoopsPython(_source, stripped, findings) {
|
|
|
192
320
|
const outerIndent = lines[i].search(/\S/);
|
|
193
321
|
if (outerIndent < 0)
|
|
194
322
|
continue;
|
|
195
|
-
// Scan forward for a nested loop with deeper indentation
|
|
196
323
|
for (let j = i + 1; j < lines.length; j++) {
|
|
197
324
|
const innerLine = lines[j];
|
|
198
325
|
if (innerLine.trim() === '')
|
|
199
326
|
continue;
|
|
200
327
|
const innerIndent = innerLine.search(/\S/);
|
|
201
|
-
// If we've returned to same or lesser indent, the outer loop body is over
|
|
202
328
|
if (innerIndent <= outerIndent)
|
|
203
329
|
break;
|
|
204
330
|
if (loopPattern.test(innerLine)) {
|
|
331
|
+
if (/\brange\s*\(\s*([0-9]|10)\s*\)/.test(innerLine))
|
|
332
|
+
continue;
|
|
333
|
+
findings.push({
|
|
334
|
+
severity: 'WARNING',
|
|
335
|
+
code: 'PERF-001',
|
|
336
|
+
line: j + 1,
|
|
337
|
+
message: `Nested loop detected (outer loop at line ${i + 1}) — O(n²) or worse complexity.`,
|
|
338
|
+
suggestion: 'Consider using a dictionary/set for O(1) lookups, or restructure to avoid nested iteration.',
|
|
339
|
+
});
|
|
340
|
+
break;
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
function detectNestedLoopsRuby(_source, stripped, findings) {
|
|
346
|
+
const lines = stripped.split('\n');
|
|
347
|
+
const loopPattern = /\b(while|until|for)\b|\.(each|times|map|select|loop)\b/;
|
|
348
|
+
for (let i = 0; i < lines.length; i++) {
|
|
349
|
+
const outerLine = lines[i];
|
|
350
|
+
if (!loopPattern.test(outerLine))
|
|
351
|
+
continue;
|
|
352
|
+
const outerIndent = outerLine.search(/\S/);
|
|
353
|
+
if (outerIndent < 0)
|
|
354
|
+
continue;
|
|
355
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
356
|
+
const innerLine = lines[j];
|
|
357
|
+
if (innerLine.trim() === '')
|
|
358
|
+
continue;
|
|
359
|
+
const innerIndent = innerLine.search(/\S/);
|
|
360
|
+
if (innerIndent <= outerIndent && !/^\s*(end|\}|\))\b/.test(innerLine))
|
|
361
|
+
break;
|
|
362
|
+
if (loopPattern.test(innerLine) && innerIndent > outerIndent) {
|
|
205
363
|
findings.push({
|
|
206
364
|
severity: 'WARNING',
|
|
207
365
|
code: 'PERF-001',
|
|
208
366
|
line: j + 1,
|
|
209
367
|
message: `Nested loop detected (outer loop at line ${i + 1}) — O(n²) or worse complexity.`,
|
|
210
|
-
suggestion: 'Consider using a
|
|
368
|
+
suggestion: 'Consider using a Hash for O(1) lookups or flat map operations.',
|
|
211
369
|
});
|
|
212
370
|
break;
|
|
213
371
|
}
|
|
@@ -218,10 +376,7 @@ function detectNestedLoopsPython(_source, stripped, findings) {
|
|
|
218
376
|
const detectChainedArrayMethods = (_source, stripped, lang, findings) => {
|
|
219
377
|
if (lang !== 'js')
|
|
220
378
|
return;
|
|
221
|
-
|
|
222
|
-
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;
|
|
223
|
-
const lines = stripped.split('\n');
|
|
224
|
-
// Search across joined lines since chains can span multiple lines
|
|
379
|
+
const chainPattern = /\.(map|filter|reduce|flatMap|flat|sort|slice|concat)\s*\([\s\S]*?\)\s*\.(map|filter|reduce|flatMap|flat|sort|slice|concat)\s*\([\s\S]*?\)\s*\.(map|filter|reduce|flatMap|flat|sort|slice|concat)/g;
|
|
225
380
|
const joined = stripped;
|
|
226
381
|
let match;
|
|
227
382
|
while ((match = chainPattern.exec(joined)) !== null) {
|
|
@@ -240,10 +395,21 @@ const detectSyncIOInAsync = (_source, stripped, lang, findings) => {
|
|
|
240
395
|
if (lang !== 'js')
|
|
241
396
|
return;
|
|
242
397
|
const syncAPIs = [
|
|
243
|
-
'readFileSync',
|
|
244
|
-
'
|
|
245
|
-
'
|
|
246
|
-
'
|
|
398
|
+
'readFileSync',
|
|
399
|
+
'writeFileSync',
|
|
400
|
+
'appendFileSync',
|
|
401
|
+
'mkdirSync',
|
|
402
|
+
'readdirSync',
|
|
403
|
+
'statSync',
|
|
404
|
+
'existsSync',
|
|
405
|
+
'copyFileSync',
|
|
406
|
+
'renameSync',
|
|
407
|
+
'unlinkSync',
|
|
408
|
+
'rmdirSync',
|
|
409
|
+
'accessSync',
|
|
410
|
+
'execSync',
|
|
411
|
+
'spawnSync',
|
|
412
|
+
'execFileSync',
|
|
247
413
|
];
|
|
248
414
|
const lines = stripped.split('\n');
|
|
249
415
|
let insideAsync = false;
|
|
@@ -251,7 +417,6 @@ const detectSyncIOInAsync = (_source, stripped, lang, findings) => {
|
|
|
251
417
|
let braceDepth = 0;
|
|
252
418
|
for (let i = 0; i < lines.length; i++) {
|
|
253
419
|
const line = lines[i];
|
|
254
|
-
// Detect async function/method/arrow entry
|
|
255
420
|
if (/\basync\s+(function\b|\w+\s*\(|(\w+)\s*=>|\(.*\)\s*=>)/.test(line) || /\basync\s+\w+\s*\(/.test(line)) {
|
|
256
421
|
insideAsync = true;
|
|
257
422
|
asyncBraceDepth = braceDepth;
|
|
@@ -309,7 +474,6 @@ const detectSpreadInLoops = (_source, stripped, lang, findings) => {
|
|
|
309
474
|
}
|
|
310
475
|
}
|
|
311
476
|
if (loopDepth > 0 && /\.\.\.[\w$]/.test(line)) {
|
|
312
|
-
// Ignore rest parameters in function definitions: function(...args) or (...args) =>
|
|
313
477
|
if (!/function\s*\([^)]*\.\.\.[\w$]/.test(line) && !/\([^)]*\.\.\.[\w$][^)]*\)\s*=>/.test(line)) {
|
|
314
478
|
findings.push({
|
|
315
479
|
severity: 'WARNING',
|
|
@@ -327,21 +491,17 @@ const detectMissingCleanup = (source, stripped, lang, findings) => {
|
|
|
327
491
|
if (lang !== 'js' && lang !== 'python')
|
|
328
492
|
return;
|
|
329
493
|
if (lang === 'js') {
|
|
330
|
-
// Look for createReadStream/createWriteStream/createServer assigned to a variable
|
|
331
494
|
const pattern = /\b(const|let|var)\s+(\w+)\s*=\s*.*\b(createReadStream|createWriteStream|createServer|createConnection|net\.connect|tls\.connect)\b/g;
|
|
332
495
|
let match;
|
|
333
496
|
while ((match = pattern.exec(stripped)) !== null) {
|
|
334
497
|
const varName = match[2];
|
|
335
498
|
const creator = match[3];
|
|
336
499
|
const line = getLineNumber(stripped, match.index);
|
|
337
|
-
// Check if .close(), .destroy(), .end(), or .disconnect() is called on this variable
|
|
338
|
-
// within a reasonable scope (the rest of the file)
|
|
339
500
|
const rest = stripped.substring(match.index);
|
|
340
501
|
const hasCleanup = rest.includes(`${varName}.close()`) ||
|
|
341
502
|
rest.includes(`${varName}.destroy()`) ||
|
|
342
503
|
rest.includes(`${varName}.end()`) ||
|
|
343
504
|
rest.includes(`${varName}.disconnect()`) ||
|
|
344
|
-
// Also accept piping (which handles cleanup) or 'using' declarations
|
|
345
505
|
rest.includes(`${varName}.pipe(`) ||
|
|
346
506
|
stripped.includes(`using ${varName}`);
|
|
347
507
|
if (!hasCleanup) {
|
|
@@ -356,11 +516,9 @@ const detectMissingCleanup = (source, stripped, lang, findings) => {
|
|
|
356
516
|
}
|
|
357
517
|
}
|
|
358
518
|
if (lang === 'python') {
|
|
359
|
-
// Detect open() not inside a 'with' statement
|
|
360
519
|
const lines = stripped.split('\n');
|
|
361
520
|
for (let i = 0; i < lines.length; i++) {
|
|
362
521
|
const line = lines[i];
|
|
363
|
-
// Match: var = open(...) but NOT: with open(...) as var
|
|
364
522
|
if (/=\s*open\s*\(/.test(line) && !/\bwith\b/.test(line)) {
|
|
365
523
|
findings.push({
|
|
366
524
|
severity: 'WARNING',
|
|
@@ -379,7 +537,6 @@ const detectDangerousEval = (_source, stripped, lang, findings) => {
|
|
|
379
537
|
for (let i = 0; i < lines.length; i++) {
|
|
380
538
|
const line = lines[i];
|
|
381
539
|
if (lang === 'js') {
|
|
382
|
-
// Match eval(...) but not .addEventListener('...', eval)
|
|
383
540
|
if (/\beval\s*\(/.test(line)) {
|
|
384
541
|
findings.push({
|
|
385
542
|
severity: 'ERROR',
|
|
@@ -444,14 +601,10 @@ const detectUnsafeJsonParse = (_source, stripped, lang, findings) => {
|
|
|
444
601
|
insideAsync = false;
|
|
445
602
|
}
|
|
446
603
|
}
|
|
447
|
-
// Detect JSON.parse(req.body) / JSON.parse(body) / JSON.parse(data) etc.
|
|
448
|
-
// in what appears to be a request handler context
|
|
449
604
|
if (/JSON\.parse\s*\(/.test(line)) {
|
|
450
|
-
// Check if this is inside a try block — if so, it's handled
|
|
451
605
|
const prevLines = lines.slice(Math.max(0, i - 5), i).join('\n');
|
|
452
606
|
if (/\btry\s*\{/.test(prevLines))
|
|
453
607
|
continue;
|
|
454
|
-
// Check if the argument looks like an external input
|
|
455
608
|
const argMatch = line.match(/JSON\.parse\s*\(\s*(\w+)/);
|
|
456
609
|
if (argMatch) {
|
|
457
610
|
const argName = argMatch[1].toLowerCase();
|
|
@@ -475,13 +628,9 @@ const detectUnboundedFetch = (_source, stripped, lang, findings) => {
|
|
|
475
628
|
for (let i = 0; i < lines.length; i++) {
|
|
476
629
|
const line = lines[i];
|
|
477
630
|
if (lang === 'js') {
|
|
478
|
-
// Detect .find() / .findMany() / .select() / .query() without .limit() / .take() / .paginate()
|
|
479
|
-
// Common in Prisma, Mongoose, Knex, Sequelize. Uses negative lookahead to ignore array methods like .find(x => ...) or .find(function(...) ...)
|
|
480
631
|
if (/\.(find|findMany|findAll|select|query)\s*\(\s*(?!(\w+|\([^)]*\))\s*=>|function\b)/.test(line)) {
|
|
481
|
-
// Look ahead a few lines for a .limit() / .take() / .skip() / .paginate() / .first()
|
|
482
632
|
const window = lines.slice(i, Math.min(i + 5, lines.length)).join('\n');
|
|
483
|
-
if (!/\.(limit|take|first|paginate|skip|offset|top)\s*\(/.test(window) &&
|
|
484
|
-
!/\bLIMIT\b/i.test(window)) {
|
|
633
|
+
if (!/\.(limit|take|first|paginate|skip|offset|top)\s*\(/.test(window) && !/\bLIMIT\b/i.test(window)) {
|
|
485
634
|
findings.push({
|
|
486
635
|
severity: 'WARNING',
|
|
487
636
|
code: 'PERF-008',
|
|
@@ -493,7 +642,6 @@ const detectUnboundedFetch = (_source, stripped, lang, findings) => {
|
|
|
493
642
|
}
|
|
494
643
|
}
|
|
495
644
|
if (lang === 'python') {
|
|
496
|
-
// Detect .all() / .filter() on Django/SQLAlchemy querysets without [:N] or .limit()
|
|
497
645
|
if (/\.(all|filter|objects\.filter|objects\.all)\s*\(/.test(line)) {
|
|
498
646
|
const window = lines.slice(i, Math.min(i + 5, lines.length)).join('\n');
|
|
499
647
|
if (!/\.(limit|first|paginate|count)\s*\(/.test(window) &&
|
|
@@ -512,8 +660,7 @@ const detectUnboundedFetch = (_source, stripped, lang, findings) => {
|
|
|
512
660
|
if (lang === 'php') {
|
|
513
661
|
if (/\.(all|get|findAll|query)\s*\(|::all\s*\(/.test(line)) {
|
|
514
662
|
const window = lines.slice(i, Math.min(i + 5, lines.length)).join('\n');
|
|
515
|
-
if (!/\.(limit|take|paginate|offset)\s*\(/.test(window) &&
|
|
516
|
-
!/\bLIMIT\b/i.test(window)) {
|
|
663
|
+
if (!/\.(limit|take|paginate|offset)\s*\(/.test(window) && !/\bLIMIT\b/i.test(window)) {
|
|
517
664
|
findings.push({
|
|
518
665
|
severity: 'WARNING',
|
|
519
666
|
code: 'PERF-008',
|
|
@@ -524,10 +671,36 @@ const detectUnboundedFetch = (_source, stripped, lang, findings) => {
|
|
|
524
671
|
}
|
|
525
672
|
}
|
|
526
673
|
}
|
|
527
|
-
|
|
528
|
-
|
|
674
|
+
if (lang === 'ruby') {
|
|
675
|
+
if (/\.(all|where)\b/.test(line) && !/\.(limit|find_each|first|take|paginate)\b/.test(line)) {
|
|
676
|
+
const window = lines.slice(i, Math.min(i + 5, lines.length)).join('\n');
|
|
677
|
+
if (!/\.(limit|find_each|first|take|paginate)\b/.test(window) && !/\bLIMIT\b/i.test(window)) {
|
|
678
|
+
findings.push({
|
|
679
|
+
severity: 'WARNING',
|
|
680
|
+
code: 'PERF-008',
|
|
681
|
+
line: i + 1,
|
|
682
|
+
message: 'ActiveRecord query without .limit() or .find_each — may fetch unbounded rows into memory.',
|
|
683
|
+
suggestion: 'Add .limit(N) or use .find_each for batch processing.',
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
if (lang === 'csharp' || lang === 'java') {
|
|
689
|
+
if (/\.(ToList|ToArray|findAll|Query)\s*\(/.test(line)) {
|
|
690
|
+
const window = lines.slice(i, Math.min(i + 5, lines.length)).join('\n');
|
|
691
|
+
if (!/\.(Take|Limit|Page|FirstOrDefault)\s*\(/.test(window) && !/\bLIMIT\b/i.test(window)) {
|
|
692
|
+
findings.push({
|
|
693
|
+
severity: 'WARNING',
|
|
694
|
+
code: 'PERF-008',
|
|
695
|
+
line: i + 1,
|
|
696
|
+
message: 'ORM/Collection query materialization without Take/Limit — may load entire dataset into memory.',
|
|
697
|
+
suggestion: 'Apply .Take(N) or pagination before calling materialization methods.',
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (lang === 'go' || lang === 'rust' || lang === 'cpp') {
|
|
529
703
|
if (/SELECT\s+/i.test(line) && !/\bLIMIT\b/i.test(line)) {
|
|
530
|
-
// Look ahead for LIMIT on subsequent lines
|
|
531
704
|
const window = lines.slice(i, Math.min(i + 5, lines.length)).join('\n');
|
|
532
705
|
if (!/\bLIMIT\b/i.test(window)) {
|
|
533
706
|
findings.push({
|
|
@@ -570,7 +743,6 @@ const detectPhpInLoopOperations = (_source, stripped, lang, findings) => {
|
|
|
570
743
|
}
|
|
571
744
|
}
|
|
572
745
|
if (loopDepth > 0) {
|
|
573
|
-
// PERF-PHP1: array_merge
|
|
574
746
|
if (/\barray_merge\s*\(/.test(line)) {
|
|
575
747
|
findings.push({
|
|
576
748
|
severity: 'WARNING',
|
|
@@ -580,7 +752,6 @@ const detectPhpInLoopOperations = (_source, stripped, lang, findings) => {
|
|
|
580
752
|
suggestion: 'Append to array with $arr[] = $val and merge once outside the loop, or use the splat operator.',
|
|
581
753
|
});
|
|
582
754
|
}
|
|
583
|
-
// PERF-PHP2: N+1 queries
|
|
584
755
|
if (/(->get\(|->find\(|->query\(|->findAll\(|::find\(|DB::|->execute\()/.test(line)) {
|
|
585
756
|
findings.push({
|
|
586
757
|
severity: 'WARNING',
|
|
@@ -590,7 +761,6 @@ const detectPhpInLoopOperations = (_source, stripped, lang, findings) => {
|
|
|
590
761
|
suggestion: 'Pre-fetch data outside the loop using eager loading (e.g., with()) or a single IN() query.',
|
|
591
762
|
});
|
|
592
763
|
}
|
|
593
|
-
// PERF-PHP3: Blocking I/O
|
|
594
764
|
if (/\b(file_get_contents|curl_exec|fopen)\s*\(/.test(line)) {
|
|
595
765
|
findings.push({
|
|
596
766
|
severity: 'WARNING',
|
|
@@ -612,12 +782,20 @@ const detectSequentialAwait = (_source, stripped, lang, findings) => {
|
|
|
612
782
|
let braceDepth = 0;
|
|
613
783
|
let inLoop = false;
|
|
614
784
|
let loopBraceStart = 0;
|
|
785
|
+
let isPaginationOrRetryLoop = false;
|
|
615
786
|
for (let i = 0; i < lines.length; i++) {
|
|
616
787
|
const line = lines[i];
|
|
617
|
-
|
|
788
|
+
const isLoopHeader = /\b(for|while|do)\s*[\s(]/.test(line) || /\.(forEach|map|filter|reduce)\s*\(/.test(line);
|
|
789
|
+
if (isLoopHeader && !inLoop) {
|
|
618
790
|
inLoop = true;
|
|
619
791
|
loopBraceStart = braceDepth;
|
|
620
792
|
loopDepth++;
|
|
793
|
+
if (/\b(cursor|hasMore|nextPage|hasNext|page|retry|attempt|backoff)\b/i.test(line)) {
|
|
794
|
+
isPaginationOrRetryLoop = true;
|
|
795
|
+
}
|
|
796
|
+
else {
|
|
797
|
+
isPaginationOrRetryLoop = false;
|
|
798
|
+
}
|
|
621
799
|
}
|
|
622
800
|
for (const ch of line) {
|
|
623
801
|
if (ch === '{')
|
|
@@ -627,10 +805,27 @@ const detectSequentialAwait = (_source, stripped, lang, findings) => {
|
|
|
627
805
|
if (inLoop && braceDepth <= loopBraceStart) {
|
|
628
806
|
inLoop = false;
|
|
629
807
|
loopDepth--;
|
|
808
|
+
isPaginationOrRetryLoop = false;
|
|
630
809
|
}
|
|
631
810
|
}
|
|
632
811
|
}
|
|
633
812
|
if (loopDepth > 0 && /\bawait\s+/.test(line)) {
|
|
813
|
+
// Context-aware heuristic 1: Concurrent execution patterns
|
|
814
|
+
if (/\bawait\s+Promise\.(all|allSettled|race|any)\b/.test(line))
|
|
815
|
+
continue;
|
|
816
|
+
// Context-aware heuristic 2: Delays / sleep / retries
|
|
817
|
+
if (/\bawait\s+(delay|sleep|wait|pause|setTimeout)\b/i.test(line) ||
|
|
818
|
+
/\bawait\s+new\s+Promise\s*\(\s*\(?resolve\)?\s*=>\s*setTimeout/.test(line)) {
|
|
819
|
+
continue;
|
|
820
|
+
}
|
|
821
|
+
// Context-aware heuristic 3: Cursor/pagination or retry loop body
|
|
822
|
+
if (isPaginationOrRetryLoop || /\b(cursor|hasMore|nextPage|hasNext|page|token)\b/i.test(line)) {
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
// Context-aware heuristic 4: Stream/reader consumption
|
|
826
|
+
if (/\b(reader\.read|stream|readline|rl|iterator\.next|gen\.next)\b/i.test(line)) {
|
|
827
|
+
continue;
|
|
828
|
+
}
|
|
634
829
|
findings.push({
|
|
635
830
|
severity: 'WARNING',
|
|
636
831
|
code: 'PERF-009',
|
|
@@ -757,12 +952,12 @@ const detectPythonInLoopOperations = (_source, stripped, lang, findings) => {
|
|
|
757
952
|
continue;
|
|
758
953
|
}
|
|
759
954
|
if (inLoop && indent > outerIndent) {
|
|
760
|
-
if (
|
|
955
|
+
if (/\b[a-zA-Z_]\w*\s*\+=/.test(line)) {
|
|
761
956
|
findings.push({
|
|
762
957
|
severity: 'WARNING',
|
|
763
958
|
code: 'PERF-PY1',
|
|
764
959
|
line: i + 1,
|
|
765
|
-
message: 'String concatenation (+=) inside a loop — Python strings are immutable, causing O(n²) memory allocations.',
|
|
960
|
+
message: 'String/collection concatenation (+=) inside a loop — Python strings are immutable, causing O(n²) memory allocations.',
|
|
766
961
|
suggestion: 'Append to a list and use "".join() outside the loop instead.',
|
|
767
962
|
});
|
|
768
963
|
}
|
|
@@ -815,7 +1010,7 @@ const detectGoInLoopOperations = (_source, stripped, lang, findings) => {
|
|
|
815
1010
|
suggestion: 'Wrap the loop body in an anonymous function (func() { ... }()) or close resources manually.',
|
|
816
1011
|
});
|
|
817
1012
|
}
|
|
818
|
-
if (/\b[a-zA-Z_]\w*\s
|
|
1013
|
+
if (/\b[a-zA-Z_]\w*\s*\+=/.test(line)) {
|
|
819
1014
|
findings.push({
|
|
820
1015
|
severity: 'WARNING',
|
|
821
1016
|
code: 'PERF-GO2',
|
|
@@ -876,6 +1071,268 @@ const detectRustInLoopOperations = (_source, stripped, lang, findings) => {
|
|
|
876
1071
|
}
|
|
877
1072
|
}
|
|
878
1073
|
};
|
|
1074
|
+
// ── PERF-CS1/2/3: C# Specific Loop Operations ──────────────────────────
|
|
1075
|
+
const detectCsharpInLoopOperations = (source, stripped, lang, findings) => {
|
|
1076
|
+
if (lang !== 'csharp')
|
|
1077
|
+
return;
|
|
1078
|
+
const lines = stripped.split('\n');
|
|
1079
|
+
const rawLines = source.split('\n');
|
|
1080
|
+
let loopDepth = 0;
|
|
1081
|
+
let braceDepth = 0;
|
|
1082
|
+
let inLoop = false;
|
|
1083
|
+
let loopBraceStart = 0;
|
|
1084
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1085
|
+
const line = lines[i];
|
|
1086
|
+
const rawLine = rawLines[i];
|
|
1087
|
+
if (/\b(for|foreach|while|do)\s*[\s(]/.test(line) && !inLoop) {
|
|
1088
|
+
inLoop = true;
|
|
1089
|
+
loopBraceStart = braceDepth;
|
|
1090
|
+
loopDepth++;
|
|
1091
|
+
}
|
|
1092
|
+
for (const ch of line) {
|
|
1093
|
+
if (ch === '{')
|
|
1094
|
+
braceDepth++;
|
|
1095
|
+
else if (ch === '}') {
|
|
1096
|
+
braceDepth--;
|
|
1097
|
+
if (inLoop && braceDepth <= loopBraceStart) {
|
|
1098
|
+
inLoop = false;
|
|
1099
|
+
loopDepth--;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
if (loopDepth > 0) {
|
|
1104
|
+
// PERF-CS1: LINQ in loop
|
|
1105
|
+
if (/\.(Where|Select|ToList|ToArray|FirstOrDefault|Count)\s*\(/.test(line)) {
|
|
1106
|
+
findings.push({
|
|
1107
|
+
severity: 'WARNING',
|
|
1108
|
+
code: 'PERF-CS1',
|
|
1109
|
+
line: i + 1,
|
|
1110
|
+
message: 'LINQ query method inside loop — allocates delegates and iterators on every iteration.',
|
|
1111
|
+
suggestion: 'Pre-compute or pull LINQ queries outside the loop.',
|
|
1112
|
+
});
|
|
1113
|
+
}
|
|
1114
|
+
// PERF-CS2: String concatenation in loop
|
|
1115
|
+
if (/\b[a-zA-Z_]\w*\s*\+=/.test(line)) {
|
|
1116
|
+
findings.push({
|
|
1117
|
+
severity: 'WARNING',
|
|
1118
|
+
code: 'PERF-CS2',
|
|
1119
|
+
line: i + 1,
|
|
1120
|
+
message: 'String concatenation (+=) inside loop — allocates new string objects repeatedly.',
|
|
1121
|
+
suggestion: 'Use System.Text.StringBuilder for efficient string construction.',
|
|
1122
|
+
});
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
// PERF-CS3: Sync-over-async blocking
|
|
1126
|
+
if (/\.(Result|Wait\(\)|GetAwaiter\(\)\.GetResult\(\))/.test(rawLine)) {
|
|
1127
|
+
findings.push({
|
|
1128
|
+
severity: 'ERROR',
|
|
1129
|
+
code: 'PERF-CS3',
|
|
1130
|
+
line: i + 1,
|
|
1131
|
+
message: 'Sync-over-async blocking (.Result / .Wait()) — causes threadpool starvation and potential deadlocks.',
|
|
1132
|
+
suggestion: 'Use await instead of synchronous blocking on Task objects.',
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
};
|
|
1137
|
+
// ── PERF-JV1/2/3: Java Specific Loop Operations ────────────────────────
|
|
1138
|
+
const detectJavaInLoopOperations = (source, stripped, lang, findings) => {
|
|
1139
|
+
if (lang !== 'java')
|
|
1140
|
+
return;
|
|
1141
|
+
const lines = stripped.split('\n');
|
|
1142
|
+
const rawLines = source.split('\n');
|
|
1143
|
+
let loopDepth = 0;
|
|
1144
|
+
let braceDepth = 0;
|
|
1145
|
+
let inLoop = false;
|
|
1146
|
+
let loopBraceStart = 0;
|
|
1147
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1148
|
+
const line = lines[i];
|
|
1149
|
+
const rawLine = rawLines[i];
|
|
1150
|
+
if (/\b(for|while|do)\s*[\s(]/.test(line) && !inLoop) {
|
|
1151
|
+
inLoop = true;
|
|
1152
|
+
loopBraceStart = braceDepth;
|
|
1153
|
+
loopDepth++;
|
|
1154
|
+
}
|
|
1155
|
+
for (const ch of line) {
|
|
1156
|
+
if (ch === '{')
|
|
1157
|
+
braceDepth++;
|
|
1158
|
+
else if (ch === '}') {
|
|
1159
|
+
braceDepth--;
|
|
1160
|
+
if (inLoop && braceDepth <= loopBraceStart) {
|
|
1161
|
+
inLoop = false;
|
|
1162
|
+
loopDepth--;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
if (loopDepth > 0) {
|
|
1167
|
+
// PERF-JV1: String concat in loop
|
|
1168
|
+
if (/\b[a-zA-Z_]\w*\s*\+=/.test(line)) {
|
|
1169
|
+
findings.push({
|
|
1170
|
+
severity: 'WARNING',
|
|
1171
|
+
code: 'PERF-JV1',
|
|
1172
|
+
line: i + 1,
|
|
1173
|
+
message: 'String concatenation (+=) inside loop — allocates intermediate String and StringBuilder instances.',
|
|
1174
|
+
suggestion: 'Use StringBuilder explicitly outside the loop.',
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
// PERF-JV3: Expensive object instantiations in loop
|
|
1178
|
+
if (/\bnew\s+(SimpleDateFormat|DecimalFormat|Pattern|BigInteger|BigDecimal)\b/.test(rawLine) ||
|
|
1179
|
+
/\bPattern\.compile\s*\(/.test(line)) {
|
|
1180
|
+
findings.push({
|
|
1181
|
+
severity: 'WARNING',
|
|
1182
|
+
code: 'PERF-JV3',
|
|
1183
|
+
line: i + 1,
|
|
1184
|
+
message: 'Heavy object instantiation / Pattern compilation inside loop.',
|
|
1185
|
+
suggestion: 'Extract formatter / pattern instances outside the loop or reuse thread-safe / static instances.',
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
// PERF-JV2: System.gc() call
|
|
1190
|
+
if (/\bSystem\.gc\s*\(\s*\)/.test(line)) {
|
|
1191
|
+
findings.push({
|
|
1192
|
+
severity: 'WARNING',
|
|
1193
|
+
code: 'PERF-JV2',
|
|
1194
|
+
line: i + 1,
|
|
1195
|
+
message: 'Explicit System.gc() invocation — triggers full stop-the-world Garbage Collection.',
|
|
1196
|
+
suggestion: 'Avoid explicit GC calls and let the JVM manage memory automatically.',
|
|
1197
|
+
});
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
};
|
|
1201
|
+
// ── PERF-CPP1/2/3: C++ Specific Loop Operations ────────────────────────
|
|
1202
|
+
const detectCppInLoopOperations = (source, stripped, lang, findings) => {
|
|
1203
|
+
if (lang !== 'cpp')
|
|
1204
|
+
return;
|
|
1205
|
+
const lines = stripped.split('\n');
|
|
1206
|
+
const rawLines = source.split('\n');
|
|
1207
|
+
let loopDepth = 0;
|
|
1208
|
+
let braceDepth = 0;
|
|
1209
|
+
let inLoop = false;
|
|
1210
|
+
let loopBraceStart = 0;
|
|
1211
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1212
|
+
const line = lines[i];
|
|
1213
|
+
const rawLine = rawLines[i];
|
|
1214
|
+
if (/\bfor\s*\(\s*(auto|[a-zA-Z_]\w*(?:::[a-zA-Z_]\w*)*)\s+([a-zA-Z_]\w*)\s*:\s*/.test(line)) {
|
|
1215
|
+
const match = line.match(/\bfor\s*\(\s*(const\s+)?(auto|[a-zA-Z_]\w*(?:::[a-zA-Z_]\w*)*)\s*(&)?\s*([a-zA-Z_]\w*)\s*:\s*/);
|
|
1216
|
+
if (match && !match[3]) {
|
|
1217
|
+
const typeName = match[2];
|
|
1218
|
+
const primitiveTypes = [
|
|
1219
|
+
'int',
|
|
1220
|
+
'char',
|
|
1221
|
+
'bool',
|
|
1222
|
+
'float',
|
|
1223
|
+
'double',
|
|
1224
|
+
'size_t',
|
|
1225
|
+
'long',
|
|
1226
|
+
'short',
|
|
1227
|
+
'uint32_t',
|
|
1228
|
+
'int32_t',
|
|
1229
|
+
];
|
|
1230
|
+
if (!primitiveTypes.includes(typeName)) {
|
|
1231
|
+
findings.push({
|
|
1232
|
+
severity: 'WARNING',
|
|
1233
|
+
code: 'PERF-CPP1',
|
|
1234
|
+
line: i + 1,
|
|
1235
|
+
message: 'Range-based for loop element copied by value — triggers copy constructor for non-primitive types.',
|
|
1236
|
+
suggestion: 'Use const auto& or auto& to pass loop elements by reference.',
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
if (/\b(for|while|do)\s*[\s(]/.test(line) && !inLoop) {
|
|
1242
|
+
inLoop = true;
|
|
1243
|
+
loopBraceStart = braceDepth;
|
|
1244
|
+
loopDepth++;
|
|
1245
|
+
}
|
|
1246
|
+
for (const ch of line) {
|
|
1247
|
+
if (ch === '{')
|
|
1248
|
+
braceDepth++;
|
|
1249
|
+
else if (ch === '}') {
|
|
1250
|
+
braceDepth--;
|
|
1251
|
+
if (inLoop && braceDepth <= loopBraceStart) {
|
|
1252
|
+
inLoop = false;
|
|
1253
|
+
loopDepth--;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
if (loopDepth > 0) {
|
|
1258
|
+
// PERF-CPP2: std::endl inside loop
|
|
1259
|
+
if (/\bstd::endl\b/.test(rawLine)) {
|
|
1260
|
+
findings.push({
|
|
1261
|
+
severity: 'WARNING',
|
|
1262
|
+
code: 'PERF-CPP2',
|
|
1263
|
+
line: i + 1,
|
|
1264
|
+
message: 'std::endl used inside loop — forces stream buffer flushing on every iteration.',
|
|
1265
|
+
suggestion: "Use '\\n' instead of std::endl to avoid frequent I/O flushing.",
|
|
1266
|
+
});
|
|
1267
|
+
}
|
|
1268
|
+
// PERF-CPP3: std::regex inside loop
|
|
1269
|
+
if (/\bstd::regex\b/.test(line)) {
|
|
1270
|
+
findings.push({
|
|
1271
|
+
severity: 'WARNING',
|
|
1272
|
+
code: 'PERF-CPP3',
|
|
1273
|
+
line: i + 1,
|
|
1274
|
+
message: 'std::regex compilation inside loop — C++ regex construction is extremely heavy.',
|
|
1275
|
+
suggestion: 'Construct std::regex objects once outside the loop.',
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
};
|
|
1281
|
+
// ── PERF-RB1/2/3: Ruby Specific Loop Operations ────────────────────────
|
|
1282
|
+
const detectRubyInLoopOperations = (source, stripped, lang, findings) => {
|
|
1283
|
+
if (lang !== 'ruby')
|
|
1284
|
+
return;
|
|
1285
|
+
const lines = stripped.split('\n');
|
|
1286
|
+
let inLoop = false;
|
|
1287
|
+
let outerIndent = -1;
|
|
1288
|
+
const loopPattern = /\b(while|until|for)\b|\.(each|times|map|select|loop)\b/;
|
|
1289
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1290
|
+
const line = lines[i];
|
|
1291
|
+
if (line.trim() === '')
|
|
1292
|
+
continue;
|
|
1293
|
+
const indent = line.search(/\S/);
|
|
1294
|
+
if (inLoop && indent <= outerIndent && /^\s*(end|\}|\))\b/.test(line)) {
|
|
1295
|
+
inLoop = false;
|
|
1296
|
+
}
|
|
1297
|
+
// PERF-RB3: Chained Enumerable methods
|
|
1298
|
+
if (/\.(map|select|reject|compact)\b[^.\n]*\.(map|select|reject|compact)\b/.test(line)) {
|
|
1299
|
+
findings.push({
|
|
1300
|
+
severity: 'WARNING',
|
|
1301
|
+
code: 'PERF-RB3',
|
|
1302
|
+
line: i + 1,
|
|
1303
|
+
message: 'Chained Enumerable transformations — creates intermediate array allocations.',
|
|
1304
|
+
suggestion: 'Use .lazy or combine transformations into a single pass.',
|
|
1305
|
+
});
|
|
1306
|
+
}
|
|
1307
|
+
if (!inLoop && loopPattern.test(line)) {
|
|
1308
|
+
inLoop = true;
|
|
1309
|
+
outerIndent = indent;
|
|
1310
|
+
continue;
|
|
1311
|
+
}
|
|
1312
|
+
if (inLoop && indent > outerIndent) {
|
|
1313
|
+
// PERF-RB1: String concat += in loop
|
|
1314
|
+
if (/\b[a-zA-Z_]\w*\s*\+=/.test(line)) {
|
|
1315
|
+
findings.push({
|
|
1316
|
+
severity: 'WARNING',
|
|
1317
|
+
code: 'PERF-RB1',
|
|
1318
|
+
line: i + 1,
|
|
1319
|
+
message: 'String concatenation (+=) inside loop — creates intermediate string allocations.',
|
|
1320
|
+
suggestion: 'Use shovel operator << or string interpolation instead.',
|
|
1321
|
+
});
|
|
1322
|
+
}
|
|
1323
|
+
// PERF-RB2: ActiveRecord N+1 query in loop
|
|
1324
|
+
if (/\.(find|where|first|find_by)\b/.test(line)) {
|
|
1325
|
+
findings.push({
|
|
1326
|
+
severity: 'WARNING',
|
|
1327
|
+
code: 'PERF-RB2',
|
|
1328
|
+
line: i + 1,
|
|
1329
|
+
message: 'Database query execution inside loop (N+1 query risk).',
|
|
1330
|
+
suggestion: 'Pre-fetch records outside loop using .includes() or bulk query.',
|
|
1331
|
+
});
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
};
|
|
879
1336
|
// ─── Rule Registry ───────────────────────────────────────────────────
|
|
880
1337
|
const ALL_RULES = [
|
|
881
1338
|
detectNestedLoops,
|
|
@@ -894,6 +1351,10 @@ const ALL_RULES = [
|
|
|
894
1351
|
detectPythonInLoopOperations,
|
|
895
1352
|
detectGoInLoopOperations,
|
|
896
1353
|
detectRustInLoopOperations,
|
|
1354
|
+
detectCsharpInLoopOperations,
|
|
1355
|
+
detectJavaInLoopOperations,
|
|
1356
|
+
detectCppInLoopOperations,
|
|
1357
|
+
detectRubyInLoopOperations,
|
|
897
1358
|
];
|
|
898
1359
|
// ─── Supported Extensions for Auditing ───────────────────────────────
|
|
899
1360
|
const AUDITABLE_EXTENSIONS = new Set(Object.keys(EXTENSION_MAP));
|
|
@@ -907,10 +1368,7 @@ export function isAuditableFile(filePath) {
|
|
|
907
1368
|
/**
|
|
908
1369
|
* Scans source code for performance anti-patterns using language-aware
|
|
909
1370
|
* regex-based heuristics. Returns structured findings grouped by severity.
|
|
910
|
-
*
|
|
911
|
-
* This function is designed to be called inline during the verification
|
|
912
|
-
* phase — it's synchronous, zero-dependency, and executes in <50ms on
|
|
913
|
-
* files up to 10,000 lines.
|
|
1371
|
+
* Supports comment suppression directives (perf-ignore).
|
|
914
1372
|
*
|
|
915
1373
|
* @param content - The raw source code string to audit.
|
|
916
1374
|
* @param filePath - The file path (used for language detection via extension).
|
|
@@ -918,15 +1376,17 @@ export function isAuditableFile(filePath) {
|
|
|
918
1376
|
*/
|
|
919
1377
|
export function auditFilePerformance(content, filePath) {
|
|
920
1378
|
const lang = detectLanguage(filePath);
|
|
921
|
-
const findings = [];
|
|
922
1379
|
if (lang === 'unknown') {
|
|
923
1380
|
return { filePath, findings: [], errors: [], warnings: [], infos: [] };
|
|
924
1381
|
}
|
|
925
1382
|
// Strip strings and comments to avoid false positives
|
|
926
1383
|
const stripped = stripStringsAndComments(content, lang);
|
|
1384
|
+
let findings = [];
|
|
927
1385
|
for (const rule of ALL_RULES) {
|
|
928
1386
|
rule(content, stripped, lang, findings);
|
|
929
1387
|
}
|
|
1388
|
+
// Filter out findings suppressed by perf-ignore comment directives
|
|
1389
|
+
findings = applyDirectiveSuppression(findings, content);
|
|
930
1390
|
// Sort by severity (ERROR first), then by line number
|
|
931
1391
|
const severityOrder = { ERROR: 0, WARNING: 1, INFO: 2 };
|
|
932
1392
|
findings.sort((a, b) => {
|
|
@@ -948,21 +1408,17 @@ const SEVERITY_ICONS = {
|
|
|
948
1408
|
INFO: '🔵',
|
|
949
1409
|
};
|
|
950
1410
|
/**
|
|
951
|
-
* Formats audit findings for
|
|
952
|
-
* Used for non-blocking warnings shown to the user after verification.
|
|
1411
|
+
* Formats audit findings for terminal display using picocolors.
|
|
953
1412
|
*/
|
|
954
1413
|
export function formatAuditForTerminal(results) {
|
|
955
1414
|
const allFindings = results.flatMap((r) => r.findings.map((f) => ({ ...f, file: r.filePath })));
|
|
956
1415
|
if (allFindings.length === 0)
|
|
957
1416
|
return '';
|
|
958
|
-
const lines = [
|
|
959
|
-
pc.bold('Performance Audit Results'),
|
|
960
|
-
pc.dim('─'.repeat(50)),
|
|
961
|
-
];
|
|
1417
|
+
const lines = [pc.bold('Performance Audit Results'), pc.dim('─'.repeat(50))];
|
|
962
1418
|
for (const f of allFindings) {
|
|
963
1419
|
const icon = SEVERITY_ICONS[f.severity];
|
|
964
1420
|
const sevColor = f.severity === 'ERROR' ? pc.red : f.severity === 'WARNING' ? pc.yellow : pc.blue;
|
|
965
|
-
lines.push(`${icon} ${sevColor(f.severity)} ${pc.dim(`[${f.code}]`)} ${pc.cyan(f.file
|
|
1421
|
+
lines.push(`${icon} ${sevColor(f.severity)} ${pc.dim(`[${f.code}]`)} ${pc.cyan(`${f.file}:${f.line}`)}`);
|
|
966
1422
|
lines.push(` ${f.message}`);
|
|
967
1423
|
lines.push(` ${pc.dim('→')} ${pc.dim(f.suggestion)}`);
|
|
968
1424
|
}
|
|
@@ -977,16 +1433,12 @@ export function formatAuditForTerminal(results) {
|
|
|
977
1433
|
}
|
|
978
1434
|
/**
|
|
979
1435
|
* Formats audit findings into a structured prompt for the AI auto-correction loop.
|
|
980
|
-
* Only includes ERROR-severity findings (warnings and info are non-blocking).
|
|
981
1436
|
*/
|
|
982
1437
|
export function formatAuditForModel(results) {
|
|
983
1438
|
const errors = results.flatMap((r) => r.errors.map((f) => ({ ...f, file: r.filePath })));
|
|
984
1439
|
if (errors.length === 0)
|
|
985
1440
|
return '';
|
|
986
|
-
const lines = [
|
|
987
|
-
'The following performance anti-patterns were detected in your generated code:',
|
|
988
|
-
'',
|
|
989
|
-
];
|
|
1441
|
+
const lines = ['The following performance anti-patterns were detected in your generated code:', ''];
|
|
990
1442
|
for (const e of errors) {
|
|
991
1443
|
lines.push(`[${e.code}] ${e.file}:${e.line} — ${e.message}`);
|
|
992
1444
|
lines.push(` Fix: ${e.suggestion}`);
|