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.
- package/README.md +207 -22
- package/dist/__tests__/parser.test.d.ts +2 -0
- package/dist/__tests__/parser.test.d.ts.map +1 -0
- package/dist/__tests__/parser.test.js +56 -0
- package/dist/__tests__/performanceBudget.test.d.ts +2 -0
- package/dist/__tests__/performanceBudget.test.d.ts.map +1 -0
- package/dist/__tests__/performanceBudget.test.js +91 -0
- package/dist/__tests__/prComments.test.d.ts +2 -0
- package/dist/__tests__/prComments.test.d.ts.map +1 -0
- package/dist/__tests__/prComments.test.js +118 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +10 -1
- package/dist/cli.js +106 -1
- package/dist/detectors/index.d.ts +1 -0
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +2 -0
- package/dist/detectors/serverComponents.d.ts +11 -0
- package/dist/detectors/serverComponents.d.ts.map +1 -0
- package/dist/detectors/serverComponents.js +222 -0
- package/dist/docGenerator.d.ts +37 -0
- package/dist/docGenerator.d.ts.map +1 -0
- package/dist/docGenerator.js +306 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/interactiveFixer.d.ts +20 -0
- package/dist/interactiveFixer.d.ts.map +1 -0
- package/dist/interactiveFixer.js +178 -0
- package/dist/performanceBudget.d.ts +54 -0
- package/dist/performanceBudget.d.ts.map +1 -0
- package/dist/performanceBudget.js +218 -0
- package/dist/prComments.d.ts +47 -0
- package/dist/prComments.d.ts.map +1 -0
- package/dist/prComments.js +233 -0
- package/dist/types/index.d.ts +2 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +2 -0
- 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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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"}
|