react-code-smell-detector 1.4.2 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/README.md +207 -22
  2. package/dist/__tests__/parser.test.d.ts +2 -0
  3. package/dist/__tests__/parser.test.d.ts.map +1 -0
  4. package/dist/__tests__/parser.test.js +56 -0
  5. package/dist/__tests__/performanceBudget.test.d.ts +2 -0
  6. package/dist/__tests__/performanceBudget.test.d.ts.map +1 -0
  7. package/dist/__tests__/performanceBudget.test.js +91 -0
  8. package/dist/__tests__/prComments.test.d.ts +2 -0
  9. package/dist/__tests__/prComments.test.d.ts.map +1 -0
  10. package/dist/__tests__/prComments.test.js +118 -0
  11. package/dist/analyzer.d.ts.map +1 -1
  12. package/dist/analyzer.js +10 -1
  13. package/dist/cli.js +106 -1
  14. package/dist/detectors/index.d.ts +1 -0
  15. package/dist/detectors/index.d.ts.map +1 -1
  16. package/dist/detectors/index.js +2 -0
  17. package/dist/detectors/serverComponents.d.ts +11 -0
  18. package/dist/detectors/serverComponents.d.ts.map +1 -0
  19. package/dist/detectors/serverComponents.js +222 -0
  20. package/dist/docGenerator.d.ts +37 -0
  21. package/dist/docGenerator.d.ts.map +1 -0
  22. package/dist/docGenerator.js +306 -0
  23. package/dist/index.d.ts +4 -0
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +4 -0
  26. package/dist/interactiveFixer.d.ts +20 -0
  27. package/dist/interactiveFixer.d.ts.map +1 -0
  28. package/dist/interactiveFixer.js +178 -0
  29. package/dist/performanceBudget.d.ts +54 -0
  30. package/dist/performanceBudget.d.ts.map +1 -0
  31. package/dist/performanceBudget.js +218 -0
  32. package/dist/prComments.d.ts +47 -0
  33. package/dist/prComments.d.ts.map +1 -0
  34. package/dist/prComments.js +233 -0
  35. package/dist/types/index.d.ts +2 -1
  36. package/dist/types/index.d.ts.map +1 -1
  37. package/dist/types/index.js +2 -0
  38. package/package.json +10 -4
