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,247 @@
1
+ import path from "node:path";
2
+ import {
3
+ collectIdentifiers,
4
+ parseAst,
5
+ severityFromScore,
6
+ traverseAst,
7
+ scoreFromRatio
8
+ } from "./utils.js";
9
+
10
+ const IDENTIFIER_STYLES = {
11
+ camelCase: /^[a-z][a-zA-Z0-9]*$/,
12
+ snake_case: /^[a-z][a-z0-9]*(?:_[a-z0-9]+)+$/,
13
+ PascalCase: /^[A-Z][a-zA-Z0-9]*$/,
14
+ SCREAMING_SNAKE: /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$/
15
+ };
16
+
17
+ const FILE_STYLES = {
18
+ "kebab-case": /^[a-z0-9]+(?:-[a-z0-9]+)*$/,
19
+ snake_case: /^[a-z0-9]+(?:_[a-z0-9]+)*$/,
20
+ camelCase: /^[a-z][a-zA-Z0-9]*$/,
21
+ PascalCase: /^[A-Z][a-zA-Z0-9]*$/
22
+ };
23
+
24
+ function styleOf(value, map) {
25
+ for (const [name, regex] of Object.entries(map)) {
26
+ if (regex.test(value)) {
27
+ return name;
28
+ }
29
+ }
30
+ return null;
31
+ }
32
+
33
+ function dominantFromCounts(counts) {
34
+ const entries = Object.entries(counts);
35
+ const total = entries.reduce((sum, [, value]) => sum + value, 0);
36
+ if (!total) {
37
+ return { name: null, total: 0, ratio: 0, entries: [] };
38
+ }
39
+
40
+ const sorted = entries.sort((a, b) => b[1] - a[1]);
41
+ const [dominantName, dominantCount] = sorted[0];
42
+
43
+ return {
44
+ name: dominantName,
45
+ total,
46
+ ratio: dominantCount / total,
47
+ entries: sorted
48
+ };
49
+ }
50
+
51
+ function extractExportedComponentNames(content) {
52
+ const ast = parseAst(content);
53
+ const names = new Set();
54
+
55
+ if (!ast) {
56
+ const fallback = content.matchAll(
57
+ /export\s+(?:default\s+)?(?:function|class|const)\s+([A-Z][A-Za-z0-9_]*)/g
58
+ );
59
+ for (const match of fallback) {
60
+ names.add(match[1]);
61
+ }
62
+ return [...names];
63
+ }
64
+
65
+ traverseAst(ast, (node) => {
66
+ if (node.type === "ExportNamedDeclaration" || node.type === "ExportDefaultDeclaration") {
67
+ const declaration = node.declaration;
68
+ if (!declaration) {
69
+ return;
70
+ }
71
+
72
+ if (
73
+ (declaration.type === "FunctionDeclaration" || declaration.type === "ClassDeclaration") &&
74
+ declaration.id?.name?.[0] === declaration.id?.name?.[0]?.toUpperCase()
75
+ ) {
76
+ names.add(declaration.id.name);
77
+ }
78
+
79
+ if (declaration.type === "VariableDeclaration") {
80
+ for (const item of declaration.declarations || []) {
81
+ if (item.id?.type === "Identifier") {
82
+ const name = item.id.name;
83
+ if (name[0] === name[0].toUpperCase()) {
84
+ names.add(name);
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
90
+ });
91
+
92
+ return [...names];
93
+ }
94
+
95
+ function normalizeName(input) {
96
+ return input.replace(/[-_]/g, "").toLowerCase();
97
+ }
98
+
99
+ export function analyzeNaming(files) {
100
+ const identifierCounts = {
101
+ camelCase: 0,
102
+ snake_case: 0,
103
+ PascalCase: 0,
104
+ SCREAMING_SNAKE: 0
105
+ };
106
+
107
+ const identifierFilesByStyle = {
108
+ camelCase: new Set(),
109
+ snake_case: new Set(),
110
+ PascalCase: new Set(),
111
+ SCREAMING_SNAKE: new Set()
112
+ };
113
+
114
+ const directoryFileStyles = new Map();
115
+ const componentMismatches = [];
116
+
117
+ for (const file of files) {
118
+ const identifiers = collectIdentifiers(file.content);
119
+ for (const identifier of identifiers) {
120
+ const style = styleOf(identifier, IDENTIFIER_STYLES);
121
+ if (!style) {
122
+ continue;
123
+ }
124
+
125
+ identifierCounts[style] += 1;
126
+ identifierFilesByStyle[style].add(file.relativePath);
127
+ }
128
+
129
+ const baseName = path.basename(file.relativePath, file.extension);
130
+ const fileStyle = styleOf(baseName, FILE_STYLES);
131
+ const dirName = path.dirname(file.relativePath);
132
+ if (!directoryFileStyles.has(dirName)) {
133
+ directoryFileStyles.set(dirName, new Map());
134
+ }
135
+ const styleMap = directoryFileStyles.get(dirName);
136
+ styleMap.set(fileStyle || "other", (styleMap.get(fileStyle || "other") || 0) + 1);
137
+
138
+ if ([".jsx", ".tsx", ".vue", ".svelte"].includes(file.extension)) {
139
+ const componentNames = extractExportedComponentNames(file.content);
140
+ for (const componentName of componentNames) {
141
+ if (normalizeName(componentName) !== normalizeName(baseName)) {
142
+ componentMismatches.push({
143
+ file: file.relativePath,
144
+ component: componentName
145
+ });
146
+ }
147
+ }
148
+ }
149
+ }
150
+
151
+ const dominant = dominantFromCounts(identifierCounts);
152
+ const dominantStyle = dominant.name;
153
+
154
+ const stylePercentages = dominant.entries.map(([style, count]) => ({
155
+ style,
156
+ count,
157
+ percent: dominant.total ? Math.round((count / dominant.total) * 100) : 0,
158
+ files: identifierFilesByStyle[style].size
159
+ }));
160
+
161
+ const minorityFiles = new Set();
162
+ for (const [style, filesForStyle] of Object.entries(identifierFilesByStyle)) {
163
+ if (!dominantStyle || style === dominantStyle) {
164
+ continue;
165
+ }
166
+
167
+ for (const file of filesForStyle) {
168
+ minorityFiles.add(file);
169
+ }
170
+ }
171
+
172
+ const mixedDirectories = [];
173
+ for (const [dirName, styleMap] of directoryFileStyles.entries()) {
174
+ if (styleMap.size > 1) {
175
+ mixedDirectories.push({
176
+ directory: dirName,
177
+ styles: [...styleMap.entries()].map(([style, count]) => `${style} (${count})`)
178
+ });
179
+ }
180
+ }
181
+
182
+ const imbalanceRatio = dominant.total ? 1 - dominant.ratio : 0;
183
+ const issueCount =
184
+ minorityFiles.size + mixedDirectories.length + componentMismatches.length;
185
+ const score = Math.min(10, scoreFromRatio(imbalanceRatio + issueCount / 120, 10));
186
+
187
+ const findings = [];
188
+ if (minorityFiles.size > 0 && dominantStyle) {
189
+ findings.push({
190
+ severity: score >= 7 ? "high" : "medium",
191
+ message: `${minorityFiles.size} files use a minority naming convention instead of ${dominantStyle}.`,
192
+ files: [...minorityFiles].slice(0, 15)
193
+ });
194
+ }
195
+
196
+ if (mixedDirectories.length > 0) {
197
+ findings.push({
198
+ severity: "medium",
199
+ message: `${mixedDirectories.length} directories mix filename conventions.`,
200
+ files: mixedDirectories.map((item) => item.directory).slice(0, 15)
201
+ });
202
+ }
203
+
204
+ if (componentMismatches.length > 0) {
205
+ findings.push({
206
+ severity: "medium",
207
+ message: `${componentMismatches.length} components do not match filename conventions.`,
208
+ files: componentMismatches.map((item) => item.file).slice(0, 15)
209
+ });
210
+ }
211
+
212
+ return {
213
+ id: "naming",
214
+ title: "NAMING INCONSISTENCY",
215
+ score,
216
+ severity: severityFromScore(score),
217
+ totalIssues: issueCount,
218
+ summary:
219
+ dominantStyle && dominant.total
220
+ ? `${dominantStyle} is dominant (${Math.round(dominant.ratio * 100)}%), but naming conventions are mixed.`
221
+ : "Not enough identifiers found to determine a dominant naming convention.",
222
+ metrics: {
223
+ identifierStyles: stylePercentages,
224
+ dominantIdentifierStyle: dominantStyle,
225
+ mixedDirectoryCount: mixedDirectories.length,
226
+ componentMismatchCount: componentMismatches.length
227
+ },
228
+ recommendations: [
229
+ dominantStyle
230
+ ? `Standardize function and variable names on ${dominantStyle}.`
231
+ : "Pick one naming convention and enforce it consistently.",
232
+ "Keep one filename style per directory (kebab-case recommended for files).",
233
+ "Make component names match their filenames."
234
+ ],
235
+ findings,
236
+ details: {
237
+ minorityFiles: [...minorityFiles].slice(0, 50),
238
+ mixedDirectories: mixedDirectories.slice(0, 25),
239
+ componentMismatches: componentMismatches.slice(0, 25)
240
+ },
241
+ preferences: {
242
+ namingStyle: dominantStyle || "camelCase",
243
+ fileNamingStyle: mixedDirectories.length ? "kebab-case" : null
244
+ }
245
+ };
246
+ }
247
+
@@ -0,0 +1,381 @@
1
+ import {
2
+ collectImportSpecifiers,
3
+ packageRoot,
4
+ severityFromScore,
5
+ scoreFromRatio,
6
+ countMatches,
7
+ parseAst
8
+ } from "./utils.js";
9
+
10
+ const HTTP_CLIENT_PACKAGES = new Set([
11
+ "axios",
12
+ "got",
13
+ "node-fetch",
14
+ "ky",
15
+ "superagent",
16
+ "undici"
17
+ ]);
18
+ const HTTP_CLIENT_IDENTIFIERS = new Set(["axios", "got", "ky", "superagent"]);
19
+
20
+ const STATE_LIBS = [
21
+ "redux",
22
+ "@reduxjs/toolkit",
23
+ "zustand",
24
+ "jotai",
25
+ "recoil",
26
+ "mobx"
27
+ ];
28
+
29
+ const DATA_FETCHING_LIBS = [
30
+ "swr",
31
+ "react-query",
32
+ "@tanstack/react-query",
33
+ "@trpc/client",
34
+ "@trpc/react-query"
35
+ ];
36
+
37
+ const STYLING_LIBS = [
38
+ "styled-components",
39
+ "@emotion/react",
40
+ "@emotion/styled",
41
+ "tailwindcss"
42
+ ];
43
+
44
+ function mapToObject(map) {
45
+ const output = {};
46
+ for (const [key, value] of map.entries()) {
47
+ output[key] = value;
48
+ }
49
+ return output;
50
+ }
51
+
52
+ function dominantKey(usageMap) {
53
+ const entries = [...usageMap.entries()].sort((a, b) => b[1].size - a[1].size);
54
+ if (!entries.length) {
55
+ return null;
56
+ }
57
+ return entries[0][0];
58
+ }
59
+
60
+ function addUsage(usageMap, key, filePath) {
61
+ if (!usageMap.has(key)) {
62
+ usageMap.set(key, new Set());
63
+ }
64
+ usageMap.get(key).add(filePath);
65
+ }
66
+
67
+ function detectHttpCalls(content) {
68
+ const ast = parseAst(content);
69
+ const used = new Set();
70
+
71
+ if (!ast) {
72
+ if (/\bfetch\s*\(/.test(content)) {
73
+ used.add("fetch");
74
+ }
75
+ return used;
76
+ }
77
+
78
+ function visit(node) {
79
+ if (!node || typeof node !== "object") {
80
+ return;
81
+ }
82
+
83
+ if (node.type === "CallExpression") {
84
+ const callee = node.callee;
85
+ if (callee?.type === "Identifier") {
86
+ if (callee.name === "fetch") {
87
+ used.add("fetch");
88
+ }
89
+ if (HTTP_CLIENT_IDENTIFIERS.has(callee.name)) {
90
+ used.add(callee.name);
91
+ }
92
+ } else if (
93
+ callee?.type === "MemberExpression" &&
94
+ !callee.computed &&
95
+ callee.object?.type === "Identifier"
96
+ ) {
97
+ if (HTTP_CLIENT_IDENTIFIERS.has(callee.object.name)) {
98
+ used.add(callee.object.name);
99
+ }
100
+ if (callee.object.name === "undici") {
101
+ used.add("undici");
102
+ }
103
+ }
104
+ }
105
+
106
+ for (const key of Object.keys(node)) {
107
+ const value = node[key];
108
+ if (Array.isArray(value)) {
109
+ for (const child of value) {
110
+ if (child?.type) {
111
+ visit(child);
112
+ }
113
+ }
114
+ } else if (value?.type) {
115
+ visit(value);
116
+ }
117
+ }
118
+ }
119
+
120
+ visit(ast);
121
+ return used;
122
+ }
123
+
124
+ function extractImportStyles(content) {
125
+ const importLines = content.match(/import\s+[^;\n]+/g) || [];
126
+ const libraryStyles = new Map();
127
+
128
+ for (const line of importLines) {
129
+ const fromMatch = line.match(/from\s+["'`]([^"'`]+)["'`]/);
130
+ if (!fromMatch) {
131
+ continue;
132
+ }
133
+
134
+ const lib = fromMatch[1];
135
+ const style = /\{[^}]+\}/.test(line)
136
+ ? "named"
137
+ : /^import\s+[\w$]+\s+from/.test(line)
138
+ ? "default"
139
+ : "other";
140
+
141
+ if (!libraryStyles.has(lib)) {
142
+ libraryStyles.set(lib, new Set());
143
+ }
144
+ libraryStyles.get(lib).add(style);
145
+ }
146
+
147
+ const mixedLibs = [];
148
+ for (const [lib, styles] of libraryStyles.entries()) {
149
+ if (styles.size > 1) {
150
+ mixedLibs.push(lib);
151
+ }
152
+ }
153
+
154
+ return mixedLibs;
155
+ }
156
+
157
+ export function analyzePatterns(files, context = {}) {
158
+ const httpUsage = new Map();
159
+ const stateUsage = new Map();
160
+ const dataFetchingUsage = new Map();
161
+ const stylingUsage = new Map();
162
+
163
+ let asyncAwaitOps = 0;
164
+ let thenChains = 0;
165
+ let callbackStyle = 0;
166
+ let filesUsingImport = 0;
167
+ let filesUsingRequire = 0;
168
+ const importStyleMixedLibs = new Set();
169
+
170
+ for (const file of files) {
171
+ const content = file.content;
172
+ const imports = collectImportSpecifiers(content);
173
+ const importedPackages = new Set();
174
+
175
+ for (const spec of imports) {
176
+ const pkg = packageRoot(spec);
177
+ if (!pkg) {
178
+ continue;
179
+ }
180
+
181
+ importedPackages.add(pkg);
182
+ if (HTTP_CLIENT_PACKAGES.has(pkg)) {
183
+ addUsage(httpUsage, pkg, file.relativePath);
184
+ }
185
+
186
+ if (STATE_LIBS.includes(pkg)) {
187
+ addUsage(stateUsage, pkg, file.relativePath);
188
+ }
189
+
190
+ if (DATA_FETCHING_LIBS.includes(pkg)) {
191
+ addUsage(dataFetchingUsage, pkg, file.relativePath);
192
+ }
193
+
194
+ if (STYLING_LIBS.includes(pkg)) {
195
+ addUsage(stylingUsage, pkg, file.relativePath);
196
+ }
197
+ }
198
+
199
+ const httpCalls = detectHttpCalls(content);
200
+ const fetchIsPolyfilled =
201
+ importedPackages.has("node-fetch") || importedPackages.has("undici");
202
+ for (const client of httpCalls) {
203
+ if (client === "fetch" && fetchIsPolyfilled) {
204
+ continue;
205
+ }
206
+ addUsage(httpUsage, client, file.relativePath);
207
+ }
208
+
209
+ if (/\buseState\s*\(/.test(content)) {
210
+ addUsage(stateUsage, "useState", file.relativePath);
211
+ }
212
+
213
+ if (/\buseReducer\s*\(/.test(content)) {
214
+ addUsage(stateUsage, "useReducer", file.relativePath);
215
+ }
216
+
217
+ if (/\bcreateContext\s*\(/.test(content) || /\buseContext\s*\(/.test(content)) {
218
+ addUsage(stateUsage, "context", file.relativePath);
219
+ }
220
+
221
+ if (/\bfetch\s*\(/.test(content)) {
222
+ addUsage(dataFetchingUsage, "raw-fetch", file.relativePath);
223
+ }
224
+
225
+ if (/style\s*=\s*\{\{/.test(content)) {
226
+ addUsage(stylingUsage, "inline-styles", file.relativePath);
227
+ }
228
+
229
+ if (/\.module\.(css|scss|sass|less)["'`]/.test(content)) {
230
+ addUsage(stylingUsage, "css-modules", file.relativePath);
231
+ }
232
+
233
+ if (/class(Name)?\s*=\s*["'`][^"'`]*(?:text-|bg-|flex|grid|px-|py-|mx-|my-)/.test(content)) {
234
+ addUsage(stylingUsage, "tailwind-utility-classes", file.relativePath);
235
+ }
236
+
237
+ asyncAwaitOps += countMatches(content, /\bawait\b/g);
238
+ thenChains += countMatches(content, /\.then\s*\(/g);
239
+ callbackStyle += countMatches(content, /\bfunction\s*\([^)]*(?:err|error)[^)]*\)/g);
240
+
241
+ if (/\bimport\s+/.test(content)) {
242
+ filesUsingImport += 1;
243
+ }
244
+ if (/\brequire\s*\(/.test(content)) {
245
+ filesUsingRequire += 1;
246
+ }
247
+
248
+ for (const lib of extractImportStyles(content)) {
249
+ importStyleMixedLibs.add(lib);
250
+ }
251
+ }
252
+
253
+ const packageJson = context.packageJson || {};
254
+ const deps = {
255
+ ...(packageJson.dependencies || {}),
256
+ ...(packageJson.devDependencies || {})
257
+ };
258
+
259
+ const installedStateLibs = STATE_LIBS.filter((lib) => lib in deps);
260
+ for (const lib of installedStateLibs) {
261
+ if (!stateUsage.has(lib)) {
262
+ stateUsage.set(lib, new Set());
263
+ }
264
+ }
265
+
266
+ const installedStylingLibs = STYLING_LIBS.filter((lib) => lib in deps);
267
+ for (const lib of installedStylingLibs) {
268
+ if (!stylingUsage.has(lib)) {
269
+ stylingUsage.set(lib, new Set());
270
+ }
271
+ }
272
+
273
+ const findings = [];
274
+
275
+ if (httpUsage.size > 1) {
276
+ const detail = [...httpUsage.entries()]
277
+ .map(([name, fileSet]) => `${name} (${fileSet.size} files)`)
278
+ .join(", ");
279
+ findings.push({
280
+ severity: "high",
281
+ message: `Multiple HTTP clients detected: ${detail}`,
282
+ files: [...new Set([...httpUsage.values()].flatMap((set) => [...set]))].slice(0, 20)
283
+ });
284
+ }
285
+
286
+ if (stateUsage.size > 2) {
287
+ findings.push({
288
+ severity: "medium",
289
+ message: `State management patterns are mixed across ${stateUsage.size} approaches.`,
290
+ files: [...new Set([...stateUsage.values()].flatMap((set) => [...set]))].slice(0, 20)
291
+ });
292
+ }
293
+
294
+ const asyncTotal = asyncAwaitOps + thenChains + callbackStyle;
295
+ if (thenChains > 0 && asyncAwaitOps > 0) {
296
+ findings.push({
297
+ severity: "medium",
298
+ message: `Mixed async styles: async/await (${asyncAwaitOps}) and .then() chains (${thenChains}).`
299
+ });
300
+ }
301
+
302
+ if (filesUsingImport > 0 && filesUsingRequire > 0) {
303
+ findings.push({
304
+ severity: "medium",
305
+ message: `Mixed module systems: ES modules in ${filesUsingImport} files and require() in ${filesUsingRequire} files.`
306
+ });
307
+ }
308
+
309
+ if (importStyleMixedLibs.size > 0) {
310
+ findings.push({
311
+ severity: "low",
312
+ message: `${importStyleMixedLibs.size} libraries are imported with both default and named styles.`
313
+ });
314
+ }
315
+
316
+ if (stylingUsage.size > 2) {
317
+ findings.push({
318
+ severity: "medium",
319
+ message: `Multiple styling approaches detected (${stylingUsage.size} patterns).`
320
+ });
321
+ }
322
+
323
+ if (dataFetchingUsage.size > 1) {
324
+ findings.push({
325
+ severity: "medium",
326
+ message: `Data fetching is split across ${dataFetchingUsage.size} patterns.`
327
+ });
328
+ }
329
+
330
+ const inconsistencySignals =
331
+ Math.max(0, httpUsage.size - 1) +
332
+ Math.max(0, stateUsage.size - 2) +
333
+ Math.max(0, stylingUsage.size - 2) +
334
+ (filesUsingImport > 0 && filesUsingRequire > 0 ? 1 : 0) +
335
+ (thenChains > 0 && asyncAwaitOps > 0 ? 1 : 0) +
336
+ Math.max(0, dataFetchingUsage.size - 1);
337
+
338
+ const score = Math.min(10, scoreFromRatio(inconsistencySignals / 8 + (asyncTotal ? thenChains / asyncTotal : 0), 10));
339
+
340
+ const dominantHttpClient = dominantKey(httpUsage);
341
+ const preferredHttpClient = dominantHttpClient || "fetch";
342
+ const asyncStyle = asyncAwaitOps >= thenChains ? "async-await" : "then-chains";
343
+
344
+ return {
345
+ id: "patterns",
346
+ title: "PATTERN INCONSISTENCY",
347
+ score,
348
+ severity: severityFromScore(score),
349
+ totalIssues: findings.length,
350
+ summary:
351
+ findings.length > 0
352
+ ? `Detected ${findings.length} pattern inconsistency signals across the codebase.`
353
+ : "No major pattern inconsistencies detected.",
354
+ metrics: {
355
+ httpClients: mapToObject(new Map([...httpUsage.entries()].map(([k, v]) => [k, v.size]))),
356
+ stateManagement: mapToObject(new Map([...stateUsage.entries()].map(([k, v]) => [k, v.size]))),
357
+ dataFetching: mapToObject(new Map([...dataFetchingUsage.entries()].map(([k, v]) => [k, v.size]))),
358
+ styling: mapToObject(new Map([...stylingUsage.entries()].map(([k, v]) => [k, v.size]))),
359
+ asyncAwaitOps,
360
+ thenChains,
361
+ callbackStyle,
362
+ filesUsingImport,
363
+ filesUsingRequire
364
+ },
365
+ recommendations: [
366
+ dominantHttpClient
367
+ ? `Standardize HTTP requests on ${dominantHttpClient}.`
368
+ : "No explicit HTTP client detected. Keep one client choice once network calls are added.",
369
+ `Prefer ${asyncStyle === "async-await" ? "async/await" : ".then() chains"} for async consistency.`,
370
+ "Use one module system (ES modules recommended).",
371
+ "Reduce mixed styling and data-fetching patterns."
372
+ ],
373
+ findings,
374
+ preferences: {
375
+ httpClient: preferredHttpClient,
376
+ asyncStyle,
377
+ importStyle: filesUsingImport >= filesUsingRequire ? "esm" : "cjs"
378
+ }
379
+ };
380
+ }
381
+