react-code-smell-detector 1.4.2 → 1.5.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.
Files changed (38) hide show
  1. package/README.md +207 -22
  2. package/dist/__tests__/parser.test.d.ts +2 -0
  3. package/dist/__tests__/parser.test.d.ts.map +1 -0
  4. package/dist/__tests__/parser.test.js +56 -0
  5. package/dist/__tests__/performanceBudget.test.d.ts +2 -0
  6. package/dist/__tests__/performanceBudget.test.d.ts.map +1 -0
  7. package/dist/__tests__/performanceBudget.test.js +91 -0
  8. package/dist/__tests__/prComments.test.d.ts +2 -0
  9. package/dist/__tests__/prComments.test.d.ts.map +1 -0
  10. package/dist/__tests__/prComments.test.js +118 -0
  11. package/dist/analyzer.d.ts.map +1 -1
  12. package/dist/analyzer.js +10 -1
  13. package/dist/cli.js +106 -1
  14. package/dist/detectors/index.d.ts +1 -0
  15. package/dist/detectors/index.d.ts.map +1 -1
  16. package/dist/detectors/index.js +2 -0
  17. package/dist/detectors/serverComponents.d.ts +11 -0
  18. package/dist/detectors/serverComponents.d.ts.map +1 -0
  19. package/dist/detectors/serverComponents.js +222 -0
  20. package/dist/docGenerator.d.ts +37 -0
  21. package/dist/docGenerator.d.ts.map +1 -0
  22. package/dist/docGenerator.js +306 -0
  23. package/dist/index.d.ts +4 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +4 -0
  26. package/dist/interactiveFixer.d.ts +20 -0
  27. package/dist/interactiveFixer.d.ts.map +1 -0
  28. package/dist/interactiveFixer.js +178 -0
  29. package/dist/performanceBudget.d.ts +54 -0
  30. package/dist/performanceBudget.d.ts.map +1 -0
  31. package/dist/performanceBudget.js +218 -0
  32. package/dist/prComments.d.ts +47 -0
  33. package/dist/prComments.d.ts.map +1 -0
  34. package/dist/prComments.js +233 -0
  35. package/dist/types/index.d.ts +2 -1
  36. package/dist/types/index.d.ts.map +1 -1
  37. package/dist/types/index.js +2 -0
  38. package/package.json +10 -4
