react-code-smell-detector 1.4.2 → 1.5.1
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 +227 -22
- package/dist/__tests__/aiRefactoring.test.d.ts +2 -0
- package/dist/__tests__/aiRefactoring.test.d.ts.map +1 -0
- package/dist/__tests__/aiRefactoring.test.js +86 -0
- package/dist/__tests__/analyzer-real.test.d.ts +2 -0
- package/dist/__tests__/analyzer-real.test.d.ts.map +1 -0
- package/dist/__tests__/analyzer-real.test.js +149 -0
- package/dist/__tests__/analyzer.test.d.ts +2 -0
- package/dist/__tests__/analyzer.test.d.ts.map +1 -0
- package/dist/__tests__/analyzer.test.js +173 -0
- package/dist/__tests__/baseline.test.d.ts +2 -0
- package/dist/__tests__/baseline.test.d.ts.map +1 -0
- package/dist/__tests__/baseline.test.js +136 -0
- package/dist/__tests__/bundleAnalyzer.test.d.ts +2 -0
- package/dist/__tests__/bundleAnalyzer.test.d.ts.map +1 -0
- package/dist/__tests__/bundleAnalyzer.test.js +182 -0
- package/dist/__tests__/customRules.test.d.ts +2 -0
- package/dist/__tests__/customRules.test.d.ts.map +1 -0
- package/dist/__tests__/customRules.test.js +283 -0
- package/dist/__tests__/detectors/index.test.d.ts +2 -0
- package/dist/__tests__/detectors/index.test.d.ts.map +1 -0
- package/dist/__tests__/detectors/index.test.js +1012 -0
- package/dist/__tests__/detectors/newDetectors.test.d.ts +2 -0
- package/dist/__tests__/detectors/newDetectors.test.d.ts.map +1 -0
- package/dist/__tests__/detectors/newDetectors.test.js +333 -0
- package/dist/__tests__/docGenerator.test.d.ts +2 -0
- package/dist/__tests__/docGenerator.test.d.ts.map +1 -0
- package/dist/__tests__/docGenerator.test.js +157 -0
- package/dist/__tests__/fixer.test.d.ts +2 -0
- package/dist/__tests__/fixer.test.d.ts.map +1 -0
- package/dist/__tests__/fixer.test.js +193 -0
- package/dist/__tests__/git.test.d.ts +2 -0
- package/dist/__tests__/git.test.d.ts.map +1 -0
- package/dist/__tests__/git.test.js +38 -0
- package/dist/__tests__/graphGenerator.test.d.ts +2 -0
- package/dist/__tests__/graphGenerator.test.d.ts.map +1 -0
- package/dist/__tests__/graphGenerator.test.js +190 -0
- package/dist/__tests__/htmlReporter.test.d.ts +2 -0
- package/dist/__tests__/htmlReporter.test.d.ts.map +1 -0
- package/dist/__tests__/htmlReporter.test.js +258 -0
- package/dist/__tests__/interactiveFixer.test.d.ts +2 -0
- package/dist/__tests__/interactiveFixer.test.d.ts.map +1 -0
- package/dist/__tests__/interactiveFixer.test.js +231 -0
- 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 +242 -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/__tests__/reporter.test.d.ts +2 -0
- package/dist/__tests__/reporter.test.d.ts.map +1 -0
- package/dist/__tests__/reporter.test.js +136 -0
- package/dist/__tests__/watcher.test.d.ts +2 -0
- package/dist/__tests__/watcher.test.d.ts.map +1 -0
- package/dist/__tests__/watcher.test.js +161 -0
- package/dist/__tests__/webhooks.test.d.ts +2 -0
- package/dist/__tests__/webhooks.test.d.ts.map +1 -0
- package/dist/__tests__/webhooks.test.js +209 -0
- package/dist/aiRefactoring.d.ts +29 -0
- package/dist/aiRefactoring.d.ts.map +1 -0
- package/dist/aiRefactoring.js +290 -0
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +33 -1
- package/dist/cli.js +123 -1
- package/dist/detectors/contextApi.d.ts +11 -0
- package/dist/detectors/contextApi.d.ts.map +1 -0
- package/dist/detectors/contextApi.js +151 -0
- package/dist/detectors/errorBoundary.d.ts +11 -0
- package/dist/detectors/errorBoundary.d.ts.map +1 -0
- package/dist/detectors/errorBoundary.js +167 -0
- package/dist/detectors/formValidation.d.ts +11 -0
- package/dist/detectors/formValidation.d.ts.map +1 -0
- package/dist/detectors/formValidation.js +193 -0
- package/dist/detectors/index.d.ts +6 -0
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +12 -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/detectors/stateManagement.d.ts +11 -0
- package/dist/detectors/stateManagement.d.ts.map +1 -0
- package/dist/detectors/stateManagement.js +193 -0
- package/dist/detectors/testingGaps.d.ts +15 -0
- package/dist/detectors/testingGaps.d.ts.map +1 -0
- package/dist/detectors/testingGaps.js +182 -0
- package/dist/docGenerator.d.ts +37 -0
- package/dist/docGenerator.d.ts.map +1 -0
- package/dist/docGenerator.js +306 -0
- package/dist/guide.d.ts +9 -0
- package/dist/guide.d.ts.map +1 -0
- package/dist/guide.js +922 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -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 +12 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +18 -0
- package/package.json +10 -4
package/dist/index.d.ts
CHANGED
|
@@ -2,4 +2,9 @@ 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';
|
|
9
|
+
export { generateAIRefactoringSuggestions, generateAIAnalysisReport, getQuickRefactoringTemplates, type AIRefactoringSuggestion, type AIRefactoringConfig } from './aiRefactoring.js';
|
|
5
10
|
//# 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;AACxG,OAAO,EACL,gCAAgC,EAChC,wBAAwB,EACxB,4BAA4B,EAC5B,KAAK,uBAAuB,EAC5B,KAAK,mBAAmB,EACzB,MAAM,oBAAoB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -2,3 +2,8 @@ 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';
|
|
9
|
+
export { generateAIRefactoringSuggestions, generateAIAnalysisReport, getQuickRefactoringTemplates } from './aiRefactoring.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"}
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import fs from 'fs/promises';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
const DEFAULT_BUDGET = {
|
|
5
|
+
maxErrors: 0,
|
|
6
|
+
minGrade: 'C',
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Load performance budget from config file
|
|
10
|
+
*/
|
|
11
|
+
export async function loadBudget(configPath) {
|
|
12
|
+
const paths = configPath
|
|
13
|
+
? [configPath]
|
|
14
|
+
: [
|
|
15
|
+
'.smellbudget.json',
|
|
16
|
+
'.smellrc.json',
|
|
17
|
+
'package.json',
|
|
18
|
+
];
|
|
19
|
+
for (const p of paths) {
|
|
20
|
+
try {
|
|
21
|
+
const fullPath = path.resolve(process.cwd(), p);
|
|
22
|
+
const content = await fs.readFile(fullPath, 'utf-8');
|
|
23
|
+
const parsed = JSON.parse(content);
|
|
24
|
+
// In package.json, look for "smellBudget" key
|
|
25
|
+
if (p === 'package.json') {
|
|
26
|
+
if (parsed.smellBudget) {
|
|
27
|
+
return { ...DEFAULT_BUDGET, ...parsed.smellBudget };
|
|
28
|
+
}
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
// In .smellrc.json, look for "budget" key
|
|
32
|
+
if (p === '.smellrc.json') {
|
|
33
|
+
if (parsed.budget) {
|
|
34
|
+
return { ...DEFAULT_BUDGET, ...parsed.budget };
|
|
35
|
+
}
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
return { ...DEFAULT_BUDGET, ...parsed };
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return DEFAULT_BUDGET;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Check analysis results against performance budget
|
|
48
|
+
*/
|
|
49
|
+
export function checkBudget(result, budget) {
|
|
50
|
+
const violations = [];
|
|
51
|
+
const { summary, debtScore } = result;
|
|
52
|
+
// Check total smells
|
|
53
|
+
if (budget.maxTotalSmells !== undefined && summary.totalSmells > budget.maxTotalSmells) {
|
|
54
|
+
violations.push({
|
|
55
|
+
rule: 'maxTotalSmells',
|
|
56
|
+
actual: summary.totalSmells,
|
|
57
|
+
threshold: budget.maxTotalSmells,
|
|
58
|
+
message: `Total issues (${summary.totalSmells}) exceeds budget (${budget.maxTotalSmells})`,
|
|
59
|
+
severity: 'error',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
// Check errors
|
|
63
|
+
if (budget.maxErrors !== undefined && summary.smellsBySeverity.error > budget.maxErrors) {
|
|
64
|
+
violations.push({
|
|
65
|
+
rule: 'maxErrors',
|
|
66
|
+
actual: summary.smellsBySeverity.error,
|
|
67
|
+
threshold: budget.maxErrors,
|
|
68
|
+
message: `Errors (${summary.smellsBySeverity.error}) exceeds budget (${budget.maxErrors})`,
|
|
69
|
+
severity: 'error',
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
// Check warnings
|
|
73
|
+
if (budget.maxWarnings !== undefined && summary.smellsBySeverity.warning > budget.maxWarnings) {
|
|
74
|
+
violations.push({
|
|
75
|
+
rule: 'maxWarnings',
|
|
76
|
+
actual: summary.smellsBySeverity.warning,
|
|
77
|
+
threshold: budget.maxWarnings,
|
|
78
|
+
message: `Warnings (${summary.smellsBySeverity.warning}) exceeds budget (${budget.maxWarnings})`,
|
|
79
|
+
severity: 'warning',
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
// Check minimum score
|
|
83
|
+
if (budget.minScore !== undefined && debtScore.score < budget.minScore) {
|
|
84
|
+
violations.push({
|
|
85
|
+
rule: 'minScore',
|
|
86
|
+
actual: debtScore.score,
|
|
87
|
+
threshold: budget.minScore,
|
|
88
|
+
message: `Technical debt score (${debtScore.score}) is below minimum (${budget.minScore})`,
|
|
89
|
+
severity: 'error',
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
// Check minimum grade
|
|
93
|
+
if (budget.minGrade !== undefined) {
|
|
94
|
+
const gradeOrder = ['F', 'D', 'C', 'B', 'A'];
|
|
95
|
+
const actualIndex = gradeOrder.indexOf(debtScore.grade);
|
|
96
|
+
const minIndex = gradeOrder.indexOf(budget.minGrade);
|
|
97
|
+
if (actualIndex < minIndex) {
|
|
98
|
+
violations.push({
|
|
99
|
+
rule: 'minGrade',
|
|
100
|
+
actual: debtScore.grade,
|
|
101
|
+
threshold: budget.minGrade,
|
|
102
|
+
message: `Technical debt grade (${debtScore.grade}) is below minimum (${budget.minGrade})`,
|
|
103
|
+
severity: 'error',
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// Check per-type thresholds
|
|
108
|
+
if (budget.maxByType) {
|
|
109
|
+
for (const [type, maxCount] of Object.entries(budget.maxByType)) {
|
|
110
|
+
const actual = summary.smellsByType[type] || 0;
|
|
111
|
+
if (maxCount !== undefined && actual > maxCount) {
|
|
112
|
+
violations.push({
|
|
113
|
+
rule: `maxByType.${type}`,
|
|
114
|
+
actual,
|
|
115
|
+
threshold: maxCount,
|
|
116
|
+
message: `${type} issues (${actual}) exceeds budget (${maxCount})`,
|
|
117
|
+
severity: 'warning',
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
// Check max smells per file
|
|
123
|
+
if (budget.maxSmellsPerFile !== undefined) {
|
|
124
|
+
for (const file of result.files) {
|
|
125
|
+
if (file.smells.length > budget.maxSmellsPerFile) {
|
|
126
|
+
const relativePath = file.file.split('/').slice(-2).join('/');
|
|
127
|
+
violations.push({
|
|
128
|
+
rule: 'maxSmellsPerFile',
|
|
129
|
+
actual: file.smells.length,
|
|
130
|
+
threshold: budget.maxSmellsPerFile,
|
|
131
|
+
message: `${relativePath} has ${file.smells.length} issues (max: ${budget.maxSmellsPerFile})`,
|
|
132
|
+
severity: 'warning',
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
const errorViolations = violations.filter(v => v.severity === 'error');
|
|
138
|
+
return {
|
|
139
|
+
passed: errorViolations.length === 0,
|
|
140
|
+
violations,
|
|
141
|
+
summary: {
|
|
142
|
+
total: violations.length,
|
|
143
|
+
passed: Object.keys(budget).length - violations.length,
|
|
144
|
+
failed: violations.length,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Format budget check result for console output
|
|
150
|
+
*/
|
|
151
|
+
export function formatBudgetReport(result) {
|
|
152
|
+
let output = '';
|
|
153
|
+
if (result.passed) {
|
|
154
|
+
output += chalk.green('\n✓ Performance budget check passed\n');
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
output += chalk.red('\n✗ Performance budget check failed\n');
|
|
158
|
+
}
|
|
159
|
+
if (result.violations.length > 0) {
|
|
160
|
+
output += chalk.dim('\nViolations:\n');
|
|
161
|
+
for (const violation of result.violations) {
|
|
162
|
+
const icon = violation.severity === 'error' ? chalk.red('✗') : chalk.yellow('⚠');
|
|
163
|
+
output += ` ${icon} ${violation.message}\n`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
output += chalk.dim(`\nChecks: ${result.summary.passed} passed, ${result.summary.failed} failed\n`);
|
|
167
|
+
return output;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Create a default budget config file
|
|
171
|
+
*/
|
|
172
|
+
export async function createBudgetConfig(targetPath) {
|
|
173
|
+
const config = {
|
|
174
|
+
maxErrors: 0,
|
|
175
|
+
maxWarnings: 10,
|
|
176
|
+
minScore: 70,
|
|
177
|
+
minGrade: 'C',
|
|
178
|
+
maxSmellsPerFile: 5,
|
|
179
|
+
maxByType: {
|
|
180
|
+
'useEffect-overuse': 3,
|
|
181
|
+
'prop-drilling': 5,
|
|
182
|
+
'large-component': 2,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
const filePath = targetPath || '.smellbudget.json';
|
|
186
|
+
const fullPath = path.resolve(process.cwd(), filePath);
|
|
187
|
+
await fs.writeFile(fullPath, JSON.stringify(config, null, 2), 'utf-8');
|
|
188
|
+
return fullPath;
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Compare current results with baseline for growth limits
|
|
192
|
+
*/
|
|
193
|
+
export function checkGrowthLimits(current, baseline, budget) {
|
|
194
|
+
const violations = [];
|
|
195
|
+
const newSmells = current.totalSmells - baseline.totalSmells;
|
|
196
|
+
if (budget.maxNewSmells !== undefined && newSmells > budget.maxNewSmells) {
|
|
197
|
+
violations.push({
|
|
198
|
+
rule: 'maxNewSmells',
|
|
199
|
+
actual: newSmells,
|
|
200
|
+
threshold: budget.maxNewSmells,
|
|
201
|
+
message: `New issues (${newSmells}) exceeds allowed increase (${budget.maxNewSmells})`,
|
|
202
|
+
severity: 'error',
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
if (budget.allowedGrowthPercent !== undefined && baseline.totalSmells > 0) {
|
|
206
|
+
const growthPercent = ((newSmells / baseline.totalSmells) * 100);
|
|
207
|
+
if (growthPercent > budget.allowedGrowthPercent) {
|
|
208
|
+
violations.push({
|
|
209
|
+
rule: 'allowedGrowthPercent',
|
|
210
|
+
actual: `${growthPercent.toFixed(1)}%`,
|
|
211
|
+
threshold: `${budget.allowedGrowthPercent}%`,
|
|
212
|
+
message: `Issue growth (${growthPercent.toFixed(1)}%) exceeds allowed (${budget.allowedGrowthPercent}%)`,
|
|
213
|
+
severity: 'error',
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return violations;
|
|
218
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { CodeSmell, AnalysisResult } from './types/index.js';
|
|
2
|
+
export interface PRCommentConfig {
|
|
3
|
+
token: string;
|
|
4
|
+
repo: string;
|
|
5
|
+
owner: string;
|
|
6
|
+
prNumber: number;
|
|
7
|
+
commitSha?: string;
|
|
8
|
+
collapseThreshold?: number;
|
|
9
|
+
onlyNew?: boolean;
|
|
10
|
+
}
|
|
11
|
+
export interface PRComment {
|
|
12
|
+
body: string;
|
|
13
|
+
path?: string;
|
|
14
|
+
line?: number;
|
|
15
|
+
side?: 'LEFT' | 'RIGHT';
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Generate a PR comment summary from analysis results
|
|
19
|
+
*/
|
|
20
|
+
export declare function generatePRComment(result: AnalysisResult, rootDir: string): string;
|
|
21
|
+
/**
|
|
22
|
+
* Generate inline review comments for specific lines
|
|
23
|
+
*/
|
|
24
|
+
export declare function generateInlineComments(smells: CodeSmell[], rootDir: string): PRComment[];
|
|
25
|
+
/**
|
|
26
|
+
* Post a comment to a GitHub PR using the GitHub API
|
|
27
|
+
*/
|
|
28
|
+
export declare function postPRComment(config: PRCommentConfig, comment: string): Promise<boolean>;
|
|
29
|
+
/**
|
|
30
|
+
* Post inline review comments to a GitHub PR
|
|
31
|
+
*/
|
|
32
|
+
export declare function postInlineComments(config: PRCommentConfig, comments: PRComment[]): Promise<{
|
|
33
|
+
success: number;
|
|
34
|
+
failed: number;
|
|
35
|
+
}>;
|
|
36
|
+
/**
|
|
37
|
+
* Parse GitHub repository info from environment or git remote
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseGitHubInfo(): {
|
|
40
|
+
owner: string;
|
|
41
|
+
repo: string;
|
|
42
|
+
} | null;
|
|
43
|
+
/**
|
|
44
|
+
* Get PR number from environment (GitHub Actions)
|
|
45
|
+
*/
|
|
46
|
+
export declare function getPRNumber(): number | null;
|
|
47
|
+
//# sourceMappingURL=prComments.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prComments.d.ts","sourceRoot":"","sources":["../src/prComments.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC;AAE7D,MAAM,WAAW,eAAe;IAC9B,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC;CACzB;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,cAAc,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAkFjF;AAED;;GAEG;AACH,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,EAAE,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,CAiCxF;AAED;;GAEG;AACH,wBAAsB,aAAa,CAAC,MAAM,EAAE,eAAe,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CA4B9F;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CACtC,MAAM,EAAE,eAAe,EACvB,QAAQ,EAAE,SAAS,EAAE,GACpB,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAkD9C;AAgBD;;GAEG;AACH,wBAAgB,eAAe,IAAI;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CASxE;AAED;;GAEG;AACH,wBAAgB,WAAW,IAAI,MAAM,GAAG,IAAI,CAyB3C"}
|