@@ -0,0 +1,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"}
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Generate a PR comment summary from analysis results
3
+ */
4
+ export function generatePRComment(result, rootDir) {
5
+ const { summary, debtScore, files } = result;
6
+ let comment = '## 🔍 Code Smell Analysis Report\n\n';
7
+ // Grade badge
8
+ const gradeEmoji = getGradeEmoji(debtScore.grade);
9
+ comment += `### ${gradeEmoji} Technical Debt Grade: **${debtScore.grade}** (Score: ${debtScore.score}/100)\n\n`;
10
+ // Summary table
11
+ comment += '| Metric | Count |\n';
12
+ comment += '|--------|-------|\n';
13
+ comment += `| Files Analyzed | ${summary.totalFiles} |\n`;
14
+ comment += `| Components | ${summary.totalComponents} |\n`;
15
+ comment += `| Total Issues | ${summary.totalSmells} |\n`;
16
+ comment += `| 🔴 Errors | ${summary.smellsBySeverity.error} |\n`;
17
+ comment += `| 🟡 Warnings | ${summary.smellsBySeverity.warning} |\n`;
18
+ comment += `| 🔵 Info | ${summary.smellsBySeverity.info} |\n\n`;
19
+ // Estimated refactor time
20
+ comment += `**Estimated Refactor Time:** ${debtScore.estimatedRefactorTime}\n\n`;
21
+ // Top issues by type
22
+ const topIssues = Object.entries(summary.smellsByType)
23
+ .filter(([_, count]) => count > 0)
24
+ .sort((a, b) => b[1] - a[1])
25
+ .slice(0, 5);
26
+ if (topIssues.length > 0) {
27
+ comment += '### Top Issues\n\n';
28
+ for (const [type, count] of topIssues) {
29
+ comment += `- **${type}**: ${count} occurrence(s)\n`;
30
+ }
31
+ comment += '\n';
32
+ }
33
+ // Breakdown by file (collapsible if many)
34
+ const filesWithIssues = files.filter(f => f.smells.length > 0);
35
+ if (filesWithIssues.length > 0) {
36
+ comment += '<details>\n<summary>📁 Issues by File</summary>\n\n';
37
+ for (const file of filesWithIssues.slice(0, 10)) {
38
+ const relativePath = file.file.replace(rootDir, '').replace(/^\//, '');
39
+ const errors = file.smells.filter(s => s.severity === 'error').length;
40
+ const warnings = file.smells.filter(s => s.severity === 'warning').length;
41
+ comment += `#### \`${relativePath}\`\n`;
42
+ comment += `🔴 ${errors} errors, 🟡 ${warnings} warnings\n\n`;
43
+ for (const smell of file.smells.slice(0, 5)) {
44
+ const emoji = smell.severity === 'error' ? '🔴' : smell.severity === 'warning' ? '🟡' : '🔵';
45
+ comment += `- ${emoji} Line ${smell.line}: ${smell.message}\n`;
46
+ }
47
+ if (file.smells.length > 5) {
48
+ comment += `- ... and ${file.smells.length - 5} more\n`;
49
+ }
50
+ comment += '\n';
51
+ }
52
+ if (filesWithIssues.length > 10) {
53
+ comment += `\n*... and ${filesWithIssues.length - 10} more files with issues*\n`;
54
+ }
55
+ comment += '</details>\n\n';
56
+ }
57
+ // Score breakdown
58
+ comment += '<details>\n<summary>📊 Score Breakdown</summary>\n\n';
59
+ comment += '| Category | Score |\n';
60
+ comment += '|----------|-------|\n';
61
+ comment += `| useEffect Usage | ${debtScore.breakdown.useEffectScore}/100 |\n`;
62
+ comment += `| Prop Drilling | ${debtScore.breakdown.propDrillingScore}/100 |\n`;
63
+ comment += `| Component Size | ${debtScore.breakdown.componentSizeScore}/100 |\n`;
64
+ comment += `| Memoization | ${debtScore.breakdown.memoizationScore}/100 |\n\n`;
65
+ comment += '</details>\n\n';
66
+ // Footer
67
+ comment += '---\n';
68
+ comment += '*Generated by [react-code-smell-detector](https://github.com/vsthakur101/react-code-smell-detector)*';
69
+ return comment;
70
+ }
71
+ /**
72
+ * Generate inline review comments for specific lines
73
+ */
74
+ export function generateInlineComments(smells, rootDir) {
75
+ const comments = [];
76
+ // Group by file and line to avoid duplicate comments
77
+ const grouped = new Map();
78
+ for (const smell of smells) {
79
+ const key = `${smell.file}:${smell.line}`;
80
+ const existing = grouped.get(key) || [];
81
+ existing.push(smell);
82
+ grouped.set(key, existing);
83
+ }
84
+ for (const [key, lineSmells] of grouped) {
85
+ const [file, lineStr] = key.split(':');
86
+ const line = parseInt(lineStr, 10);
87
+ const relativePath = file.replace(rootDir, '').replace(/^\//, '');
88
+ let body = '';
89
+ for (const smell of lineSmells) {
90
+ const emoji = smell.severity === 'error' ? '🔴' : smell.severity === 'warning' ? '🟡' : '🔵';
91
+ body += `${emoji} **${smell.type}**: ${smell.message}\n\n`;
92
+ body += `> 💡 ${smell.suggestion}\n\n`;
93
+ }
94
+ comments.push({
95
+ body: body.trim(),
96
+ path: relativePath,
97
+ line,
98
+ side: 'RIGHT',
99
+ });
100
+ }
101
+ return comments;
102
+ }
103
+ /**
104
+ * Post a comment to a GitHub PR using the GitHub API
105
+ */
106
+ export async function postPRComment(config, comment) {
107
+ const { token, owner, repo, prNumber } = config;
108
+ const url = `https://api.github.com/repos/${owner}/${repo}/issues/${prNumber}/comments`;
109
+ try {
110
+ const response = await fetch(url, {
111
+ method: 'POST',
112
+ headers: {
113
+ 'Authorization': `Bearer ${token}`,
114
+ 'Accept': 'application/vnd.github.v3+json',
115
+ 'Content-Type': 'application/json',
116
+ 'X-GitHub-Api-Version': '2022-11-28',
117
+ },
118
+ body: JSON.stringify({ body: comment }),
119
+ });
120
+ if (!response.ok) {
121
+ const error = await response.text();
122
+ console.error(`Failed to post PR comment: ${response.status} ${error}`);
123
+ return false;
124
+ }
125
+ return true;
126
+ }
127
+ catch (error) {
128
+ console.error(`Error posting PR comment: ${error.message}`);
129
+ return false;
130
+ }
131
+ }
132
+ /**
133
+ * Post inline review comments to a GitHub PR
134
+ */
135
+ export async function postInlineComments(config, comments) {
136
+ const { token, owner, repo, prNumber, commitSha } = config;
137
+ if (!commitSha) {
138
+ console.error('Commit SHA is required for inline comments');
139
+ return { success: 0, failed: comments.length };
140
+ }
141
+ let success = 0;
142
+ let failed = 0;
143
+ // Create a review with all comments
144
+ const url = `https://api.github.com/repos/${owner}/${repo}/pulls/${prNumber}/reviews`;
145
+ const reviewComments = comments.map(c => ({
146
+ path: c.path,
147
+ line: c.line,
148
+ side: c.side || 'RIGHT',
149
+ body: c.body,
150
+ }));
151
+ try {
152
+ const response = await fetch(url, {
153
+ method: 'POST',
154
+ headers: {
155
+ 'Authorization': `Bearer ${token}`,
156
+ 'Accept': 'application/vnd.github.v3+json',
157
+ 'Content-Type': 'application/json',
158
+ 'X-GitHub-Api-Version': '2022-11-28',
159
+ },
160
+ body: JSON.stringify({
161
+ commit_id: commitSha,
162
+ event: 'COMMENT',
163
+ comments: reviewComments,
164
+ }),
165
+ });
166
+ if (!response.ok) {
167
+ const error = await response.text();
168
+ console.error(`Failed to post inline comments: ${response.status} ${error}`);
169
+ failed = comments.length;
170
+ }
171
+ else {
172
+ success = comments.length;
173
+ }
174
+ }
175
+ catch (error) {
176
+ console.error(`Error posting inline comments: ${error.message}`);
177
+ failed = comments.length;
178
+ }
179
+ return { success, failed };
180
+ }
181
+ /**
182
+ * Get emoji for grade
183
+ */
184
+ function getGradeEmoji(grade) {
185
+ switch (grade) {
186
+ case 'A': return '🏆';
187
+ case 'B': return '✅';
188
+ case 'C': return '⚠️';
189
+ case 'D': return '🔶';
190
+ case 'F': return '🔴';
191
+ default: return '📊';
192
+ }
193
+ }
194
+ /**
195
+ * Parse GitHub repository info from environment or git remote
196
+ */
197
+ export function parseGitHubInfo() {
198
+ // Try GITHUB_REPOSITORY environment variable (set in GitHub Actions)
199
+ const ghRepo = process.env.GITHUB_REPOSITORY;
200
+ if (ghRepo) {
201
+ const [owner, repo] = ghRepo.split('/');
202
+ return { owner, repo };
203
+ }
204
+ return null;
205
+ }
206
+ /**
207
+ * Get PR number from environment (GitHub Actions)
208
+ */
209
+ export function getPRNumber() {
210
+ // GITHUB_REF format: refs/pull/{number}/merge
211
+ const ref = process.env.GITHUB_REF;
212
+ if (ref && ref.includes('/pull/')) {
213
+ const match = ref.match(/\/pull\/(\d+)\//);
214
+ if (match) {
215
+ return parseInt(match[1], 10);
216
+ }
217
+ }
218
+ // Try GITHUB_EVENT_PATH for pull_request events
219
+ const eventPath = process.env.GITHUB_EVENT_PATH;
220
+ if (eventPath) {
221
+ try {
222
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
223
+ const event = require(eventPath);
224
+ if (event.pull_request?.number) {
225
+ return event.pull_request.number;
226
+ }
227
+ }
228
+ catch {
229
+ // Ignore
230
+ }
231
+ }
232
+ return null;
233
+ }
@@ -1,5 +1,5 @@
1
1
  export type SmellSeverity = 'error' | 'warning' | 'info';
2
- export type SmellType = 'useEffect-overuse' | 'prop-drilling' | 'large-component' | 'unmemoized-calculation' | 'missing-dependency' | 'state-in-loop' | 'inline-function-prop' | 'deep-nesting' | 'missing-key' | 'hooks-rules-violation' | 'dependency-array-issue' | 'nested-ternary' | 'dead-code' | 'magic-value' | 'debug-statement' | 'todo-comment' | 'security-xss' | 'security-eval' | 'security-secrets' | 'a11y-missing-alt' | 'a11y-missing-label' | 'a11y-interactive-role' | 'a11y-keyboard' | 'a11y-semantic' | 'nextjs-client-server-boundary' | 'nextjs-missing-metadata' | 'nextjs-image-unoptimized' | 'nextjs-router-misuse' | 'rn-inline-style' | 'rn-missing-accessibility' | 'rn-performance-issue' | 'nodejs-callback-hell' | 'nodejs-unhandled-promise' | 'nodejs-sync-io' | 'nodejs-missing-error-handling' | 'js-var-usage' | 'js-loose-equality' | 'js-implicit-coercion' | 'js-global-pollution' | 'ts-any-usage' | 'ts-missing-return-type' | 'ts-non-null-assertion' | 'ts-type-assertion' | 'high-cyclomatic-complexity' | 'high-cognitive-complexity' | 'memory-leak-event-listener' | 'memory-leak-subscription' | 'memory-leak-timer' | 'memory-leak-async' | 'circular-dependency' | 'barrel-file-import' | 'namespace-import' | 'excessive-imports' | 'unused-export' | 'dead-import' | 'custom-rule';
2
+ export type SmellType = 'useEffect-overuse' | 'prop-drilling' | 'large-component' | 'unmemoized-calculation' | 'missing-dependency' | 'state-in-loop' | 'inline-function-prop' | 'deep-nesting' | 'missing-key' | 'hooks-rules-violation' | 'dependency-array-issue' | 'nested-ternary' | 'dead-code' | 'magic-value' | 'debug-statement' | 'todo-comment' | 'security-xss' | 'security-eval' | 'security-secrets' | 'a11y-missing-alt' | 'a11y-missing-label' | 'a11y-interactive-role' | 'a11y-keyboard' | 'a11y-semantic' | 'nextjs-client-server-boundary' | 'nextjs-missing-metadata' | 'nextjs-image-unoptimized' | 'nextjs-router-misuse' | 'rn-inline-style' | 'rn-missing-accessibility' | 'rn-performance-issue' | 'nodejs-callback-hell' | 'nodejs-unhandled-promise' | 'nodejs-sync-io' | 'nodejs-missing-error-handling' | 'js-var-usage' | 'js-loose-equality' | 'js-implicit-coercion' | 'js-global-pollution' | 'ts-any-usage' | 'ts-missing-return-type' | 'ts-non-null-assertion' | 'ts-type-assertion' | 'high-cyclomatic-complexity' | 'high-cognitive-complexity' | 'memory-leak-event-listener' | 'memory-leak-subscription' | 'memory-leak-timer' | 'memory-leak-async' | 'circular-dependency' | 'barrel-file-import' | 'namespace-import' | 'excessive-imports' | 'unused-export' | 'dead-import' | 'server-component-hooks' | 'server-component-events' | 'server-component-browser-api' | 'async-client-component' | 'mixed-directives' | 'custom-rule';
3
3
  export interface CodeSmell {
4
4
  type: SmellType;
5
5
  severity: SmellSeverity;
@@ -92,6 +92,7 @@ export interface DetectorConfig {
92
92
  analyzeBundleSize?: boolean;
93
93
  maxComponentSize?: number;
94
94
  customRules?: any[];
95
+ checkServerComponents: boolean;
95
96
  }
96
97
  export declare const DEFAULT_CONFIG: DetectorConfig;
97
98
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
@@ -48,4 +48,6 @@ export const DEFAULT_CONFIG = {
48
48
  maxComponentSize: 10000,
49
49
  // Custom rules
50
50
  customRules: undefined,
51
+ // Server Components (React 19)
52
+ checkServerComponents: true,
51
53
  };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "react-code-smell-detector",
3
- "version": "1.4.2",
4
- "description": "Detect code smells in React projects - useEffect overuse, prop drilling, large components, security issues, accessibility, memory leaks, and more",
3
+ "version": "1.5.0",
4
+ "description": "Detect code smells in React projects - useEffect overuse, prop drilling, large components, security issues, accessibility, memory leaks, React 19 Server Components, and more",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "react-smell": "dist/cli.js"
@@ -16,7 +16,11 @@
16
16
  "build": "tsc",
17
17
  "dev": "tsc --watch",
18
18
  "start": "node dist/cli.js",
19
- "test": "vitest"
19
+ "test": "vitest",
20
+ "test:ui": "vitest --ui",
21
+ "test:coverage": "vitest run --coverage",
22
+ "test:report": "vitest run --reporter=html --outputFile.html=./test-report/index.html && open ./test-report/index.html",
23
+ "test:all": "vitest run --coverage --reporter=html --outputFile.html=./test-report/index.html"
20
24
  },
21
25
  "keywords": [
22
26
  "react",
@@ -44,8 +48,10 @@
44
48
  "devDependencies": {
45
49
  "@types/babel__traverse": "^7.20.4",
46
50
  "@types/node": "^20.10.0",
51
+ "@vitest/coverage-v8": "^4.0.18",
52
+ "@vitest/ui": "^4.0.18",
47
53
  "typescript": "^5.3.0",
48
- "vitest": "^1.0.0"
54
+ "vitest": "^4.0.18"
49
55
  },
50
56
  "engines": {
51
57
  "node": ">=18.0.0"