react-code-smell-detector 1.4.1 → 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 +347 -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 +34 -1
- package/dist/bundleAnalyzer.d.ts +25 -0
- package/dist/bundleAnalyzer.d.ts.map +1 -0
- package/dist/bundleAnalyzer.js +375 -0
- package/dist/cli.js +148 -1
- package/dist/customRules.d.ts +31 -0
- package/dist/customRules.d.ts.map +1 -0
- package/dist/customRules.js +289 -0
- package/dist/detectors/complexity.d.ts +0 -4
- package/dist/detectors/complexity.d.ts.map +1 -1
- package/dist/detectors/complexity.js +1 -1
- package/dist/detectors/deadCode.d.ts +0 -7
- package/dist/detectors/deadCode.d.ts.map +1 -1
- package/dist/detectors/deadCode.js +0 -24
- package/dist/detectors/index.d.ts +3 -2
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +4 -2
- 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/git.d.ts.map +1 -1
- package/dist/git.js +0 -7
- package/dist/graphGenerator.d.ts +34 -0
- package/dist/graphGenerator.d.ts.map +1 -0
- package/dist/graphGenerator.js +320 -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/reporter.js +2 -0
- package/dist/types/index.d.ts +7 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +10 -0
- package/package.json +10 -4
|
@@ -0,0 +1,320 @@
|
|
|
1
|
+
import _traverse from '@babel/traverse';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
const traverse = typeof _traverse === 'function' ? _traverse : _traverse.default;
|
|
4
|
+
/**
|
|
5
|
+
* Build dependency graph from parsed components
|
|
6
|
+
*/
|
|
7
|
+
export function buildDependencyGraph(files, rootDir) {
|
|
8
|
+
const nodes = new Map();
|
|
9
|
+
const edges = [];
|
|
10
|
+
// Create nodes
|
|
11
|
+
for (const file of files) {
|
|
12
|
+
const id = normalizeId(file.file, rootDir);
|
|
13
|
+
nodes.set(id, {
|
|
14
|
+
id,
|
|
15
|
+
file: file.file,
|
|
16
|
+
type: 'file',
|
|
17
|
+
imports: file.imports.map(imp => normalizeId(imp, rootDir)),
|
|
18
|
+
importedBy: [],
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
// Build edges
|
|
22
|
+
for (const file of files) {
|
|
23
|
+
const fromId = normalizeId(file.file, rootDir);
|
|
24
|
+
for (const imp of file.imports) {
|
|
25
|
+
const toId = normalizeId(imp, rootDir);
|
|
26
|
+
if (nodes.has(toId)) {
|
|
27
|
+
const toNode = nodes.get(toId);
|
|
28
|
+
toNode.importedBy.push(fromId);
|
|
29
|
+
edges.push({ from: fromId, to: toId, circular: false });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
// Detect circular dependencies
|
|
34
|
+
const circularDependencies = detectCircularDeps(nodes);
|
|
35
|
+
for (const cycle of circularDependencies) {
|
|
36
|
+
for (let i = 0; i < cycle.length; i++) {
|
|
37
|
+
const from = cycle[i];
|
|
38
|
+
const to = cycle[(i + 1) % cycle.length];
|
|
39
|
+
const edge = edges.find(e => e.from === from && e.to === to);
|
|
40
|
+
if (edge)
|
|
41
|
+
edge.circular = true;
|
|
42
|
+
const node = nodes.get(from);
|
|
43
|
+
if (node)
|
|
44
|
+
node.isCircular = true;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return { nodes, edges, circularDependencies };
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Generate SVG representation of dependency graph
|
|
51
|
+
*/
|
|
52
|
+
export function generateDependencyGraph(graph, width = 1200, height = 800) {
|
|
53
|
+
const nodeArray = Array.from(graph.nodes.values());
|
|
54
|
+
// Simple force-directed layout
|
|
55
|
+
const positions = calculateNodePositions(nodeArray, graph.edges, width, height);
|
|
56
|
+
let svg = `<svg width="${width}" height="${height}" xmlns="http://www.w3.org/2000/svg">\n`;
|
|
57
|
+
svg += `<defs>\n`;
|
|
58
|
+
svg += `<marker id="arrowhead" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">\n`;
|
|
59
|
+
svg += `<polygon points="0 0, 10 3.5, 0 7" fill="#666" />\n`;
|
|
60
|
+
svg += `</marker>\n`;
|
|
61
|
+
svg += `<marker id="arrowhead-circular" markerWidth="10" markerHeight="7" refX="9" refY="3.5" orient="auto">\n`;
|
|
62
|
+
svg += `<polygon points="0 0, 10 3.5, 0 7" fill="#d32f2f" />\n`;
|
|
63
|
+
svg += `</marker>\n`;
|
|
64
|
+
svg += `</defs>\n`;
|
|
65
|
+
// Draw edges
|
|
66
|
+
for (const edge of graph.edges) {
|
|
67
|
+
const fromPos = positions.get(edge.from);
|
|
68
|
+
const toPos = positions.get(edge.to);
|
|
69
|
+
if (fromPos && toPos) {
|
|
70
|
+
const color = edge.circular ? '#d32f2f' : '#999';
|
|
71
|
+
const marker = edge.circular ? 'url(#arrowhead-circular)' : 'url(#arrowhead)';
|
|
72
|
+
svg += `<line x1="${fromPos.x}" y1="${fromPos.y}" x2="${toPos.x}" y2="${toPos.y}" stroke="${color}" stroke-width="${edge.circular ? 2 : 1}" marker-end="${marker}" opacity="${edge.circular ? 1 : 0.6}" />\n`;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Draw nodes
|
|
76
|
+
for (const node of nodeArray) {
|
|
77
|
+
const pos = positions.get(node.id);
|
|
78
|
+
if (pos) {
|
|
79
|
+
const fillColor = node.isCircular ? '#d32f2f' : '#1976d2';
|
|
80
|
+
const r = 25;
|
|
81
|
+
svg += `<circle cx="${pos.x}" cy="${pos.y}" r="${r}" fill="${fillColor}" opacity="0.8" />\n`;
|
|
82
|
+
// Node label
|
|
83
|
+
const label = path.basename(node.file, path.extname(node.file));
|
|
84
|
+
svg += `<text x="${pos.x}" y="${pos.y}" text-anchor="middle" dy="0.3em" fill="white" font-size="11" font-weight="bold">\n`;
|
|
85
|
+
svg += label.length > 12 ? label.substring(0, 10) + '..' : label;
|
|
86
|
+
svg += `</text>\n`;
|
|
87
|
+
// Circular indicator
|
|
88
|
+
if (node.isCircular) {
|
|
89
|
+
svg += `<circle cx="${pos.x}" cy="${pos.y}" r="${r + 3}" fill="none" stroke="#d32f2f" stroke-width="2" stroke-dasharray="5,5" />\n`;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
// Legend
|
|
94
|
+
svg += `<g transform="translate(20, 20)">\n`;
|
|
95
|
+
svg += `<rect x="0" y="0" width="200" height="100" fill="white" stroke="#ccc" rx="4" />\n`;
|
|
96
|
+
svg += `<text x="10" y="20" font-weight="bold" font-size="12">Legend</text>\n`;
|
|
97
|
+
svg += `<circle cx="20" cy="40" r="6" fill="#1976d2" />\n`;
|
|
98
|
+
svg += `<text x="35" y="45" font-size="11">Regular File</text>\n`;
|
|
99
|
+
svg += `<circle cx="20" cy="65" r="6" fill="#d32f2f" />\n`;
|
|
100
|
+
svg += `<text x="35" y="70" font-size="11">Circular Dependency</text>\n`;
|
|
101
|
+
svg += `</g>\n`;
|
|
102
|
+
svg += `</svg>`;
|
|
103
|
+
return svg;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Generate HTML report with dependency graph
|
|
107
|
+
*/
|
|
108
|
+
export function generateDependencyGraphHTML(graph, projectName, circularCount = 0) {
|
|
109
|
+
const svgContent = generateDependencyGraph(graph);
|
|
110
|
+
return `<!DOCTYPE html>
|
|
111
|
+
<html lang="en">
|
|
112
|
+
<head>
|
|
113
|
+
<meta charset="UTF-8">
|
|
114
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
115
|
+
<title>Dependency Graph - ${projectName}</title>
|
|
116
|
+
<style>
|
|
117
|
+
body {
|
|
118
|
+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
119
|
+
background: #f5f5f5;
|
|
120
|
+
margin: 0;
|
|
121
|
+
padding: 20px;
|
|
122
|
+
}
|
|
123
|
+
.container {
|
|
124
|
+
max-width: 1400px;
|
|
125
|
+
margin: 0 auto;
|
|
126
|
+
background: white;
|
|
127
|
+
border-radius: 8px;
|
|
128
|
+
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
129
|
+
padding: 30px;
|
|
130
|
+
}
|
|
131
|
+
h1 {
|
|
132
|
+
color: #1976d2;
|
|
133
|
+
margin: 0 0 10px 0;
|
|
134
|
+
}
|
|
135
|
+
.stats {
|
|
136
|
+
display: grid;
|
|
137
|
+
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
|
138
|
+
gap: 20px;
|
|
139
|
+
margin: 20px 0 30px 0;
|
|
140
|
+
}
|
|
141
|
+
.stat-card {
|
|
142
|
+
background: #f8f9fa;
|
|
143
|
+
border-left: 4px solid #1976d2;
|
|
144
|
+
padding: 15px;
|
|
145
|
+
border-radius: 4px;
|
|
146
|
+
}
|
|
147
|
+
.stat-card.warning {
|
|
148
|
+
border-left-color: #d32f2f;
|
|
149
|
+
}
|
|
150
|
+
.stat-label {
|
|
151
|
+
font-size: 12px;
|
|
152
|
+
color: #666;
|
|
153
|
+
text-transform: uppercase;
|
|
154
|
+
}
|
|
155
|
+
.stat-value {
|
|
156
|
+
font-size: 28px;
|
|
157
|
+
font-weight: bold;
|
|
158
|
+
color: #1976d2;
|
|
159
|
+
}
|
|
160
|
+
.stat-card.warning .stat-value {
|
|
161
|
+
color: #d32f2f;
|
|
162
|
+
}
|
|
163
|
+
.graph-container {
|
|
164
|
+
border: 1px solid #e0e0e0;
|
|
165
|
+
border-radius: 4px;
|
|
166
|
+
overflow: auto;
|
|
167
|
+
background: #fafafa;
|
|
168
|
+
margin: 20px 0;
|
|
169
|
+
}
|
|
170
|
+
svg {
|
|
171
|
+
display: block;
|
|
172
|
+
margin: 0 auto;
|
|
173
|
+
}
|
|
174
|
+
.circular-list {
|
|
175
|
+
background: #ffebee;
|
|
176
|
+
border-left: 4px solid #d32f2f;
|
|
177
|
+
padding: 15px;
|
|
178
|
+
border-radius: 4px;
|
|
179
|
+
margin-top: 20px;
|
|
180
|
+
}
|
|
181
|
+
.circular-list h3 {
|
|
182
|
+
margin: 0 0 10px 0;
|
|
183
|
+
color: #d32f2f;
|
|
184
|
+
}
|
|
185
|
+
.cycle {
|
|
186
|
+
background: white;
|
|
187
|
+
padding: 8px 12px;
|
|
188
|
+
margin: 5px 0;
|
|
189
|
+
border-radius: 3px;
|
|
190
|
+
font-family: 'Courier New', monospace;
|
|
191
|
+
font-size: 12px;
|
|
192
|
+
}
|
|
193
|
+
</style>
|
|
194
|
+
</head>
|
|
195
|
+
<body>
|
|
196
|
+
<div class="container">
|
|
197
|
+
<h1>📊 Dependency Graph - ${projectName}</h1>
|
|
198
|
+
<p style="color: #666; margin-top: 5px;">Visual representation of file and component dependencies</p>
|
|
199
|
+
|
|
200
|
+
<div class="stats">
|
|
201
|
+
<div class="stat-card">
|
|
202
|
+
<div class="stat-label">Total Files</div>
|
|
203
|
+
<div class="stat-value">${graph.nodes.size}</div>
|
|
204
|
+
</div>
|
|
205
|
+
<div class="stat-card">
|
|
206
|
+
<div class="stat-label">Total Dependencies</div>
|
|
207
|
+
<div class="stat-value">${graph.edges.length}</div>
|
|
208
|
+
</div>
|
|
209
|
+
<div class="stat-card ${circularCount > 0 ? 'warning' : ''}">
|
|
210
|
+
<div class="stat-label">Circular Dependencies</div>
|
|
211
|
+
<div class="stat-value">${circularCount}</div>
|
|
212
|
+
</div>
|
|
213
|
+
</div>
|
|
214
|
+
|
|
215
|
+
<div class="graph-container">
|
|
216
|
+
${svgContent}
|
|
217
|
+
</div>
|
|
218
|
+
|
|
219
|
+
${circularCount > 0
|
|
220
|
+
? `
|
|
221
|
+
<div class="circular-list">
|
|
222
|
+
<h3>⚠️ Circular Dependencies Detected</h3>
|
|
223
|
+
${graph.circularDependencies.map(cycle => `<div class="cycle">${cycle.join(' → ')} → ${cycle[0]}</div>`).join('')}
|
|
224
|
+
</div>
|
|
225
|
+
`
|
|
226
|
+
: ''}
|
|
227
|
+
</div>
|
|
228
|
+
</body>
|
|
229
|
+
</html>`;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Calculate node positions using simple spring layout
|
|
233
|
+
*/
|
|
234
|
+
function calculateNodePositions(nodes, edges, width, height) {
|
|
235
|
+
const positions = new Map();
|
|
236
|
+
// Initial random positions
|
|
237
|
+
for (const node of nodes) {
|
|
238
|
+
positions.set(node.id, {
|
|
239
|
+
x: Math.random() * width * 0.8 + width * 0.1,
|
|
240
|
+
y: Math.random() * height * 0.8 + height * 0.1,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
// Simple spring simulation (few iterations)
|
|
244
|
+
const iterations = 50;
|
|
245
|
+
for (let iter = 0; iter < iterations; iter++) {
|
|
246
|
+
for (const node of nodes) {
|
|
247
|
+
let fx = 0;
|
|
248
|
+
let fy = 0;
|
|
249
|
+
const pos = positions.get(node.id);
|
|
250
|
+
// Repulsion from other nodes
|
|
251
|
+
for (const other of nodes) {
|
|
252
|
+
if (node.id === other.id)
|
|
253
|
+
continue;
|
|
254
|
+
const otherPos = positions.get(other.id);
|
|
255
|
+
const dx = pos.x - otherPos.x;
|
|
256
|
+
const dy = pos.y - otherPos.y;
|
|
257
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
258
|
+
const force = 50 / dist;
|
|
259
|
+
fx += (dx / dist) * force;
|
|
260
|
+
fy += (dy / dist) * force;
|
|
261
|
+
}
|
|
262
|
+
// Attraction to connected nodes
|
|
263
|
+
for (const edge of edges) {
|
|
264
|
+
if (edge.from === node.id) {
|
|
265
|
+
const targetPos = positions.get(edge.to);
|
|
266
|
+
if (targetPos) {
|
|
267
|
+
const dx = targetPos.x - pos.x;
|
|
268
|
+
const dy = targetPos.y - pos.y;
|
|
269
|
+
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
270
|
+
fx += (dx / dist) * 0.1;
|
|
271
|
+
fy += (dy / dist) * 0.1;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
// Update position
|
|
276
|
+
pos.x += fx * 0.1;
|
|
277
|
+
pos.y += fy * 0.1;
|
|
278
|
+
pos.x = Math.max(30, Math.min(width - 30, pos.x));
|
|
279
|
+
pos.y = Math.max(30, Math.min(height - 30, pos.y));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
return positions;
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* Detect circular dependencies using DFS
|
|
286
|
+
*/
|
|
287
|
+
function detectCircularDeps(nodes) {
|
|
288
|
+
const cycles = [];
|
|
289
|
+
const visited = new Set();
|
|
290
|
+
const recursionStack = new Set();
|
|
291
|
+
const path = [];
|
|
292
|
+
function dfs(nodeId) {
|
|
293
|
+
visited.add(nodeId);
|
|
294
|
+
recursionStack.add(nodeId);
|
|
295
|
+
path.push(nodeId);
|
|
296
|
+
const node = nodes.get(nodeId);
|
|
297
|
+
if (node) {
|
|
298
|
+
for (const impId of node.imports) {
|
|
299
|
+
if (!visited.has(impId)) {
|
|
300
|
+
dfs(impId);
|
|
301
|
+
}
|
|
302
|
+
else if (recursionStack.has(impId)) {
|
|
303
|
+
const cycleStart = path.indexOf(impId);
|
|
304
|
+
cycles.push(path.slice(cycleStart));
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
recursionStack.delete(nodeId);
|
|
309
|
+
path.pop();
|
|
310
|
+
}
|
|
311
|
+
for (const nodeId of nodes.keys()) {
|
|
312
|
+
if (!visited.has(nodeId)) {
|
|
313
|
+
dfs(nodeId);
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
return cycles;
|
|
317
|
+
}
|
|
318
|
+
function normalizeId(filePath, rootDir) {
|
|
319
|
+
return path.relative(rootDir, filePath);
|
|
320
|
+
}
|
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"}
|