@@ -0,0 +1,306 @@
1
+ import path from 'path';
2
+ import fs from 'fs/promises';
3
+ /**
4
+ * Generate documentation from component analysis
5
+ */
6
+ export async function generateComponentDocs(result, rootDir, options = { format: 'markdown' }) {
7
+ const docs = extractComponentDocs(result, rootDir);
8
+ switch (options.format) {
9
+ case 'html':
10
+ return generateHTMLDocs(docs, result, options);
11
+ case 'json':
12
+ return JSON.stringify(docs, null, 2);
13
+ case 'markdown':
14
+ default:
15
+ return generateMarkdownDocs(docs, result, options);
16
+ }
17
+ }
18
+ /**
19
+ * Extract documentation data from analysis
20
+ */
21
+ function extractComponentDocs(result, rootDir) {
22
+ const docs = [];
23
+ for (const file of result.files) {
24
+ for (const component of file.components) {
25
+ const relativePath = file.file.replace(rootDir, '').replace(/^\//, '');
26
+ const componentSmells = file.smells.filter(s => s.line >= component.startLine && s.line <= component.endLine);
27
+ // Determine complexity rating
28
+ const complexity = getComplexityRating(component);
29
+ const maintainability = getMaintainabilityRating(componentSmells.length, component.lineCount);
30
+ docs.push({
31
+ name: component.name,
32
+ file: file.file,
33
+ relativePath,
34
+ lineCount: component.lineCount,
35
+ props: [], // Would need parser enhancement to get prop names
36
+ hooks: {
37
+ useState: component.useStateCount,
38
+ useEffect: component.useEffectCount,
39
+ useMemo: component.useMemoCount,
40
+ useCallback: component.useCallbackCount,
41
+ useRef: 0, // Would need parser enhancement
42
+ custom: [],
43
+ },
44
+ smells: componentSmells,
45
+ metrics: {
46
+ complexity,
47
+ maintainability,
48
+ },
49
+ });
50
+ }
51
+ }
52
+ return docs.sort((a, b) => a.name.localeCompare(b.name));
53
+ }
54
+ /**
55
+ * Generate Markdown documentation
56
+ */
57
+ function generateMarkdownDocs(docs, result, options) {
58
+ let md = '# Component Documentation\n\n';
59
+ md += `*Generated by react-code-smell-detector*\n\n`;
60
+ md += `---\n\n`;
61
+ // Summary
62
+ md += '## Summary\n\n';
63
+ md += `| Metric | Value |\n`;
64
+ md += `|--------|-------|\n`;
65
+ md += `| Total Components | ${docs.length} |\n`;
66
+ md += `| Total Files | ${result.summary.totalFiles} |\n`;
67
+ md += `| Technical Debt Grade | ${result.debtScore.grade} |\n`;
68
+ md += `| Estimated Refactor Time | ${result.debtScore.estimatedRefactorTime} |\n\n`;
69
+ // Table of contents
70
+ md += '## Components\n\n';
71
+ if (options.groupByFolder) {
72
+ const grouped = groupByFolder(docs);
73
+ for (const [folder, folderDocs] of Object.entries(grouped)) {
74
+ md += `### šŸ“ ${folder || 'Root'}\n\n`;
75
+ for (const doc of folderDocs) {
76
+ md += formatComponentMarkdown(doc, options);
77
+ }
78
+ }
79
+ }
80
+ else {
81
+ for (const doc of docs) {
82
+ md += formatComponentMarkdown(doc, options);
83
+ }
84
+ }
85
+ // Index
86
+ md += '\n---\n\n';
87
+ md += '## Index\n\n';
88
+ md += '| Component | File | Lines | Hooks | Issues |\n';
89
+ md += '|-----------|------|-------|-------|--------|\n';
90
+ for (const doc of docs) {
91
+ const totalHooks = doc.hooks.useState + doc.hooks.useEffect + doc.hooks.useMemo + doc.hooks.useCallback;
92
+ md += `| [${doc.name}](#${doc.name.toLowerCase()}) | \`${doc.relativePath}\` | ${doc.lineCount} | ${totalHooks} | ${doc.smells.length} |\n`;
93
+ }
94
+ return md;
95
+ }
96
+ /**
97
+ * Format a single component as Markdown
98
+ */
99
+ function formatComponentMarkdown(doc, options) {
100
+ let md = `#### ${doc.name}\n\n`;
101
+ md += `šŸ“„ \`${doc.relativePath}\`\n\n`;
102
+ // Metrics table
103
+ if (options.includeMetrics !== false) {
104
+ md += `| Metric | Value |\n`;
105
+ md += `|--------|-------|\n`;
106
+ md += `| Lines | ${doc.lineCount} |\n`;
107
+ md += `| Complexity | ${doc.metrics.complexity} |\n`;
108
+ md += `| Maintainability | ${doc.metrics.maintainability} |\n`;
109
+ }
110
+ // Hooks
111
+ const hooks = [];
112
+ if (doc.hooks.useState > 0)
113
+ hooks.push(`useState (${doc.hooks.useState})`);
114
+ if (doc.hooks.useEffect > 0)
115
+ hooks.push(`useEffect (${doc.hooks.useEffect})`);
116
+ if (doc.hooks.useMemo > 0)
117
+ hooks.push(`useMemo (${doc.hooks.useMemo})`);
118
+ if (doc.hooks.useCallback > 0)
119
+ hooks.push(`useCallback (${doc.hooks.useCallback})`);
120
+ if (hooks.length > 0) {
121
+ md += `\n**Hooks:** ${hooks.join(', ')}\n`;
122
+ }
123
+ // Issues
124
+ if (options.includeSmells !== false && doc.smells.length > 0) {
125
+ md += `\n**Issues (${doc.smells.length}):**\n`;
126
+ for (const smell of doc.smells.slice(0, 5)) {
127
+ const emoji = smell.severity === 'error' ? 'šŸ”“' : smell.severity === 'warning' ? '🟔' : 'šŸ”µ';
128
+ md += `- ${emoji} ${smell.type}: ${smell.message}\n`;
129
+ }
130
+ if (doc.smells.length > 5) {
131
+ md += `- *... and ${doc.smells.length - 5} more*\n`;
132
+ }
133
+ }
134
+ md += '\n---\n\n';
135
+ return md;
136
+ }
137
+ /**
138
+ * Generate HTML documentation
139
+ */
140
+ function generateHTMLDocs(docs, result, options) {
141
+ return `<!DOCTYPE html>
142
+ <html lang="en">
143
+ <head>
144
+ <meta charset="UTF-8">
145
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
146
+ <title>Component Documentation</title>
147
+ <style>
148
+ * { box-sizing: border-box; margin: 0; padding: 0; }
149
+ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; color: #333; background: #f5f5f5; }
150
+ .container { max-width: 1200px; margin: 0 auto; padding: 20px; }
151
+ header { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; padding: 40px 20px; margin-bottom: 30px; border-radius: 10px; }
152
+ h1 { font-size: 2.5rem; margin-bottom: 10px; }
153
+ .subtitle { opacity: 0.9; }
154
+ .summary { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 20px; margin-bottom: 30px; }
155
+ .summary-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center; }
156
+ .summary-value { font-size: 2rem; font-weight: bold; color: #667eea; }
157
+ .summary-label { color: #666; font-size: 0.9rem; }
158
+ .components { display: grid; gap: 20px; }
159
+ .component-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
160
+ .component-name { font-size: 1.3rem; color: #333; margin-bottom: 5px; }
161
+ .component-path { color: #666; font-size: 0.85rem; font-family: monospace; background: #f0f0f0; padding: 2px 8px; border-radius: 4px; }
162
+ .metrics { display: flex; gap: 15px; margin: 15px 0; flex-wrap: wrap; }
163
+ .metric { background: #f0f0f0; padding: 5px 12px; border-radius: 20px; font-size: 0.85rem; }
164
+ .hooks { display: flex; gap: 10px; flex-wrap: wrap; margin: 10px 0; }
165
+ .hook { background: #e3f2fd; color: #1976d2; padding: 4px 10px; border-radius: 4px; font-size: 0.8rem; }
166
+ .issues { margin-top: 15px; }
167
+ .issue { padding: 8px; margin: 5px 0; border-radius: 4px; font-size: 0.9rem; }
168
+ .issue.error { background: #ffebee; color: #c62828; }
169
+ .issue.warning { background: #fff3e0; color: #ef6c00; }
170
+ .issue.info { background: #e3f2fd; color: #1565c0; }
171
+ .search { margin-bottom: 20px; }
172
+ .search input { width: 100%; padding: 12px; border: 1px solid #ddd; border-radius: 8px; font-size: 1rem; }
173
+ .grade { font-size: 3rem; }
174
+ footer { text-align: center; padding: 20px; color: #666; font-size: 0.85rem; }
175
+ </style>
176
+ </head>
177
+ <body>
178
+ <div class="container">
179
+ <header>
180
+ <h1>šŸ“š Component Documentation</h1>
181
+ <p class="subtitle">Generated by react-code-smell-detector</p>
182
+ </header>
183
+
184
+ <div class="summary">
185
+ <div class="summary-card">
186
+ <div class="summary-value">${docs.length}</div>
187
+ <div class="summary-label">Components</div>
188
+ </div>
189
+ <div class="summary-card">
190
+ <div class="summary-value">${result.summary.totalFiles}</div>
191
+ <div class="summary-label">Files</div>
192
+ </div>
193
+ <div class="summary-card">
194
+ <div class="summary-value grade">${result.debtScore.grade}</div>
195
+ <div class="summary-label">Debt Grade</div>
196
+ </div>
197
+ <div class="summary-card">
198
+ <div class="summary-value">${result.debtScore.score}</div>
199
+ <div class="summary-label">Score</div>
200
+ </div>
201
+ </div>
202
+
203
+ <div class="search">
204
+ <input type="text" placeholder="Search components..." id="search" onkeyup="filterComponents()">
205
+ </div>
206
+
207
+ <div class="components" id="components">
208
+ ${docs.map(doc => `
209
+ <div class="component-card" data-name="${doc.name.toLowerCase()}">
210
+ <div class="component-name">${doc.name}</div>
211
+ <span class="component-path">${doc.relativePath}</span>
212
+
213
+ <div class="metrics">
214
+ <span class="metric">šŸ“ ${doc.lineCount} lines</span>
215
+ <span class="metric">⚔ ${doc.metrics.complexity}</span>
216
+ <span class="metric">šŸ”§ ${doc.metrics.maintainability}</span>
217
+ </div>
218
+
219
+ <div class="hooks">
220
+ ${doc.hooks.useState > 0 ? `<span class="hook">useState Ɨ${doc.hooks.useState}</span>` : ''}
221
+ ${doc.hooks.useEffect > 0 ? `<span class="hook">useEffect Ɨ${doc.hooks.useEffect}</span>` : ''}
222
+ ${doc.hooks.useMemo > 0 ? `<span class="hook">useMemo Ɨ${doc.hooks.useMemo}</span>` : ''}
223
+ ${doc.hooks.useCallback > 0 ? `<span class="hook">useCallback Ɨ${doc.hooks.useCallback}</span>` : ''}
224
+ </div>
225
+
226
+ ${doc.smells.length > 0 ? `
227
+ <div class="issues">
228
+ ${doc.smells.slice(0, 3).map(s => `
229
+ <div class="issue ${s.severity}">${s.type}: ${s.message}</div>
230
+ `).join('')}
231
+ ${doc.smells.length > 3 ? `<div class="issue info">... and ${doc.smells.length - 3} more</div>` : ''}
232
+ </div>
233
+ ` : ''}
234
+ </div>
235
+ `).join('')}
236
+ </div>
237
+
238
+ <footer>
239
+ Generated by <a href="https://github.com/vsthakur101/react-code-smell-detector">react-code-smell-detector</a>
240
+ </footer>
241
+ </div>
242
+
243
+ <script>
244
+ function filterComponents() {
245
+ const query = document.getElementById('search').value.toLowerCase();
246
+ const cards = document.querySelectorAll('.component-card');
247
+ cards.forEach(card => {
248
+ const name = card.dataset.name;
249
+ card.style.display = name.includes(query) ? 'block' : 'none';
250
+ });
251
+ }
252
+ </script>
253
+ </body>
254
+ </html>`;
255
+ }
256
+ /**
257
+ * Group components by folder
258
+ */
259
+ function groupByFolder(docs) {
260
+ const grouped = {};
261
+ for (const doc of docs) {
262
+ const parts = doc.relativePath.split('/');
263
+ const folder = parts.length > 1 ? parts.slice(0, -1).join('/') : '';
264
+ if (!grouped[folder]) {
265
+ grouped[folder] = [];
266
+ }
267
+ grouped[folder].push(doc);
268
+ }
269
+ return grouped;
270
+ }
271
+ /**
272
+ * Get complexity rating based on component metrics
273
+ */
274
+ function getComplexityRating(component) {
275
+ const hookCount = component.useEffectCount + component.useStateCount +
276
+ component.useMemoCount + component.useCallbackCount;
277
+ if (component.lineCount > 300 || hookCount > 10)
278
+ return 'šŸ”“ High';
279
+ if (component.lineCount > 150 || hookCount > 5)
280
+ return '🟔 Medium';
281
+ return '🟢 Low';
282
+ }
283
+ /**
284
+ * Get maintainability rating based on smells and size
285
+ */
286
+ function getMaintainabilityRating(smellCount, lineCount) {
287
+ const ratio = smellCount / Math.max(lineCount / 50, 1);
288
+ if (ratio > 2 || smellCount > 5)
289
+ return 'šŸ”“ Poor';
290
+ if (ratio > 1 || smellCount > 2)
291
+ return '🟔 Fair';
292
+ return '🟢 Good';
293
+ }
294
+ /**
295
+ * Write documentation to file
296
+ */
297
+ export async function writeComponentDocs(result, rootDir, options) {
298
+ const content = await generateComponentDocs(result, rootDir, options);
299
+ const ext = options.format === 'html' ? 'html' : options.format === 'json' ? 'json' : 'md';
300
+ const filename = `COMPONENTS.${ext}`;
301
+ const outputPath = options.outputDir
302
+ ? path.join(options.outputDir, filename)
303
+ : path.join(rootDir, filename);
304
+ await fs.writeFile(outputPath, content, 'utf-8');
305
+ return outputPath;
306
+ }
package/dist/index.d.ts CHANGED
@@ -2,4 +2,8 @@ export { analyzeProject, type AnalyzerOptions } from './analyzer.js';
2
2
  export { reportResults, type ReporterOptions } from './reporter.js';
3
3
  export * from './types/index.js';
4
4
  export { parseFile, parseCode, type ParseResult, type ParsedComponent } from './parser/index.js';
5
+ export { runInteractiveFix, previewFixes, type InteractiveFixOptions } from './interactiveFixer.js';
6
+ export { generatePRComment, postPRComment, generateInlineComments, type PRCommentConfig } from './prComments.js';
7
+ export { loadBudget, checkBudget, formatBudgetReport, createBudgetConfig, type PerformanceBudget, type BudgetCheckResult } from './performanceBudget.js';
8
+ export { generateComponentDocs, writeComponentDocs, type DocGeneratorOptions } from './docGenerator.js';
5
9
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AACpE,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AACpE,cAAc,kBAAkB,CAAC;AACjC,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,WAAW,EAAE,KAAK,eAAe,EAAE,MAAM,mBAAmB,CAAC;AACjG,OAAO,EAAE,iBAAiB,EAAE,YAAY,EAAE,KAAK,qBAAqB,EAAE,MAAM,uBAAuB,CAAC;AACpG,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,sBAAsB,EAAE,KAAK,eAAe,EAAE,MAAM,iBAAiB,CAAC;AACjH,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,kBAAkB,EAAE,kBAAkB,EAAE,KAAK,iBAAiB,EAAE,KAAK,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AACzJ,OAAO,EAAE,qBAAqB,EAAE,kBAAkB,EAAE,KAAK,mBAAmB,EAAE,MAAM,mBAAmB,CAAC"}
package/dist/index.js CHANGED
@@ -2,3 +2,7 @@ export { analyzeProject } from './analyzer.js';
2
2
  export { reportResults } from './reporter.js';
3
3
  export * from './types/index.js';
4
4
  export { parseFile, parseCode } from './parser/index.js';
5
+ export { runInteractiveFix, previewFixes } from './interactiveFixer.js';
6
+ export { generatePRComment, postPRComment, generateInlineComments } from './prComments.js';
7
+ export { loadBudget, checkBudget, formatBudgetReport, createBudgetConfig } from './performanceBudget.js';
8
+ export { generateComponentDocs, writeComponentDocs } from './docGenerator.js';
@@ -0,0 +1,20 @@
1
+ import { CodeSmell } from './types/index.js';
2
+ export interface InteractiveFixOptions {
3
+ smells: CodeSmell[];
4
+ rootDir: string;
5
+ showDiff?: boolean;
6
+ }
7
+ export interface InteractiveFixResult {
8
+ applied: number;
9
+ skipped: number;
10
+ total: number;
11
+ }
12
+ /**
13
+ * Interactive fix mode - review and apply fixes one by one
14
+ */
15
+ export declare function runInteractiveFix(options: InteractiveFixOptions): Promise<InteractiveFixResult>;
16
+ /**
17
+ * Preview all fixable issues without applying
18
+ */
19
+ export declare function previewFixes(smells: CodeSmell[], rootDir: string): void;
20
+ //# sourceMappingURL=interactiveFixer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"interactiveFixer.d.ts","sourceRoot":"","sources":["../src/interactiveFixer.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAa,MAAM,kBAAkB,CAAC;AAIxD,MAAM,WAAW,qBAAqB;IACpC,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;CACf;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,OAAO,EAAE,qBAAqB,GAAG,OAAO,CAAC,oBAAoB,CAAC,CA6HrG;AAoCD;;GAEG;AACH,wBAAgB,YAAY,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CA6BvE"}
@@ -0,0 +1,178 @@
1
+ import chalk from 'chalk';
2
+ import fs from 'fs/promises';
3
+ import { isFixable, describeFixAction } from './fixer.js';
4
+ import * as readline from 'readline';
5
+ /**
6
+ * Interactive fix mode - review and apply fixes one by one
7
+ */
8
+ export async function runInteractiveFix(options) {
9
+ const { smells, rootDir, showDiff = true } = options;
10
+ const fixableSmells = smells.filter(isFixable);
11
+ if (fixableSmells.length === 0) {
12
+ console.log(chalk.yellow('\nNo auto-fixable issues found.\n'));
13
+ return { applied: 0, skipped: 0, total: 0 };
14
+ }
15
+ console.log(chalk.cyan(`\nšŸ”§ Interactive Fix Mode`));
16
+ console.log(chalk.dim(`Found ${fixableSmells.length} fixable issue(s). Review each one:\n`));
17
+ console.log(chalk.dim('Commands: [y]es, [n]o, [a]ll, [q]uit\n'));
18
+ const rl = readline.createInterface({
19
+ input: process.stdin,
20
+ output: process.stdout,
21
+ });
22
+ const question = (prompt) => {
23
+ return new Promise((resolve) => {
24
+ rl.question(prompt, resolve);
25
+ });
26
+ };
27
+ let applied = 0;
28
+ let skipped = 0;
29
+ let applyAll = false;
30
+ // Group smells by file for efficient processing
31
+ const smellsByFile = new Map();
32
+ for (const smell of fixableSmells) {
33
+ const existing = smellsByFile.get(smell.file) || [];
34
+ existing.push(smell);
35
+ smellsByFile.set(smell.file, existing);
36
+ }
37
+ for (const [file, fileSmells] of smellsByFile) {
38
+ let content = await fs.readFile(file, 'utf-8');
39
+ const lines = content.split('\n');
40
+ const relativePath = file.replace(rootDir, '').replace(/^\//, '');
41
+ // Sort by line descending to preserve line numbers when fixing
42
+ const sortedSmells = [...fileSmells].sort((a, b) => b.line - a.line);
43
+ for (const smell of sortedSmells) {
44
+ const lineIndex = smell.line - 1;
45
+ if (lineIndex < 0 || lineIndex >= lines.length) {
46
+ skipped++;
47
+ continue;
48
+ }
49
+ const originalLine = lines[lineIndex];
50
+ const fixedLine = getFixedLine(smell, originalLine);
51
+ if (fixedLine === originalLine) {
52
+ skipped++;
53
+ continue;
54
+ }
55
+ // Display the issue
56
+ console.log(chalk.white('─'.repeat(60)));
57
+ console.log(chalk.yellow(`${relativePath}:${smell.line}`));
58
+ console.log(chalk.red(` ${smell.type}: ${smell.message}`));
59
+ console.log(chalk.blue(` Fix: ${describeFixAction(smell.type)}`));
60
+ if (showDiff) {
61
+ console.log();
62
+ console.log(chalk.red(` - ${originalLine.trim()}`));
63
+ if (fixedLine !== null) {
64
+ console.log(chalk.green(` + ${fixedLine.trim()}`));
65
+ }
66
+ else {
67
+ console.log(chalk.green(` + (line removed)`));
68
+ }
69
+ console.log();
70
+ }
71
+ let shouldApply = applyAll;
72
+ if (!applyAll) {
73
+ const answer = await question(chalk.cyan('Apply this fix? [y/n/a/q]: '));
74
+ const choice = answer.toLowerCase().trim();
75
+ if (choice === 'q') {
76
+ console.log(chalk.yellow('\nQuitting interactive mode.\n'));
77
+ rl.close();
78
+ return { applied, skipped: skipped + (fixableSmells.length - applied - skipped), total: fixableSmells.length };
79
+ }
80
+ else if (choice === 'a') {
81
+ applyAll = true;
82
+ shouldApply = true;
83
+ }
84
+ else if (choice === 'y' || choice === 'yes') {
85
+ shouldApply = true;
86
+ }
87
+ else {
88
+ shouldApply = false;
89
+ }
90
+ }
91
+ if (shouldApply) {
92
+ if (fixedLine === null) {
93
+ lines.splice(lineIndex, 1);
94
+ }
95
+ else {
96
+ lines[lineIndex] = fixedLine;
97
+ }
98
+ applied++;
99
+ console.log(chalk.green(' āœ“ Applied'));
100
+ }
101
+ else {
102
+ skipped++;
103
+ console.log(chalk.dim(' ā—‹ Skipped'));
104
+ }
105
+ }
106
+ // Write back the file if any changes were made
107
+ if (applied > 0) {
108
+ await fs.writeFile(file, lines.join('\n'), 'utf-8');
109
+ }
110
+ }
111
+ rl.close();
112
+ console.log(chalk.white('\n' + '─'.repeat(60)));
113
+ console.log(chalk.green(`āœ“ Applied ${applied} fix(es)`));
114
+ console.log(chalk.dim(`ā—‹ Skipped ${skipped} issue(s)`));
115
+ console.log();
116
+ return { applied, skipped, total: fixableSmells.length };
117
+ }
118
+ /**
119
+ * Get the fixed version of a line
120
+ */
121
+ function getFixedLine(smell, line) {
122
+ switch (smell.type) {
123
+ case 'debug-statement':
124
+ return fixDebugStatement(line);
125
+ case 'js-var-usage':
126
+ return line.replace(/\bvar\s+/, 'let ');
127
+ case 'js-loose-equality':
128
+ return line
129
+ .replace(/([^=!])={2}([^=])/g, '$1===$2')
130
+ .replace(/!={1}([^=])/g, '!==$1');
131
+ case 'a11y-missing-alt':
132
+ return line.replace(/<img\s+(?![^>]*\balt\b)([^>]*?)(\/?>)/gi, '<img $1alt="" $2');
133
+ default:
134
+ return line;
135
+ }
136
+ }
137
+ function fixDebugStatement(line) {
138
+ const trimmed = line.trim();
139
+ if (/^console\.(log|debug|info|warn|error|trace|dir)\s*\(.*\);?\s*$/.test(trimmed)) {
140
+ return null;
141
+ }
142
+ if (/^debugger;?\s*$/.test(trimmed)) {
143
+ return null;
144
+ }
145
+ if (/console\.(log|debug|info|warn|error|trace|dir)\s*\(/.test(line)) {
146
+ return line.replace(/console\.(log|debug|info|warn|error|trace|dir)\s*\([^)]*\);?/g, '/* $& */');
147
+ }
148
+ return line;
149
+ }
150
+ /**
151
+ * Preview all fixable issues without applying
152
+ */
153
+ export function previewFixes(smells, rootDir) {
154
+ const fixableSmells = smells.filter(isFixable);
155
+ if (fixableSmells.length === 0) {
156
+ console.log(chalk.yellow('\nNo auto-fixable issues found.\n'));
157
+ return;
158
+ }
159
+ console.log(chalk.cyan(`\nšŸ“‹ Fixable Issues Preview (${fixableSmells.length} total)\n`));
160
+ const grouped = new Map();
161
+ for (const smell of fixableSmells) {
162
+ const existing = grouped.get(smell.type) || [];
163
+ existing.push(smell);
164
+ grouped.set(smell.type, existing);
165
+ }
166
+ for (const [type, typeSmells] of grouped) {
167
+ console.log(chalk.yellow(`\n${type} (${typeSmells.length})`));
168
+ console.log(chalk.dim(` Fix: ${describeFixAction(type)}`));
169
+ for (const smell of typeSmells.slice(0, 5)) {
170
+ const relativePath = smell.file.replace(rootDir, '').replace(/^\//, '');
171
+ console.log(chalk.white(` ${relativePath}:${smell.line}`));
172
+ }
173
+ if (typeSmells.length > 5) {
174
+ console.log(chalk.dim(` ... and ${typeSmells.length - 5} more`));
175
+ }
176
+ }
177
+ console.log();
178
+ }
@@ -0,0 +1,54 @@
1
+ import { AnalysisResult, AnalysisSummary, SmellType } from './types/index.js';
2
+ export interface PerformanceBudget {
3
+ maxTotalSmells?: number;
4
+ maxErrors?: number;
5
+ maxWarnings?: number;
6
+ minScore?: number;
7
+ minGrade?: 'A' | 'B' | 'C' | 'D' | 'F';
8
+ maxByType?: Partial<Record<SmellType, number>>;
9
+ maxCyclomaticComplexity?: number;
10
+ maxCognitiveComplexity?: number;
11
+ maxComponentLines?: number;
12
+ maxPropsCount?: number;
13
+ maxUseEffectsPerComponent?: number;
14
+ maxSmellsPerFile?: number;
15
+ maxNewSmells?: number;
16
+ allowedGrowthPercent?: number;
17
+ }
18
+ export interface BudgetCheckResult {
19
+ passed: boolean;
20
+ violations: BudgetViolation[];
21
+ summary: {
22
+ total: number;
23
+ passed: number;
24
+ failed: number;
25
+ };
26
+ }
27
+ export interface BudgetViolation {
28
+ rule: string;
29
+ actual: number | string;
30
+ threshold: number | string;
31
+ message: string;
32
+ severity: 'error' | 'warning';
33
+ }
34
+ /**
35
+ * Load performance budget from config file
36
+ */
37
+ export declare function loadBudget(configPath?: string): Promise<PerformanceBudget>;
38
+ /**
39
+ * Check analysis results against performance budget
40
+ */
41
+ export declare function checkBudget(result: AnalysisResult, budget: PerformanceBudget): BudgetCheckResult;
42
+ /**
43
+ * Format budget check result for console output
44
+ */
45
+ export declare function formatBudgetReport(result: BudgetCheckResult): string;
46
+ /**
47
+ * Create a default budget config file
48
+ */
49
+ export declare function createBudgetConfig(targetPath?: string): Promise<string>;
50
+ /**
51
+ * Compare current results with baseline for growth limits
52
+ */
53
+ export declare function checkGrowthLimits(current: AnalysisSummary, baseline: AnalysisSummary, budget: PerformanceBudget): BudgetViolation[];
54
+ //# sourceMappingURL=performanceBudget.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"performanceBudget.d.ts","sourceRoot":"","sources":["../src/performanceBudget.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,cAAc,EAAE,eAAe,EAAsB,SAAS,EAAiB,MAAM,kBAAkB,CAAC;AAEjH,MAAM,WAAW,iBAAiB;IAEhC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,GAAG,CAAC;IAGvC,SAAS,CAAC,EAAE,OAAO,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC,CAAC;IAG/C,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAGhC,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,yBAAyB,CAAC,EAAE,MAAM,CAAC;IAGnC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAG1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,OAAO,CAAC;IAChB,UAAU,EAAE,eAAe,EAAE,CAAC;IAC9B,OAAO,EAAE;QACP,KAAK,EAAE,MAAM,CAAC;QACd,MAAM,EAAE,MAAM,CAAC;QACf,MAAM,EAAE,MAAM,CAAC;KAChB,CAAC;CACH;AAED,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,GAAG,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,OAAO,GAAG,SAAS,CAAC;CAC/B;AAOD;;GAEG;AACH,wBAAsB,UAAU,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC,CAsChF;AAED;;GAEG;AACH,wBAAgB,WAAW,CACzB,MAAM,EAAE,cAAc,EACtB,MAAM,EAAE,iBAAiB,GACxB,iBAAiB,CA6GnB;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,MAAM,EAAE,iBAAiB,GAAG,MAAM,CAqBpE;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,UAAU,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAoB7E;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,EAAE,eAAe,EACxB,QAAQ,EAAE,eAAe,EACzB,MAAM,EAAE,iBAAiB,GACxB,eAAe,EAAE,CA6BnB"}