react-code-smell-detector 1.2.0 ā 1.4.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/LICENSE +21 -0
- package/README.md +200 -4
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +22 -1
- package/dist/baseline.d.ts +37 -0
- package/dist/baseline.d.ts.map +1 -0
- package/dist/baseline.js +112 -0
- package/dist/cli.js +125 -26
- package/dist/detectors/complexity.d.ts +17 -0
- package/dist/detectors/complexity.d.ts.map +1 -0
- package/dist/detectors/complexity.js +69 -0
- package/dist/detectors/imports.d.ts +22 -0
- package/dist/detectors/imports.d.ts.map +1 -0
- package/dist/detectors/imports.js +210 -0
- package/dist/detectors/index.d.ts +4 -0
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +5 -0
- package/dist/detectors/memoryLeak.d.ts +7 -0
- package/dist/detectors/memoryLeak.d.ts.map +1 -0
- package/dist/detectors/memoryLeak.js +111 -0
- package/dist/detectors/unusedCode.d.ts +7 -0
- package/dist/detectors/unusedCode.d.ts.map +1 -0
- package/dist/detectors/unusedCode.js +78 -0
- package/dist/fixer.d.ts +23 -0
- package/dist/fixer.d.ts.map +1 -0
- package/dist/fixer.js +133 -0
- package/dist/git.d.ts +31 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +137 -0
- package/dist/reporter.js +16 -0
- package/dist/types/index.d.ts +13 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +18 -0
- package/dist/watcher.d.ts +16 -0
- package/dist/watcher.d.ts.map +1 -0
- package/dist/watcher.js +89 -0
- package/dist/webhooks.d.ts +20 -0
- package/dist/webhooks.d.ts.map +1 -0
- package/dist/webhooks.js +199 -0
- package/package.json +10 -2
- package/src/analyzer.ts +0 -324
- package/src/cli.ts +0 -159
- package/src/detectors/accessibility.ts +0 -212
- package/src/detectors/deadCode.ts +0 -163
- package/src/detectors/debug.ts +0 -103
- package/src/detectors/dependencyArray.ts +0 -176
- package/src/detectors/hooksRules.ts +0 -101
- package/src/detectors/index.ts +0 -20
- package/src/detectors/javascript.ts +0 -169
- package/src/detectors/largeComponent.ts +0 -63
- package/src/detectors/magicValues.ts +0 -114
- package/src/detectors/memoization.ts +0 -177
- package/src/detectors/missingKey.ts +0 -105
- package/src/detectors/nestedTernary.ts +0 -75
- package/src/detectors/nextjs.ts +0 -124
- package/src/detectors/nodejs.ts +0 -199
- package/src/detectors/propDrilling.ts +0 -103
- package/src/detectors/reactNative.ts +0 -154
- package/src/detectors/security.ts +0 -179
- package/src/detectors/typescript.ts +0 -151
- package/src/detectors/useEffect.ts +0 -117
- package/src/htmlReporter.ts +0 -464
- package/src/index.ts +0 -4
- package/src/parser/index.ts +0 -195
- package/src/reporter.ts +0 -291
- package/src/types/index.ts +0 -165
- package/tsconfig.json +0 -19
package/dist/watcher.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import chokidar from 'chokidar';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { analyzeProject } from './analyzer.js';
|
|
5
|
+
import { reportResults } from './reporter.js';
|
|
6
|
+
/**
|
|
7
|
+
* Start watching a project for changes and re-analyze on each change
|
|
8
|
+
*/
|
|
9
|
+
export function startWatch(options) {
|
|
10
|
+
const { rootDir, include = ['**/*.tsx', '**/*.jsx'], exclude = ['**/node_modules/**', '**/dist/**', '**/build/**'], config, showSnippets, onAnalysis, } = options;
|
|
11
|
+
console.log(chalk.cyan('\nš Watch mode started'));
|
|
12
|
+
console.log(chalk.dim(` Watching: ${rootDir}`));
|
|
13
|
+
console.log(chalk.dim(` Patterns: ${include.join(', ')}`));
|
|
14
|
+
console.log(chalk.dim(' Press Ctrl+C to stop\n'));
|
|
15
|
+
// Debounce timer
|
|
16
|
+
let debounceTimer = null;
|
|
17
|
+
let isAnalyzing = false;
|
|
18
|
+
const runAnalysis = async () => {
|
|
19
|
+
if (isAnalyzing)
|
|
20
|
+
return;
|
|
21
|
+
isAnalyzing = true;
|
|
22
|
+
console.log(chalk.dim('\nāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāāā'));
|
|
23
|
+
console.log(chalk.cyan('š Re-analyzing...'));
|
|
24
|
+
try {
|
|
25
|
+
const result = await analyzeProject({
|
|
26
|
+
rootDir,
|
|
27
|
+
include,
|
|
28
|
+
exclude,
|
|
29
|
+
config,
|
|
30
|
+
});
|
|
31
|
+
// Clear console and show results
|
|
32
|
+
console.clear();
|
|
33
|
+
console.log(chalk.cyan('\nš Watch mode - Last update: ' + new Date().toLocaleTimeString()));
|
|
34
|
+
console.log(chalk.dim(' Press Ctrl+C to stop\n'));
|
|
35
|
+
const output = reportResults(result, {
|
|
36
|
+
format: 'console',
|
|
37
|
+
showCodeSnippets: showSnippets,
|
|
38
|
+
rootDir,
|
|
39
|
+
});
|
|
40
|
+
console.log(output);
|
|
41
|
+
if (onAnalysis) {
|
|
42
|
+
onAnalysis(result.summary.totalFiles, result.summary.totalSmells);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
console.error(chalk.red('Analysis error:'), error.message);
|
|
47
|
+
}
|
|
48
|
+
finally {
|
|
49
|
+
isAnalyzing = false;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
// Debounced analysis trigger
|
|
53
|
+
const triggerAnalysis = () => {
|
|
54
|
+
if (debounceTimer) {
|
|
55
|
+
clearTimeout(debounceTimer);
|
|
56
|
+
}
|
|
57
|
+
debounceTimer = setTimeout(runAnalysis, 300);
|
|
58
|
+
};
|
|
59
|
+
// Set up file watcher
|
|
60
|
+
const watchPatterns = include.map(p => path.join(rootDir, p));
|
|
61
|
+
const watcher = chokidar.watch(watchPatterns, {
|
|
62
|
+
ignored: exclude.map(p => {
|
|
63
|
+
if (p.startsWith('**/'))
|
|
64
|
+
return p;
|
|
65
|
+
return path.join(rootDir, p);
|
|
66
|
+
}),
|
|
67
|
+
persistent: true,
|
|
68
|
+
ignoreInitial: false,
|
|
69
|
+
awaitWriteFinish: {
|
|
70
|
+
stabilityThreshold: 100,
|
|
71
|
+
pollInterval: 50,
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
watcher
|
|
75
|
+
.on('add', () => triggerAnalysis())
|
|
76
|
+
.on('change', () => triggerAnalysis())
|
|
77
|
+
.on('unlink', () => triggerAnalysis())
|
|
78
|
+
.on('error', error => console.error(chalk.red('Watcher error:'), error));
|
|
79
|
+
// Initial analysis
|
|
80
|
+
triggerAnalysis();
|
|
81
|
+
return {
|
|
82
|
+
close: () => {
|
|
83
|
+
if (debounceTimer)
|
|
84
|
+
clearTimeout(debounceTimer);
|
|
85
|
+
watcher.close();
|
|
86
|
+
console.log(chalk.yellow('\nš Watch mode stopped'));
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { CodeSmell } from './types/index.js';
|
|
2
|
+
export interface WebhookConfig {
|
|
3
|
+
url: string;
|
|
4
|
+
type: 'slack' | 'discord' | 'generic';
|
|
5
|
+
threshold?: number;
|
|
6
|
+
includeDetails?: boolean;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Send analysis results to chat platform via webhook
|
|
10
|
+
*/
|
|
11
|
+
export declare function sendWebhookNotification(whConfig: WebhookConfig, smells: CodeSmell[], projectName: string, metadata?: {
|
|
12
|
+
branch?: string;
|
|
13
|
+
commit?: string;
|
|
14
|
+
author?: string;
|
|
15
|
+
}): Promise<boolean>;
|
|
16
|
+
/**
|
|
17
|
+
* Parse webhook URL from environment or config
|
|
18
|
+
*/
|
|
19
|
+
export declare function getWebhookConfig(slackUrl?: string, discordUrl?: string, genericUrl?: string): WebhookConfig | null;
|
|
20
|
+
//# sourceMappingURL=webhooks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"webhooks.d.ts","sourceRoot":"","sources":["../src/webhooks.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C,MAAM,WAAW,aAAa;IAC5B,GAAG,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;IACtC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;GAEG;AACH,wBAAsB,uBAAuB,CAC3C,QAAQ,EAAE,aAAa,EACvB,MAAM,EAAE,SAAS,EAAE,EACnB,WAAW,EAAE,MAAM,EACnB,QAAQ,CAAC,EAAE;IACT,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,GACA,OAAO,CAAC,OAAO,CAAC,CAqBlB;AAmLD;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,QAAQ,CAAC,EAAE,MAAM,EACjB,UAAU,CAAC,EAAE,MAAM,EACnB,UAAU,CAAC,EAAE,MAAM,GAClB,aAAa,GAAG,IAAI,CAoBtB"}
|
package/dist/webhooks.js
ADDED
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
import https from 'https';
|
|
2
|
+
/**
|
|
3
|
+
* Send analysis results to chat platform via webhook
|
|
4
|
+
*/
|
|
5
|
+
export async function sendWebhookNotification(whConfig, smells, projectName, metadata) {
|
|
6
|
+
if (!whConfig.url)
|
|
7
|
+
return false;
|
|
8
|
+
// Check threshold
|
|
9
|
+
if (whConfig.threshold && smells.length < whConfig.threshold) {
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
try {
|
|
13
|
+
const payload = whConfig.type === 'slack'
|
|
14
|
+
? formatSlackMessage(smells, projectName, metadata, whConfig.includeDetails)
|
|
15
|
+
: whConfig.type === 'discord'
|
|
16
|
+
? formatDiscordMessage(smells, projectName, metadata, whConfig.includeDetails)
|
|
17
|
+
: formatGenericMessage(smells, projectName, metadata);
|
|
18
|
+
return await postToWebhook(whConfig.url, payload);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
console.error('Webhook notification failed:', error);
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Post JSON to a webhook URL
|
|
27
|
+
*/
|
|
28
|
+
function postToWebhook(url, payload) {
|
|
29
|
+
return new Promise((resolve) => {
|
|
30
|
+
try {
|
|
31
|
+
const data = JSON.stringify(payload);
|
|
32
|
+
const urlObj = new URL(url);
|
|
33
|
+
const options = {
|
|
34
|
+
hostname: urlObj.hostname,
|
|
35
|
+
path: urlObj.pathname + urlObj.search,
|
|
36
|
+
method: 'POST',
|
|
37
|
+
headers: {
|
|
38
|
+
'Content-Type': 'application/json',
|
|
39
|
+
'Content-Length': Buffer.byteLength(data),
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
const req = https.request(options, (res) => {
|
|
43
|
+
resolve(res.statusCode ? res.statusCode >= 200 && res.statusCode < 300 : false);
|
|
44
|
+
});
|
|
45
|
+
req.on('error', () => resolve(false));
|
|
46
|
+
req.write(data);
|
|
47
|
+
req.end();
|
|
48
|
+
}
|
|
49
|
+
catch (error) {
|
|
50
|
+
resolve(false);
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function formatSlackMessage(smells, projectName, metadata, includeDetails = false) {
|
|
55
|
+
const smellSummary = summarizeSmells(smells);
|
|
56
|
+
const color = smells.length > 20 ? 'danger' : smells.length > 10 ? 'warning' : 'good';
|
|
57
|
+
const fields = [
|
|
58
|
+
{
|
|
59
|
+
title: 'Total Smells',
|
|
60
|
+
value: smells.length.toString(),
|
|
61
|
+
short: true,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
title: 'Severity',
|
|
65
|
+
value: getSeverityEmoji(smells.length),
|
|
66
|
+
short: true,
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
if (metadata?.branch) {
|
|
70
|
+
fields.push({
|
|
71
|
+
title: 'Branch',
|
|
72
|
+
value: metadata.branch,
|
|
73
|
+
short: true,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
if (metadata?.commit) {
|
|
77
|
+
fields.push({
|
|
78
|
+
title: 'Commit',
|
|
79
|
+
value: metadata.commit.slice(0, 7),
|
|
80
|
+
short: true,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
// Add top issues
|
|
84
|
+
const topTypes = Object.entries(smellSummary)
|
|
85
|
+
.sort(([, a], [, b]) => b - a)
|
|
86
|
+
.slice(0, 5);
|
|
87
|
+
if (topTypes.length > 0) {
|
|
88
|
+
fields.push({
|
|
89
|
+
title: 'Top Issues',
|
|
90
|
+
value: topTypes.map(([type, count]) => `⢠${type}: ${count}`).join('\n'),
|
|
91
|
+
short: false,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
text: `Code Smell Analysis: ${projectName}`,
|
|
96
|
+
attachments: [
|
|
97
|
+
{
|
|
98
|
+
fallback: `${smells.length} code smells detected`,
|
|
99
|
+
color,
|
|
100
|
+
title: `${smells.length} Code Smells Detected`,
|
|
101
|
+
fields,
|
|
102
|
+
footer: 'React Code Smell Detector',
|
|
103
|
+
ts: Math.floor(Date.now() / 1000),
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
function formatDiscordMessage(smells, projectName, metadata, includeDetails = false) {
|
|
109
|
+
const smellSummary = summarizeSmells(smells);
|
|
110
|
+
const color = smells.length > 20 ? 15671935 : smells.length > 10 ? 16776960 : 65280; // Red, Yellow, Green
|
|
111
|
+
const topTypes = Object.entries(smellSummary)
|
|
112
|
+
.sort(([, a], [, b]) => b - a)
|
|
113
|
+
.slice(0, 5);
|
|
114
|
+
let description = `**Total Smells:** ${smells.length}\n`;
|
|
115
|
+
if (metadata?.branch) {
|
|
116
|
+
description += `**Branch:** ${metadata.branch}\n`;
|
|
117
|
+
}
|
|
118
|
+
if (metadata?.author) {
|
|
119
|
+
description += `**Author:** ${metadata.author}\n`;
|
|
120
|
+
}
|
|
121
|
+
if (topTypes.length > 0) {
|
|
122
|
+
description += '\n**Top Issues:**\n';
|
|
123
|
+
description += topTypes.map(([type, count]) => `⢠${type}: ${count}`).join('\n');
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
content: `Code Smell Analysis for ${projectName}`,
|
|
127
|
+
embeds: [
|
|
128
|
+
{
|
|
129
|
+
title: `${smells.length} Code Smells Detected`,
|
|
130
|
+
description,
|
|
131
|
+
color,
|
|
132
|
+
footer: {
|
|
133
|
+
text: 'React Code Smell Detector',
|
|
134
|
+
},
|
|
135
|
+
timestamp: new Date().toISOString(),
|
|
136
|
+
},
|
|
137
|
+
],
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
function formatGenericMessage(smells, projectName, metadata) {
|
|
141
|
+
const smellSummary = summarizeSmells(smells);
|
|
142
|
+
return {
|
|
143
|
+
project: projectName,
|
|
144
|
+
totalSmells: smells.length,
|
|
145
|
+
timestamp: new Date().toISOString(),
|
|
146
|
+
metadata,
|
|
147
|
+
summary: smellSummary,
|
|
148
|
+
severity: getSeverityLevel(smells.length),
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
function summarizeSmells(smells) {
|
|
152
|
+
const summary = {};
|
|
153
|
+
for (const smell of smells) {
|
|
154
|
+
summary[smell.type] = (summary[smell.type] || 0) + 1;
|
|
155
|
+
}
|
|
156
|
+
return summary;
|
|
157
|
+
}
|
|
158
|
+
function getSeverityEmoji(count) {
|
|
159
|
+
if (count > 50)
|
|
160
|
+
return 'š“ Critical';
|
|
161
|
+
if (count > 20)
|
|
162
|
+
return 'š High';
|
|
163
|
+
if (count > 10)
|
|
164
|
+
return 'š” Medium';
|
|
165
|
+
if (count > 0)
|
|
166
|
+
return 'š¢ Low';
|
|
167
|
+
return 'ā
Excellent';
|
|
168
|
+
}
|
|
169
|
+
function getSeverityLevel(count) {
|
|
170
|
+
if (count > 50)
|
|
171
|
+
return 'critical';
|
|
172
|
+
if (count > 20)
|
|
173
|
+
return 'high';
|
|
174
|
+
if (count > 10)
|
|
175
|
+
return 'medium';
|
|
176
|
+
if (count > 0)
|
|
177
|
+
return 'low';
|
|
178
|
+
return 'excellent';
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* Parse webhook URL from environment or config
|
|
182
|
+
*/
|
|
183
|
+
export function getWebhookConfig(slackUrl, discordUrl, genericUrl) {
|
|
184
|
+
const slack = slackUrl || process.env.REACT_SMELL_SLACK_WEBHOOK || process.env.SLACK_WEBHOOK_URL;
|
|
185
|
+
const discord = discordUrl ||
|
|
186
|
+
process.env.REACT_SMELL_DISCORD_WEBHOOK ||
|
|
187
|
+
process.env.DISCORD_WEBHOOK_URL;
|
|
188
|
+
const generic = genericUrl || process.env.REACT_SMELL_WEBHOOK;
|
|
189
|
+
if (slack) {
|
|
190
|
+
return { url: slack, type: 'slack' };
|
|
191
|
+
}
|
|
192
|
+
if (discord) {
|
|
193
|
+
return { url: discord, type: 'discord' };
|
|
194
|
+
}
|
|
195
|
+
if (generic) {
|
|
196
|
+
return { url: generic, type: 'generic' };
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-code-smell-detector",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "Detect code smells in React projects - useEffect overuse, prop drilling, large components, security issues, accessibility, and more",
|
|
3
|
+
"version": "1.4.1",
|
|
4
|
+
"description": "Detect code smells in React projects - useEffect overuse, prop drilling, large components, security issues, accessibility, memory leaks, and more",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
7
7
|
"react-smell": "dist/cli.js"
|
|
8
8
|
},
|
|
9
9
|
"type": "module",
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
10
15
|
"scripts": {
|
|
11
16
|
"build": "tsc",
|
|
12
17
|
"dev": "tsc --watch",
|
|
@@ -29,8 +34,11 @@
|
|
|
29
34
|
"@babel/traverse": "^7.23.0",
|
|
30
35
|
"@babel/types": "^7.23.0",
|
|
31
36
|
"chalk": "^5.3.0",
|
|
37
|
+
"chokidar": "^5.0.0",
|
|
32
38
|
"commander": "^11.1.0",
|
|
33
39
|
"fast-glob": "^3.3.2",
|
|
40
|
+
"fs-extra": "^11.3.3",
|
|
41
|
+
"node-fetch": "^3.3.2",
|
|
34
42
|
"ora": "^8.0.1"
|
|
35
43
|
},
|
|
36
44
|
"devDependencies": {
|
package/src/analyzer.ts
DELETED
|
@@ -1,324 +0,0 @@
|
|
|
1
|
-
import fg from 'fast-glob';
|
|
2
|
-
import path from 'path';
|
|
3
|
-
import { parseFile, ParseResult } from './parser/index.js';
|
|
4
|
-
import {
|
|
5
|
-
detectUseEffectOveruse,
|
|
6
|
-
detectPropDrilling,
|
|
7
|
-
analyzePropDrillingDepth,
|
|
8
|
-
detectLargeComponent,
|
|
9
|
-
detectUnmemoizedCalculations,
|
|
10
|
-
detectMissingKeys,
|
|
11
|
-
detectHooksRulesViolations,
|
|
12
|
-
detectDependencyArrayIssues,
|
|
13
|
-
detectNestedTernaries,
|
|
14
|
-
detectDeadCode,
|
|
15
|
-
detectMagicValues,
|
|
16
|
-
detectNextjsIssues,
|
|
17
|
-
detectReactNativeIssues,
|
|
18
|
-
detectNodejsIssues,
|
|
19
|
-
detectJavascriptIssues,
|
|
20
|
-
detectTypescriptIssues,
|
|
21
|
-
detectDebugStatements,
|
|
22
|
-
detectSecurityIssues,
|
|
23
|
-
detectAccessibilityIssues,
|
|
24
|
-
} from './detectors/index.js';
|
|
25
|
-
import {
|
|
26
|
-
AnalysisResult,
|
|
27
|
-
FileAnalysis,
|
|
28
|
-
ComponentInfo,
|
|
29
|
-
CodeSmell,
|
|
30
|
-
AnalysisSummary,
|
|
31
|
-
TechnicalDebtScore,
|
|
32
|
-
DetectorConfig,
|
|
33
|
-
DEFAULT_CONFIG,
|
|
34
|
-
SmellType,
|
|
35
|
-
SmellSeverity,
|
|
36
|
-
} from './types/index.js';
|
|
37
|
-
|
|
38
|
-
export interface AnalyzerOptions {
|
|
39
|
-
rootDir: string;
|
|
40
|
-
include?: string[];
|
|
41
|
-
exclude?: string[];
|
|
42
|
-
config?: Partial<DetectorConfig>;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
export async function analyzeProject(options: AnalyzerOptions): Promise<AnalysisResult> {
|
|
46
|
-
const {
|
|
47
|
-
rootDir,
|
|
48
|
-
include = ['**/*.tsx', '**/*.jsx'],
|
|
49
|
-
exclude = ['**/node_modules/**', '**/dist/**', '**/build/**', '**/*.test.*', '**/*.spec.*'],
|
|
50
|
-
config: userConfig = {},
|
|
51
|
-
} = options;
|
|
52
|
-
|
|
53
|
-
const config: DetectorConfig = { ...DEFAULT_CONFIG, ...userConfig };
|
|
54
|
-
|
|
55
|
-
// Find all React files
|
|
56
|
-
const patterns = include.map(p => path.join(rootDir, p));
|
|
57
|
-
const files = await fg(patterns, {
|
|
58
|
-
ignore: exclude,
|
|
59
|
-
absolute: true,
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
const fileAnalyses: FileAnalysis[] = [];
|
|
63
|
-
|
|
64
|
-
// Analyze each file
|
|
65
|
-
for (const file of files) {
|
|
66
|
-
try {
|
|
67
|
-
const parseResult = await parseFile(file);
|
|
68
|
-
const analysis = analyzeFile(parseResult, file, config);
|
|
69
|
-
if (analysis.components.length > 0 || analysis.smells.length > 0) {
|
|
70
|
-
fileAnalyses.push(analysis);
|
|
71
|
-
}
|
|
72
|
-
} catch (error) {
|
|
73
|
-
// Skip files that can't be parsed
|
|
74
|
-
console.warn(`Warning: Could not parse ${file}: ${(error as Error).message}`);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
// Calculate summary and score
|
|
79
|
-
const summary = calculateSummary(fileAnalyses);
|
|
80
|
-
const debtScore = calculateTechnicalDebtScore(fileAnalyses, summary);
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
files: fileAnalyses,
|
|
84
|
-
summary,
|
|
85
|
-
debtScore,
|
|
86
|
-
};
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function analyzeFile(parseResult: ParseResult, filePath: string, config: DetectorConfig): FileAnalysis {
|
|
90
|
-
const { components, imports, sourceCode } = parseResult;
|
|
91
|
-
const smells: CodeSmell[] = [];
|
|
92
|
-
const componentInfos: ComponentInfo[] = [];
|
|
93
|
-
|
|
94
|
-
// Run all detectors on each component
|
|
95
|
-
components.forEach(component => {
|
|
96
|
-
// Collect component info
|
|
97
|
-
componentInfos.push({
|
|
98
|
-
name: component.name,
|
|
99
|
-
file: filePath,
|
|
100
|
-
startLine: component.startLine,
|
|
101
|
-
endLine: component.endLine,
|
|
102
|
-
lineCount: component.endLine - component.startLine + 1,
|
|
103
|
-
useEffectCount: component.hooks.useEffect.length,
|
|
104
|
-
useStateCount: component.hooks.useState.length,
|
|
105
|
-
useMemoCount: component.hooks.useMemo.length,
|
|
106
|
-
useCallbackCount: component.hooks.useCallback.length,
|
|
107
|
-
propsCount: component.props.length,
|
|
108
|
-
propsDrillingDepth: 0, // Calculated separately
|
|
109
|
-
hasExpensiveCalculation: false, // Will be set by memoization detector
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
// Run detectors
|
|
113
|
-
smells.push(...detectUseEffectOveruse(component, filePath, sourceCode, config));
|
|
114
|
-
smells.push(...detectPropDrilling(component, filePath, sourceCode, config));
|
|
115
|
-
smells.push(...detectLargeComponent(component, filePath, sourceCode, config));
|
|
116
|
-
smells.push(...detectUnmemoizedCalculations(component, filePath, sourceCode, config));
|
|
117
|
-
smells.push(...detectMissingKeys(component, filePath, sourceCode, config));
|
|
118
|
-
smells.push(...detectHooksRulesViolations(component, filePath, sourceCode, config));
|
|
119
|
-
smells.push(...detectDependencyArrayIssues(component, filePath, sourceCode, config));
|
|
120
|
-
smells.push(...detectNestedTernaries(component, filePath, sourceCode, config));
|
|
121
|
-
smells.push(...detectDeadCode(component, filePath, sourceCode, config));
|
|
122
|
-
smells.push(...detectMagicValues(component, filePath, sourceCode, config));
|
|
123
|
-
// Framework-specific detectors
|
|
124
|
-
smells.push(...detectNextjsIssues(component, filePath, sourceCode, config, imports));
|
|
125
|
-
smells.push(...detectReactNativeIssues(component, filePath, sourceCode, config, imports));
|
|
126
|
-
smells.push(...detectNodejsIssues(component, filePath, sourceCode, config, imports));
|
|
127
|
-
smells.push(...detectJavascriptIssues(component, filePath, sourceCode, config));
|
|
128
|
-
smells.push(...detectTypescriptIssues(component, filePath, sourceCode, config));
|
|
129
|
-
// Debug, Security, Accessibility
|
|
130
|
-
smells.push(...detectDebugStatements(component, filePath, sourceCode, config));
|
|
131
|
-
smells.push(...detectSecurityIssues(component, filePath, sourceCode, config));
|
|
132
|
-
smells.push(...detectAccessibilityIssues(component, filePath, sourceCode, config));
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
// Run cross-component analysis
|
|
136
|
-
smells.push(...analyzePropDrillingDepth(components, filePath, sourceCode, config));
|
|
137
|
-
|
|
138
|
-
// Filter out smells with @smell-ignore comments
|
|
139
|
-
const filteredSmells = filterIgnoredSmells(smells, sourceCode);
|
|
140
|
-
|
|
141
|
-
return {
|
|
142
|
-
file: filePath,
|
|
143
|
-
components: componentInfos,
|
|
144
|
-
smells: filteredSmells,
|
|
145
|
-
imports,
|
|
146
|
-
};
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/**
|
|
150
|
-
* Filter out smells that have @smell-ignore comment on the preceding line
|
|
151
|
-
* Supports: // @smell-ignore, block comments with @smell-ignore, @smell-ignore [type]
|
|
152
|
-
*/
|
|
153
|
-
function filterIgnoredSmells(smells: CodeSmell[], sourceCode: string): CodeSmell[] {
|
|
154
|
-
const lines = sourceCode.split('\n');
|
|
155
|
-
|
|
156
|
-
return smells.filter(smell => {
|
|
157
|
-
if (smell.line <= 1) return true;
|
|
158
|
-
|
|
159
|
-
// Check the line before and the same line for ignore comments
|
|
160
|
-
const lineIndex = smell.line - 1; // Convert to 0-indexed
|
|
161
|
-
const prevLine = lines[lineIndex - 1]?.trim() || '';
|
|
162
|
-
const currentLine = lines[lineIndex]?.trim() || '';
|
|
163
|
-
|
|
164
|
-
// Check for @smell-ignore patterns
|
|
165
|
-
const ignorePatterns = [
|
|
166
|
-
/@smell-ignore\s*$/, // @smell-ignore (ignore all)
|
|
167
|
-
/@smell-ignore\s+\*/, // @smell-ignore * (ignore all)
|
|
168
|
-
new RegExp(`@smell-ignore\\s+${smell.type}`), // @smell-ignore [specific-type]
|
|
169
|
-
];
|
|
170
|
-
|
|
171
|
-
for (const pattern of ignorePatterns) {
|
|
172
|
-
if (pattern.test(prevLine) || pattern.test(currentLine)) {
|
|
173
|
-
return false; // Filter out this smell
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
return true; // Keep this smell
|
|
178
|
-
});
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
function calculateSummary(files: FileAnalysis[]): AnalysisSummary {
|
|
182
|
-
const smellsByType: Record<SmellType, number> = {
|
|
183
|
-
'useEffect-overuse': 0,
|
|
184
|
-
'prop-drilling': 0,
|
|
185
|
-
'large-component': 0,
|
|
186
|
-
'unmemoized-calculation': 0,
|
|
187
|
-
'missing-dependency': 0,
|
|
188
|
-
'state-in-loop': 0,
|
|
189
|
-
'inline-function-prop': 0,
|
|
190
|
-
'deep-nesting': 0,
|
|
191
|
-
'missing-key': 0,
|
|
192
|
-
'hooks-rules-violation': 0,
|
|
193
|
-
'dependency-array-issue': 0,
|
|
194
|
-
'nested-ternary': 0,
|
|
195
|
-
'dead-code': 0,
|
|
196
|
-
'magic-value': 0,
|
|
197
|
-
// Next.js
|
|
198
|
-
'nextjs-client-server-boundary': 0,
|
|
199
|
-
'nextjs-missing-metadata': 0,
|
|
200
|
-
'nextjs-image-unoptimized': 0,
|
|
201
|
-
'nextjs-router-misuse': 0,
|
|
202
|
-
// React Native
|
|
203
|
-
'rn-inline-style': 0,
|
|
204
|
-
'rn-missing-accessibility': 0,
|
|
205
|
-
'rn-performance-issue': 0,
|
|
206
|
-
// Node.js
|
|
207
|
-
'nodejs-callback-hell': 0,
|
|
208
|
-
'nodejs-unhandled-promise': 0,
|
|
209
|
-
'nodejs-sync-io': 0,
|
|
210
|
-
'nodejs-missing-error-handling': 0,
|
|
211
|
-
// JavaScript
|
|
212
|
-
'js-var-usage': 0,
|
|
213
|
-
'js-loose-equality': 0,
|
|
214
|
-
'js-implicit-coercion': 0,
|
|
215
|
-
'js-global-pollution': 0,
|
|
216
|
-
// TypeScript
|
|
217
|
-
'ts-any-usage': 0,
|
|
218
|
-
'ts-missing-return-type': 0,
|
|
219
|
-
'ts-non-null-assertion': 0,
|
|
220
|
-
'ts-type-assertion': 0,
|
|
221
|
-
// Debug statements
|
|
222
|
-
'debug-statement': 0,
|
|
223
|
-
'todo-comment': 0,
|
|
224
|
-
// Security
|
|
225
|
-
'security-xss': 0,
|
|
226
|
-
'security-eval': 0,
|
|
227
|
-
'security-secrets': 0,
|
|
228
|
-
// Accessibility
|
|
229
|
-
'a11y-missing-alt': 0,
|
|
230
|
-
'a11y-missing-label': 0,
|
|
231
|
-
'a11y-interactive-role': 0,
|
|
232
|
-
'a11y-keyboard': 0,
|
|
233
|
-
'a11y-semantic': 0,
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
const smellsBySeverity: Record<SmellSeverity, number> = {
|
|
237
|
-
error: 0,
|
|
238
|
-
warning: 0,
|
|
239
|
-
info: 0,
|
|
240
|
-
};
|
|
241
|
-
|
|
242
|
-
let totalSmells = 0;
|
|
243
|
-
let totalComponents = 0;
|
|
244
|
-
|
|
245
|
-
files.forEach(file => {
|
|
246
|
-
totalComponents += file.components.length;
|
|
247
|
-
file.smells.forEach(smell => {
|
|
248
|
-
totalSmells++;
|
|
249
|
-
smellsByType[smell.type]++;
|
|
250
|
-
smellsBySeverity[smell.severity]++;
|
|
251
|
-
});
|
|
252
|
-
});
|
|
253
|
-
|
|
254
|
-
return {
|
|
255
|
-
totalFiles: files.length,
|
|
256
|
-
totalComponents,
|
|
257
|
-
totalSmells,
|
|
258
|
-
smellsByType,
|
|
259
|
-
smellsBySeverity,
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function calculateTechnicalDebtScore(files: FileAnalysis[], summary: AnalysisSummary): TechnicalDebtScore {
|
|
264
|
-
// Calculate individual scores (0-100, higher is better)
|
|
265
|
-
|
|
266
|
-
// useEffect score: penalize based on useEffect-related issues
|
|
267
|
-
const useEffectIssues = summary.smellsByType['useEffect-overuse'];
|
|
268
|
-
const useEffectScore = Math.max(0, 100 - useEffectIssues * 15);
|
|
269
|
-
|
|
270
|
-
// Prop drilling score
|
|
271
|
-
const propDrillingIssues = summary.smellsByType['prop-drilling'];
|
|
272
|
-
const propDrillingScore = Math.max(0, 100 - propDrillingIssues * 12);
|
|
273
|
-
|
|
274
|
-
// Component size score
|
|
275
|
-
const sizeIssues = summary.smellsByType['large-component'] + summary.smellsByType['deep-nesting'];
|
|
276
|
-
const componentSizeScore = Math.max(0, 100 - sizeIssues * 10);
|
|
277
|
-
|
|
278
|
-
// Memoization score
|
|
279
|
-
const memoIssues = summary.smellsByType['unmemoized-calculation'] + summary.smellsByType['inline-function-prop'];
|
|
280
|
-
const memoizationScore = Math.max(0, 100 - memoIssues * 8);
|
|
281
|
-
|
|
282
|
-
// Overall score (weighted average)
|
|
283
|
-
const score = Math.round(
|
|
284
|
-
useEffectScore * 0.3 +
|
|
285
|
-
propDrillingScore * 0.25 +
|
|
286
|
-
componentSizeScore * 0.25 +
|
|
287
|
-
memoizationScore * 0.2
|
|
288
|
-
);
|
|
289
|
-
|
|
290
|
-
// Determine grade
|
|
291
|
-
let grade: 'A' | 'B' | 'C' | 'D' | 'F';
|
|
292
|
-
if (score >= 90) grade = 'A';
|
|
293
|
-
else if (score >= 80) grade = 'B';
|
|
294
|
-
else if (score >= 70) grade = 'C';
|
|
295
|
-
else if (score >= 60) grade = 'D';
|
|
296
|
-
else grade = 'F';
|
|
297
|
-
|
|
298
|
-
// Estimate refactor time
|
|
299
|
-
const errorCount = summary.smellsBySeverity.error;
|
|
300
|
-
const warningCount = summary.smellsBySeverity.warning;
|
|
301
|
-
const totalIssues = errorCount * 30 + warningCount * 15; // minutes
|
|
302
|
-
|
|
303
|
-
let estimatedRefactorTime: string;
|
|
304
|
-
if (totalIssues < 30) estimatedRefactorTime = '< 30 minutes';
|
|
305
|
-
else if (totalIssues < 60) estimatedRefactorTime = '30 min - 1 hour';
|
|
306
|
-
else if (totalIssues < 120) estimatedRefactorTime = '1-2 hours';
|
|
307
|
-
else if (totalIssues < 240) estimatedRefactorTime = '2-4 hours';
|
|
308
|
-
else if (totalIssues < 480) estimatedRefactorTime = '4-8 hours';
|
|
309
|
-
else estimatedRefactorTime = '> 1 day';
|
|
310
|
-
|
|
311
|
-
return {
|
|
312
|
-
score,
|
|
313
|
-
grade,
|
|
314
|
-
breakdown: {
|
|
315
|
-
useEffectScore,
|
|
316
|
-
propDrillingScore,
|
|
317
|
-
componentSizeScore,
|
|
318
|
-
memoizationScore,
|
|
319
|
-
},
|
|
320
|
-
estimatedRefactorTime,
|
|
321
|
-
};
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
export { DEFAULT_CONFIG, DetectorConfig } from './types/index.js';
|