vibeclean 1.0.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.
@@ -0,0 +1,216 @@
1
+ import { severityFromScore, scoreFromRatio, countMatches, parseAst } from "./utils.js";
2
+
3
+ function countAwaitStats(fileContent) {
4
+ const ast = parseAst(fileContent);
5
+ if (!ast) {
6
+ return { totalAwait: countMatches(fileContent, /\bawait\b/g), unhandledAwait: 0 };
7
+ }
8
+
9
+ const counters = {
10
+ totalAwait: 0,
11
+ unhandledAwait: 0
12
+ };
13
+
14
+ function hasCatchChain(node) {
15
+ if (!node || typeof node !== "object") {
16
+ return false;
17
+ }
18
+
19
+ if (
20
+ node.type === "CallExpression" &&
21
+ node.callee?.type === "MemberExpression" &&
22
+ !node.callee.computed &&
23
+ node.callee.property?.type === "Identifier" &&
24
+ node.callee.property.name === "catch"
25
+ ) {
26
+ return true;
27
+ }
28
+
29
+ if (node.type === "CallExpression" && node.callee?.type === "MemberExpression") {
30
+ return hasCatchChain(node.callee.object);
31
+ }
32
+
33
+ return false;
34
+ }
35
+
36
+ function visit(node, inTryBlock = false) {
37
+ if (!node || typeof node !== "object") {
38
+ return;
39
+ }
40
+
41
+ if (node.type === "AwaitExpression") {
42
+ counters.totalAwait += 1;
43
+ if (!inTryBlock && !hasCatchChain(node.argument)) {
44
+ counters.unhandledAwait += 1;
45
+ }
46
+ }
47
+
48
+ if (node.type === "TryStatement") {
49
+ visit(node.block, true);
50
+ if (node.handler) {
51
+ visit(node.handler, false);
52
+ }
53
+ if (node.finalizer) {
54
+ visit(node.finalizer, false);
55
+ }
56
+ return;
57
+ }
58
+
59
+ for (const key of Object.keys(node)) {
60
+ const value = node[key];
61
+ if (Array.isArray(value)) {
62
+ for (const child of value) {
63
+ if (child?.type) {
64
+ visit(child, inTryBlock);
65
+ }
66
+ }
67
+ } else if (value?.type) {
68
+ visit(value, inTryBlock);
69
+ }
70
+ }
71
+ }
72
+
73
+ visit(ast, false);
74
+ return counters;
75
+ }
76
+
77
+ export function analyzeErrorHandling(files) {
78
+ let totalFunctions = 0;
79
+ let tryBlocks = 0;
80
+ let catchBlocks = 0;
81
+ let emptyCatch = 0;
82
+ let catchLogOnly = 0;
83
+ let unhandledAwait = 0;
84
+ let thenChains = 0;
85
+ let catchChains = 0;
86
+ let throwCount = 0;
87
+ let returnNullCount = 0;
88
+ let returnErrorObjectCount = 0;
89
+ let totalAwait = 0;
90
+
91
+ for (const file of files) {
92
+ const content = file.content;
93
+
94
+ totalFunctions += countMatches(
95
+ content,
96
+ /(?:async\s+)?function\s+[A-Za-z_$][A-Za-z0-9_$]*\s*\(|\b[A-Za-z_$][A-Za-z0-9_$]*\s*=\s*(?:async\s*)?\([^)]*\)\s*=>|\([^)]*\)\s*=>\s*\{/g
97
+ );
98
+
99
+ tryBlocks += countMatches(content, /\btry\s*\{/g);
100
+ catchBlocks += countMatches(content, /\bcatch\s*\(/g);
101
+
102
+ emptyCatch += countMatches(content, /catch\s*\([^)]*\)\s*\{\s*\}/gs);
103
+ catchLogOnly += countMatches(
104
+ content,
105
+ /catch\s*\([^)]*\)\s*\{\s*console\.(?:log|warn|error|debug)\([^)]*\);?\s*\}/gs
106
+ );
107
+
108
+ const awaitStats = countAwaitStats(content);
109
+ totalAwait += awaitStats.totalAwait;
110
+ unhandledAwait += awaitStats.unhandledAwait;
111
+
112
+ thenChains += countMatches(content, /\.then\s*\(/g);
113
+ catchChains += countMatches(content, /\.catch\s*\(/g);
114
+
115
+ throwCount += countMatches(content, /\bthrow\b/g);
116
+ returnNullCount += countMatches(content, /return\s+null\b/g);
117
+ returnErrorObjectCount += countMatches(content, /return\s+\{\s*error\s*[:}]|return\s+error\b/g);
118
+ }
119
+
120
+ const functionsWithTry = Math.min(totalFunctions, tryBlocks);
121
+ const handledRate = totalFunctions ? Math.round((functionsWithTry / totalFunctions) * 100) : 100;
122
+ const promiseWithoutCatch = Math.max(0, thenChains - catchChains);
123
+
124
+ const findings = [];
125
+ if (handledRate < 60) {
126
+ findings.push({
127
+ severity: handledRate < 40 ? "high" : "medium",
128
+ message: `Only ${handledRate}% of detected functions use try/catch.`
129
+ });
130
+ }
131
+
132
+ if (emptyCatch > 0) {
133
+ findings.push({
134
+ severity: "high",
135
+ message: `${emptyCatch} empty catch blocks found.`
136
+ });
137
+ }
138
+
139
+ if (catchLogOnly > 0) {
140
+ findings.push({
141
+ severity: "medium",
142
+ message: `${catchLogOnly} catch blocks only log errors and do not recover or rethrow.`
143
+ });
144
+ }
145
+
146
+ if (unhandledAwait > 0) {
147
+ findings.push({
148
+ severity: "high",
149
+ message: `${unhandledAwait} await calls appear to be outside local try/catch context.`
150
+ });
151
+ }
152
+
153
+ if (promiseWithoutCatch > 0) {
154
+ findings.push({
155
+ severity: "medium",
156
+ message: `${promiseWithoutCatch} promise chains appear to miss .catch() handling.`
157
+ });
158
+ }
159
+
160
+ const mixedPatterns =
161
+ (throwCount > 0 ? 1 : 0) +
162
+ (returnNullCount > 0 ? 1 : 0) +
163
+ (returnErrorObjectCount > 0 ? 1 : 0);
164
+
165
+ if (mixedPatterns > 1) {
166
+ findings.push({
167
+ severity: "medium",
168
+ message: "Mixed error return patterns detected (throw, return null, and/or return error objects)."
169
+ });
170
+ }
171
+
172
+ const awaitRisk = totalAwait ? (unhandledAwait / totalAwait) * 2 : 0;
173
+ const promiseRisk = thenChains ? (promiseWithoutCatch / thenChains) * 2 : 0;
174
+ const signal =
175
+ (handledRate < 50 ? (50 - handledRate) / 18 : 0) +
176
+ emptyCatch * 1.5 +
177
+ catchLogOnly +
178
+ awaitRisk +
179
+ promiseRisk;
180
+
181
+ const score = Math.min(10, scoreFromRatio(signal / Math.max(files.length * 0.8, 1), 10));
182
+
183
+ return {
184
+ id: "errorhandling",
185
+ title: "ERROR HANDLING",
186
+ score,
187
+ severity: severityFromScore(score),
188
+ totalIssues: findings.length,
189
+ summary:
190
+ findings.length > 0
191
+ ? "Inconsistent error handling patterns detected in async and promise code paths."
192
+ : "Error handling patterns look reasonably consistent.",
193
+ metrics: {
194
+ totalFunctions,
195
+ functionsWithTry,
196
+ handledRate,
197
+ tryBlocks,
198
+ catchBlocks,
199
+ emptyCatch,
200
+ catchLogOnly,
201
+ totalAwait,
202
+ unhandledAwait,
203
+ promiseWithoutCatch,
204
+ throwCount,
205
+ returnNullCount,
206
+ returnErrorObjectCount
207
+ },
208
+ recommendations: [
209
+ "Wrap async operations in try/catch and surface errors consistently.",
210
+ "Avoid empty catch blocks and catch-and-log-only handlers.",
211
+ "Use one error propagation pattern across the codebase."
212
+ ],
213
+ findings
214
+ };
215
+ }
216
+
@@ -0,0 +1,422 @@
1
+ import { severityFromScore, scoreFromRatio } from "./utils.js";
2
+
3
+ const TODO_RE = /\/\/\s*(TODO|FIXME|HACK|XXX)\b.*$/gim;
4
+ const AI_TODO_RE = /\/\/\s*TODO\s*:\s*(implement this|add error handling|replace with actual implementation|improve this|finish this)/gim;
5
+ const CONSOLE_RE = /\bconsole\.(log|warn|error|debug|trace)\s*\(/g;
6
+ const LOCALHOST_RE = /(https?:\/\/(?:localhost|127\.0\.0\.1)(?::\d+)?[\w\-/?.=&]*)/gi;
7
+ const PLACEHOLDER_RE =
8
+ /(your-api-key-here|sk-[xX]{3,}|pk_test_[a-zA-Z0-9]+|REPLACE_ME|test@test\.com|user@example\.com|admin@admin\.com|(?:password|passwd|pwd|secret|api[_-]?key)\s*[:=]\s*["'`](?:password123|admin|test)["'`])/gi;
9
+ const LOREM_RE = /lorem ipsum/gi;
10
+ const AI_COMMENT_RE = /\/\/\s*(AI generated|Generated by|Created by Copilot|This function\b.*)/gim;
11
+ const MAX_LOCATIONS_PER_FINDING = 12;
12
+
13
+ function countCommentedOutBlocks(content) {
14
+ const lines = content.split("\n");
15
+ let blocks = 0;
16
+ let linesInBlocks = 0;
17
+ let streak = 0;
18
+ const blockLocations = [];
19
+
20
+ for (let index = 0; index < lines.length; index += 1) {
21
+ const line = lines[index];
22
+ if (/^\s*\/\//.test(line) && /[;{}()[\]=]|\b(const|let|var|if|for|while|return|import|export|function|class)\b/.test(line)) {
23
+ if (streak === 0) {
24
+ blockLocations.push(index + 1);
25
+ }
26
+ streak += 1;
27
+ } else {
28
+ if (streak >= 3) {
29
+ blocks += 1;
30
+ linesInBlocks += streak;
31
+ } else if (streak > 0) {
32
+ blockLocations.pop();
33
+ }
34
+ streak = 0;
35
+ }
36
+ }
37
+
38
+ if (streak >= 3) {
39
+ blocks += 1;
40
+ linesInBlocks += streak;
41
+ } else if (streak > 0) {
42
+ blockLocations.pop();
43
+ }
44
+
45
+ return { blocks, linesInBlocks, blockLocations };
46
+ }
47
+
48
+ function lineNumberAtIndex(content, index) {
49
+ return content.slice(0, index).split("\n").length;
50
+ }
51
+
52
+ function lineAtNumber(content, lineNumber) {
53
+ return content.split("\n")[lineNumber - 1] || "";
54
+ }
55
+
56
+ function lineSnippet(content, lineNumber) {
57
+ const line = lineAtNumber(content, lineNumber);
58
+ return line.trim().slice(0, 140);
59
+ }
60
+
61
+ function collectRegexLocations(content, regex, relativePath, maxLocations) {
62
+ const flags = regex.flags.includes("g") ? regex.flags : `${regex.flags}g`;
63
+ const probe = new RegExp(regex.source, flags);
64
+ const locations = [];
65
+ const seen = new Set();
66
+
67
+ for (const match of content.matchAll(probe)) {
68
+ const index = typeof match.index === "number" ? match.index : 0;
69
+ const line = lineNumberAtIndex(content, index);
70
+ const locationKey = `${relativePath}:${line}`;
71
+ if (seen.has(locationKey)) {
72
+ continue;
73
+ }
74
+ seen.add(locationKey);
75
+ locations.push({
76
+ file: relativePath,
77
+ line,
78
+ snippet: lineSnippet(content, line)
79
+ });
80
+ if (locations.length >= maxLocations) {
81
+ break;
82
+ }
83
+ }
84
+
85
+ return locations;
86
+ }
87
+
88
+ function normalizePath(value = "") {
89
+ return value.replace(/\\/g, "/");
90
+ }
91
+
92
+ function matchesPathPrefix(relativePath, prefixes = []) {
93
+ const normalizedPath = normalizePath(relativePath);
94
+ for (const prefix of prefixes) {
95
+ if (!prefix) {
96
+ continue;
97
+ }
98
+ const normalizedPrefix = normalizePath(prefix);
99
+ if (normalizedPath.startsWith(normalizedPrefix)) {
100
+ return true;
101
+ }
102
+ }
103
+ return false;
104
+ }
105
+
106
+ function isDetectorRegexDefinitionLine(content, index) {
107
+ const lineNumber = lineNumberAtIndex(content, index);
108
+ const line = lineAtNumber(content, lineNumber).trim();
109
+ const prevLine = lineAtNumber(content, lineNumber - 1).trim();
110
+
111
+ if (/^const\s+[A-Z_]+_RE\s*=/.test(line) && /\/.+\/[gimsuy]*;?$/.test(line)) {
112
+ return true;
113
+ }
114
+
115
+ if (/^\/.+\/[gimsuy]*;?$/.test(line) && /^const\s+[A-Z_]+_RE\s*=\s*$/.test(prevLine)) {
116
+ return true;
117
+ }
118
+
119
+ return false;
120
+ }
121
+
122
+ function collectRegexMatches(content, regex, relativePath, maxLocations, skipMatch) {
123
+ const flags = regex.flags.includes("g") ? regex.flags : `${regex.flags}g`;
124
+ const probe = new RegExp(regex.source, flags);
125
+ let count = 0;
126
+ const locations = [];
127
+ const seen = new Set();
128
+
129
+ for (const match of content.matchAll(probe)) {
130
+ const index = typeof match.index === "number" ? match.index : 0;
131
+ if (typeof skipMatch === "function" && skipMatch(index, match)) {
132
+ continue;
133
+ }
134
+
135
+ count += 1;
136
+
137
+ if (locations.length >= maxLocations) {
138
+ continue;
139
+ }
140
+
141
+ const line = lineNumberAtIndex(content, index);
142
+ const locationKey = `${relativePath}:${line}`;
143
+ if (seen.has(locationKey)) {
144
+ continue;
145
+ }
146
+ seen.add(locationKey);
147
+ locations.push({
148
+ file: relativePath,
149
+ line,
150
+ snippet: lineSnippet(content, line)
151
+ });
152
+ }
153
+
154
+ return { count, locations };
155
+ }
156
+
157
+ function extractImportedNames(content) {
158
+ const imported = [];
159
+ const importLines = content.match(/import\s+[^;\n]+/g) || [];
160
+
161
+ for (const line of importLines) {
162
+ const noFrom = line.replace(/\s+from\s+["'`][^"'`]+["'`]/, "").replace(/^import\s+/, "");
163
+ const parts = noFrom
164
+ .replace(/[{}]/g, "")
165
+ .split(",")
166
+ .map((item) => item.trim())
167
+ .filter(Boolean);
168
+
169
+ for (const part of parts) {
170
+ const aliasParts = part.split(/\s+as\s+/i);
171
+ const localName = aliasParts[1] || aliasParts[0];
172
+ if (localName && /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(localName)) {
173
+ imported.push(localName);
174
+ }
175
+ }
176
+ }
177
+
178
+ return imported;
179
+ }
180
+
181
+ function findUnusedImports(content) {
182
+ const importedNames = extractImportedNames(content);
183
+ if (!importedNames.length) {
184
+ return [];
185
+ }
186
+
187
+ const linesWithoutImports = content
188
+ .split("\n")
189
+ .filter((line) => !line.trim().startsWith("import "))
190
+ .join("\n");
191
+
192
+ const unused = [];
193
+ for (const name of importedNames) {
194
+ const re = new RegExp(`\\b${name}\\b`, "g");
195
+ const matches = linesWithoutImports.match(re);
196
+ if (!matches || matches.length === 0) {
197
+ unused.push(name);
198
+ }
199
+ }
200
+
201
+ return unused;
202
+ }
203
+
204
+ export function analyzeLeftovers(files, context = {}) {
205
+ let todoCount = 0;
206
+ let aiTodoCount = 0;
207
+ let consoleCount = 0;
208
+ let localhostCount = 0;
209
+ let placeholderCount = 0;
210
+ let loremCount = 0;
211
+ let aiCommentCount = 0;
212
+ let commentedOutBlocks = 0;
213
+ let commentedOutLines = 0;
214
+ let unusedImportCount = 0;
215
+
216
+ const filesWithConsole = new Set();
217
+ const filesWithTodos = new Set();
218
+ const filesWithPlaceholders = new Set();
219
+ const consoleLocations = [];
220
+ const todoLocations = [];
221
+ const placeholderLocations = [];
222
+ const commentedCodeLocations = [];
223
+ const profileOptions = context.config?.leftovers || {};
224
+ const allowConsolePaths = Array.isArray(profileOptions.allowConsolePaths)
225
+ ? profileOptions.allowConsolePaths
226
+ : [];
227
+ const ignoreTodoPaths = Array.isArray(profileOptions.ignoreTodoPaths)
228
+ ? profileOptions.ignoreTodoPaths
229
+ : [];
230
+
231
+ for (const file of files) {
232
+ const content = file.content;
233
+ const skipTodoSignals = matchesPathPrefix(file.relativePath, ignoreTodoPaths);
234
+ const allowConsole = matchesPathPrefix(file.relativePath, allowConsolePaths);
235
+
236
+ if (!skipTodoSignals) {
237
+ const todos = content.match(TODO_RE) || [];
238
+ if (todos.length > 0) {
239
+ todoCount += todos.length;
240
+ filesWithTodos.add(file.relativePath);
241
+ if (todoLocations.length < MAX_LOCATIONS_PER_FINDING) {
242
+ todoLocations.push(
243
+ ...collectRegexLocations(
244
+ content,
245
+ TODO_RE,
246
+ file.relativePath,
247
+ MAX_LOCATIONS_PER_FINDING - todoLocations.length
248
+ )
249
+ );
250
+ }
251
+ }
252
+
253
+ aiTodoCount += (content.match(AI_TODO_RE) || []).length;
254
+ }
255
+
256
+ if (!allowConsole && !/(logger|logging|middleware)/i.test(file.relativePath)) {
257
+ const consoles = content.match(CONSOLE_RE) || [];
258
+ if (consoles.length > 0) {
259
+ consoleCount += consoles.length;
260
+ filesWithConsole.add(file.relativePath);
261
+ if (consoleLocations.length < MAX_LOCATIONS_PER_FINDING) {
262
+ consoleLocations.push(
263
+ ...collectRegexLocations(
264
+ content,
265
+ CONSOLE_RE,
266
+ file.relativePath,
267
+ MAX_LOCATIONS_PER_FINDING - consoleLocations.length
268
+ )
269
+ );
270
+ }
271
+ }
272
+ }
273
+
274
+ const localhostSignals = collectRegexMatches(
275
+ content,
276
+ LOCALHOST_RE,
277
+ file.relativePath,
278
+ MAX_LOCATIONS_PER_FINDING - placeholderLocations.length,
279
+ (index) => isDetectorRegexDefinitionLine(content, index)
280
+ );
281
+ if (localhostSignals.count > 0) {
282
+ localhostCount += localhostSignals.count;
283
+ filesWithPlaceholders.add(file.relativePath);
284
+ if (placeholderLocations.length < MAX_LOCATIONS_PER_FINDING) {
285
+ placeholderLocations.push(...localhostSignals.locations);
286
+ }
287
+ }
288
+
289
+ const placeholderSignals = collectRegexMatches(
290
+ content,
291
+ PLACEHOLDER_RE,
292
+ file.relativePath,
293
+ MAX_LOCATIONS_PER_FINDING - placeholderLocations.length,
294
+ (index) => isDetectorRegexDefinitionLine(content, index)
295
+ );
296
+ if (placeholderSignals.count > 0) {
297
+ placeholderCount += placeholderSignals.count;
298
+ filesWithPlaceholders.add(file.relativePath);
299
+ if (placeholderLocations.length < MAX_LOCATIONS_PER_FINDING) {
300
+ placeholderLocations.push(...placeholderSignals.locations);
301
+ }
302
+ }
303
+
304
+ loremCount += (content.match(LOREM_RE) || []).length;
305
+ aiCommentCount += (content.match(AI_COMMENT_RE) || []).length;
306
+
307
+ const blockStats = countCommentedOutBlocks(content);
308
+ commentedOutBlocks += blockStats.blocks;
309
+ commentedOutLines += blockStats.linesInBlocks;
310
+ if (commentedCodeLocations.length < MAX_LOCATIONS_PER_FINDING) {
311
+ for (const line of blockStats.blockLocations) {
312
+ commentedCodeLocations.push({
313
+ file: file.relativePath,
314
+ line,
315
+ snippet: lineSnippet(content, line)
316
+ });
317
+ if (commentedCodeLocations.length >= MAX_LOCATIONS_PER_FINDING) {
318
+ break;
319
+ }
320
+ }
321
+ }
322
+
323
+ const unusedImports = findUnusedImports(content);
324
+ unusedImportCount += unusedImports.length;
325
+ }
326
+
327
+ const issueWeight =
328
+ consoleCount +
329
+ todoCount +
330
+ aiTodoCount +
331
+ localhostCount * 2 +
332
+ placeholderCount * 2 +
333
+ aiCommentCount +
334
+ commentedOutBlocks * 2 +
335
+ unusedImportCount;
336
+
337
+ const score = Math.min(10, scoreFromRatio(issueWeight / Math.max(files.length * 1.2, 1), 10));
338
+
339
+ const findings = [];
340
+ if (consoleCount) {
341
+ findings.push({
342
+ severity: consoleCount > 15 ? "high" : "medium",
343
+ message: `${consoleCount} console statements found across ${filesWithConsole.size} files.`,
344
+ files: [...filesWithConsole].slice(0, 20),
345
+ locations: consoleLocations
346
+ });
347
+ }
348
+
349
+ if (todoCount) {
350
+ findings.push({
351
+ severity: todoCount > 10 ? "high" : "medium",
352
+ message: `${todoCount} TODO/FIXME/HACK markers found (${aiTodoCount} look AI-generated).`,
353
+ files: [...filesWithTodos].slice(0, 20),
354
+ locations: todoLocations
355
+ });
356
+ }
357
+
358
+ if (localhostCount || placeholderCount) {
359
+ findings.push({
360
+ severity: "high",
361
+ message: `${localhostCount} localhost URLs and ${placeholderCount} placeholder values found in code.`,
362
+ files: [...filesWithPlaceholders].slice(0, 20),
363
+ locations: placeholderLocations
364
+ });
365
+ }
366
+
367
+ if (commentedOutBlocks) {
368
+ findings.push({
369
+ severity: "low",
370
+ message: `${commentedOutBlocks} commented-out code blocks (${commentedOutLines} lines total).`,
371
+ locations: commentedCodeLocations
372
+ });
373
+ }
374
+
375
+ if (unusedImportCount) {
376
+ findings.push({
377
+ severity: "low",
378
+ message: `${unusedImportCount} potentially unused imports detected.`
379
+ });
380
+ }
381
+
382
+ const totalIssues =
383
+ consoleCount +
384
+ todoCount +
385
+ localhostCount +
386
+ placeholderCount +
387
+ commentedOutBlocks +
388
+ unusedImportCount +
389
+ aiCommentCount +
390
+ loremCount;
391
+
392
+ return {
393
+ id: "leftovers",
394
+ title: "AI LEFTOVERS",
395
+ score,
396
+ severity: severityFromScore(score),
397
+ totalIssues,
398
+ summary:
399
+ totalIssues > 0
400
+ ? `${totalIssues} leftover signals found (console logs, TODOs, placeholders, or dead comments).`
401
+ : "No common AI leftovers detected.",
402
+ metrics: {
403
+ consoleCount,
404
+ todoCount,
405
+ aiTodoCount,
406
+ localhostCount,
407
+ placeholderCount,
408
+ loremCount,
409
+ aiCommentCount,
410
+ commentedOutBlocks,
411
+ commentedOutLines,
412
+ unusedImportCount
413
+ },
414
+ recommendations: [
415
+ "Replace console.* with project logging utilities.",
416
+ "Move TODO/FIXME notes into issues and remove placeholder comments.",
417
+ "Remove hardcoded localhost/credentials and use environment configuration.",
418
+ "Delete commented-out code and unused imports."
419
+ ],
420
+ findings
421
+ };
422
+ }