react-code-smell-detector 1.3.1 → 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/README.md +154 -4
- package/dist/analyzer.d.ts.map +1 -1
- package/dist/analyzer.js +5 -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 +32 -0
- package/dist/detectors/index.d.ts +1 -0
- package/dist/detectors/index.d.ts.map +1 -1
- package/dist/detectors/index.js +1 -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/git.d.ts +3 -0
- package/dist/git.d.ts.map +1 -1
- package/dist/git.js +20 -0
- package/dist/reporter.js +3 -0
- package/dist/types/index.d.ts +7 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +9 -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 +3 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@ A CLI tool that analyzes React projects and detects common code smells, providin
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
- 🔍 **Detect Code Smells**: Identifies common React anti-patterns
|
|
7
|
+
- 🔍 **Detect Code Smells**: Identifies common React anti-patterns (51+ smell types)
|
|
8
8
|
- 📊 **Technical Debt Score**: Grades your codebase from A to F
|
|
9
9
|
- 💡 **Refactoring Suggestions**: Actionable recommendations for each issue
|
|
10
10
|
- 📝 **Multiple Output Formats**: Console (colored), JSON, Markdown, and HTML
|
|
@@ -18,6 +18,9 @@ A CLI tool that analyzes React projects and detects common code smells, providin
|
|
|
18
18
|
- 🧮 **Complexity Metrics**: Cyclomatic and cognitive complexity scoring
|
|
19
19
|
- 💧 **Memory Leak Detection**: Find missing cleanup in useEffect
|
|
20
20
|
- 🔄 **Import Analysis**: Detect circular dependencies and barrel file issues
|
|
21
|
+
- 🗑️ **Unused Code Detection**: Find unused exports and dead imports
|
|
22
|
+
- 📈 **Baseline Tracking**: Track code smell trends over time with git commit history
|
|
23
|
+
- 💬 **Chat Notifications**: Send analysis results to Slack, Discord, or custom webhooks
|
|
21
24
|
|
|
22
25
|
### Detected Code Smells
|
|
23
26
|
|
|
@@ -34,6 +37,7 @@ A CLI tool that analyzes React projects and detects common code smells, providin
|
|
|
34
37
|
| **Memory Leaks** | Missing cleanup for event listeners, timers, subscriptions |
|
|
35
38
|
| **Code Complexity** | Cyclomatic complexity, cognitive complexity, deep nesting |
|
|
36
39
|
| **Import Issues** | Circular dependencies, barrel file imports, excessive imports |
|
|
40
|
+
| **Unused Code** | Unused exports, dead imports |
|
|
37
41
|
| **Framework-Specific** | Next.js, React Native, Node.js, TypeScript issues |
|
|
38
42
|
|
|
39
43
|
## Installation
|
|
@@ -115,6 +119,11 @@ Or create manually:
|
|
|
115
119
|
| `--include <patterns>` | Glob patterns to include | `**/*.tsx,**/*.jsx` |
|
|
116
120
|
| `--exclude <patterns>` | Glob patterns to exclude | `node_modules,dist` |
|
|
117
121
|
| `-o, --output <file>` | Write output to file | - |
|
|
122
|
+
| `--baseline` | Enable baseline tracking and trend analysis | `false` |
|
|
123
|
+
| `--slack <url>` | Slack webhook URL for notifications | - |
|
|
124
|
+
| `--discord <url>` | Discord webhook URL for notifications | - |
|
|
125
|
+
| `--webhook <url>` | Generic webhook URL for notifications | - |
|
|
126
|
+
| `--webhook-threshold <number>` | Only notify if smells exceed threshold | `10` |
|
|
118
127
|
|
|
119
128
|
### Auto-Fix
|
|
120
129
|
|
|
@@ -150,6 +159,99 @@ Only analyze modified files:
|
|
|
150
159
|
react-smell ./src --changed
|
|
151
160
|
```
|
|
152
161
|
|
|
162
|
+
### Baseline Tracking & Trend Analysis
|
|
163
|
+
|
|
164
|
+
Track code smell trends over time with automatic baseline recording:
|
|
165
|
+
|
|
166
|
+
```bash
|
|
167
|
+
# Enable baseline tracking
|
|
168
|
+
react-smell ./src --baseline
|
|
169
|
+
|
|
170
|
+
# Output includes trend analysis
|
|
171
|
+
📊 Code Smell Trend Analysis
|
|
172
|
+
──────────────────────────
|
|
173
|
+
Latest Run: 42 total smells
|
|
174
|
+
Trend: IMPROVING
|
|
175
|
+
Previous Run: 48 smells
|
|
176
|
+
Improved: 6 issues fixed
|
|
177
|
+
Worsened: 0 new issues
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Features:**
|
|
181
|
+
- Stores history in `.smellrc-baseline.json`
|
|
182
|
+
- Tracks timestamps, commit hashes, and author names
|
|
183
|
+
- Automatic trend calculation (improving/worsening/stable)
|
|
184
|
+
- Keeps last 50 records with automatic cleanup
|
|
185
|
+
- Perfect for CI/CD pipelines to monitor progress
|
|
186
|
+
|
|
187
|
+
**Configuration:**
|
|
188
|
+
```json
|
|
189
|
+
{
|
|
190
|
+
"baselineEnabled": true,
|
|
191
|
+
"baselineThreshold": 5
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### Chat Notifications
|
|
196
|
+
|
|
197
|
+
Send analysis results to Slack, Discord, or custom webhooks:
|
|
198
|
+
|
|
199
|
+
#### Slack Integration
|
|
200
|
+
|
|
201
|
+
```bash
|
|
202
|
+
react-smell ./src --slack https://hooks.slack.com/services/YOUR/WEBHOOK
|
|
203
|
+
|
|
204
|
+
# With threshold (only notify if issues exceed limit)
|
|
205
|
+
react-smell ./src --slack $SLACK_URL --webhook-threshold 20
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Features:**
|
|
209
|
+
- Rich formatted messages with severity levels
|
|
210
|
+
- Includes branch, commit hash, and author info
|
|
211
|
+
- Shows top 5 issue types by frequency
|
|
212
|
+
- Color-coded severity (Critical/High/Medium/Low)
|
|
213
|
+
|
|
214
|
+
#### Discord Integration
|
|
215
|
+
|
|
216
|
+
```bash
|
|
217
|
+
react-smell ./src --discord https://discord.com/api/webhooks/123/abc
|
|
218
|
+
|
|
219
|
+
# With environment variable
|
|
220
|
+
export REACT_SMELL_DISCORD_WEBHOOK=https://...
|
|
221
|
+
react-smell ./src --discord $REACT_SMELL_DISCORD_WEBHOOK
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
**Features:**
|
|
225
|
+
- Embedded messages with visual design
|
|
226
|
+
- Color-coded smells by severity
|
|
227
|
+
- Full metadata included (branch, author, commit)
|
|
228
|
+
|
|
229
|
+
#### Generic Webhooks
|
|
230
|
+
|
|
231
|
+
```bash
|
|
232
|
+
react-smell ./src --webhook https://example.com/webhook
|
|
233
|
+
|
|
234
|
+
# Custom platform webhooks
|
|
235
|
+
react-smell ./src --webhook $CUSTOM_URL --webhook-threshold 15
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
**Environment Variables:**
|
|
239
|
+
```bash
|
|
240
|
+
export REACT_SMELL_SLACK_WEBHOOK=https://...
|
|
241
|
+
export REACT_SMELL_DISCORD_WEBHOOK=https://...
|
|
242
|
+
export REACT_SMELL_WEBHOOK=https://...
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
**CI/CD Example with Notifications:**
|
|
246
|
+
```bash
|
|
247
|
+
# Run analysis, track baseline, and notify on failures
|
|
248
|
+
react-smell ./src \
|
|
249
|
+
--baseline \
|
|
250
|
+
--slack $SLACK_WEBHOOK \
|
|
251
|
+
--webhook-threshold 10 \
|
|
252
|
+
--ci
|
|
253
|
+
```
|
|
254
|
+
|
|
153
255
|
## Example Output
|
|
154
256
|
|
|
155
257
|
```
|
|
@@ -180,18 +282,41 @@ react-smell ./src --changed
|
|
|
180
282
|
|
|
181
283
|
```typescript
|
|
182
284
|
import { analyzeProject, reportResults } from 'react-code-smell-detector';
|
|
285
|
+
import { initializeBaseline, recordBaseline, getTrendAnalysis, formatTrendReport } from 'react-code-smell-detector';
|
|
286
|
+
import { sendWebhookNotification, getWebhookConfig } from 'react-code-smell-detector';
|
|
183
287
|
|
|
184
288
|
const result = await analyzeProject({
|
|
185
289
|
rootDir: './src',
|
|
186
290
|
config: {
|
|
187
291
|
maxUseEffectsPerComponent: 3,
|
|
188
292
|
maxComponentLines: 300,
|
|
293
|
+
checkUnusedCode: true,
|
|
294
|
+
baselineEnabled: true,
|
|
189
295
|
},
|
|
190
296
|
});
|
|
191
297
|
|
|
192
298
|
console.log(`Grade: ${result.debtScore.grade}`);
|
|
193
299
|
console.log(`Total issues: ${result.summary.totalSmells}`);
|
|
194
300
|
|
|
301
|
+
// Track baseline
|
|
302
|
+
initializeBaseline('./src');
|
|
303
|
+
recordBaseline('./src', result.files.flatMap(f => f.smells));
|
|
304
|
+
const trend = getTrendAnalysis('./src');
|
|
305
|
+
console.log(formatTrendReport('./src'));
|
|
306
|
+
|
|
307
|
+
// Send notification
|
|
308
|
+
const webhookConfig = getWebhookConfig(
|
|
309
|
+
process.env.REACT_SMELL_SLACK_WEBHOOK,
|
|
310
|
+
process.env.REACT_SMELL_DISCORD_WEBHOOK
|
|
311
|
+
);
|
|
312
|
+
if (webhookConfig) {
|
|
313
|
+
await sendWebhookNotification(
|
|
314
|
+
webhookConfig,
|
|
315
|
+
result.files.flatMap(f => f.smells),
|
|
316
|
+
'my-project'
|
|
317
|
+
);
|
|
318
|
+
}
|
|
319
|
+
|
|
195
320
|
// Or use the reporter
|
|
196
321
|
const report = reportResults(result, {
|
|
197
322
|
format: 'markdown',
|
|
@@ -202,7 +327,7 @@ const report = reportResults(result, {
|
|
|
202
327
|
|
|
203
328
|
## CI/CD Integration
|
|
204
329
|
|
|
205
|
-
The tool provides flexible exit codes for CI/CD pipelines:
|
|
330
|
+
The tool provides flexible exit codes and notification capabilities for CI/CD pipelines:
|
|
206
331
|
|
|
207
332
|
```bash
|
|
208
333
|
# Fail on any issues (strict mode)
|
|
@@ -213,6 +338,9 @@ react-smell ./src --fail-on warning
|
|
|
213
338
|
|
|
214
339
|
# Generate HTML report and fail on errors
|
|
215
340
|
react-smell ./src -f html -o report.html --fail-on error
|
|
341
|
+
|
|
342
|
+
# Track trends and notify
|
|
343
|
+
react-smell ./src --baseline --slack $SLACK_WEBHOOK --ci
|
|
216
344
|
```
|
|
217
345
|
|
|
218
346
|
### GitHub Actions Example
|
|
@@ -227,6 +355,9 @@ jobs:
|
|
|
227
355
|
runs-on: ubuntu-latest
|
|
228
356
|
steps:
|
|
229
357
|
- uses: actions/checkout@v4
|
|
358
|
+
with:
|
|
359
|
+
fetch-depth: 0 # For git baseline tracking
|
|
360
|
+
|
|
230
361
|
- uses: actions/setup-node@v4
|
|
231
362
|
with:
|
|
232
363
|
node-version: '20'
|
|
@@ -234,8 +365,12 @@ jobs:
|
|
|
234
365
|
- name: Install dependencies
|
|
235
366
|
run: npm ci
|
|
236
367
|
|
|
237
|
-
- name: Check code smells
|
|
238
|
-
run: npx react-smell ./src -f html -o smell-report.html --fail-on warning
|
|
368
|
+
- name: Check code smells with baseline tracking
|
|
369
|
+
run: npx react-smell ./src -f html -o smell-report.html --baseline --fail-on warning
|
|
370
|
+
|
|
371
|
+
- name: Notify Slack on failure
|
|
372
|
+
if: failure()
|
|
373
|
+
run: npx react-smell ./src --slack ${{ secrets.SLACK_WEBHOOK }} --webhook-threshold 10
|
|
239
374
|
|
|
240
375
|
- name: Upload report
|
|
241
376
|
uses: actions/upload-artifact@v4
|
|
@@ -245,6 +380,21 @@ jobs:
|
|
|
245
380
|
path: smell-report.html
|
|
246
381
|
```
|
|
247
382
|
|
|
383
|
+
### GitLab CI Example
|
|
384
|
+
|
|
385
|
+
```yaml
|
|
386
|
+
code-quality:
|
|
387
|
+
image: node:20
|
|
388
|
+
script:
|
|
389
|
+
- npm ci
|
|
390
|
+
- npx react-smell ./src --baseline --fail-on warning
|
|
391
|
+
artifacts:
|
|
392
|
+
reports:
|
|
393
|
+
codequality: code-smell-report.json
|
|
394
|
+
after_script:
|
|
395
|
+
- npx react-smell ./src --discord $DISCORD_WEBHOOK || true
|
|
396
|
+
```
|
|
397
|
+
|
|
248
398
|
## Ignoring Issues
|
|
249
399
|
|
|
250
400
|
Use `@smell-ignore` comments to suppress specific issues:
|
package/dist/analyzer.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AA6BA,OAAO,EACL,cAAc,EAMd,cAAc,EAIf,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;CAClC;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CA0CtF;AAkQD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC"}
|
package/dist/analyzer.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fg from 'fast-glob';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import { parseFile } from './parser/index.js';
|
|
4
|
-
import { detectUseEffectOveruse, detectPropDrilling, analyzePropDrillingDepth, detectLargeComponent, detectUnmemoizedCalculations, detectMissingKeys, detectHooksRulesViolations, detectDependencyArrayIssues, detectNestedTernaries, detectDeadCode, detectMagicValues, detectNextjsIssues, detectReactNativeIssues, detectNodejsIssues, detectJavascriptIssues, detectTypescriptIssues, detectDebugStatements, detectSecurityIssues, detectAccessibilityIssues, detectComplexity, detectMemoryLeaks, detectImportIssues, } from './detectors/index.js';
|
|
4
|
+
import { detectUseEffectOveruse, detectPropDrilling, analyzePropDrillingDepth, detectLargeComponent, detectUnmemoizedCalculations, detectMissingKeys, detectHooksRulesViolations, detectDependencyArrayIssues, detectNestedTernaries, detectDeadCode, detectMagicValues, detectNextjsIssues, detectReactNativeIssues, detectNodejsIssues, detectJavascriptIssues, detectTypescriptIssues, detectDebugStatements, detectSecurityIssues, detectAccessibilityIssues, detectComplexity, detectMemoryLeaks, detectImportIssues, detectUnusedCode, } from './detectors/index.js';
|
|
5
5
|
import { DEFAULT_CONFIG, } from './types/index.js';
|
|
6
6
|
export async function analyzeProject(options) {
|
|
7
7
|
const { rootDir, include = ['**/*.tsx', '**/*.jsx'], exclude = ['**/node_modules/**', '**/dist/**', '**/build/**', '**/*.test.*', '**/*.spec.*'], config: userConfig = {}, } = options;
|
|
@@ -82,6 +82,7 @@ function analyzeFile(parseResult, filePath, config) {
|
|
|
82
82
|
smells.push(...detectComplexity(component, filePath, sourceCode, config));
|
|
83
83
|
smells.push(...detectMemoryLeaks(component, filePath, sourceCode, config));
|
|
84
84
|
smells.push(...detectImportIssues(component, filePath, sourceCode, config));
|
|
85
|
+
smells.push(...detectUnusedCode(component, filePath, sourceCode, config));
|
|
85
86
|
});
|
|
86
87
|
// Run cross-component analysis
|
|
87
88
|
smells.push(...analyzePropDrillingDepth(components, filePath, sourceCode, config));
|
|
@@ -187,6 +188,9 @@ function calculateSummary(files) {
|
|
|
187
188
|
'barrel-file-import': 0,
|
|
188
189
|
'namespace-import': 0,
|
|
189
190
|
'excessive-imports': 0,
|
|
191
|
+
// Unused code
|
|
192
|
+
'unused-export': 0,
|
|
193
|
+
'dead-import': 0,
|
|
190
194
|
};
|
|
191
195
|
const smellsBySeverity = {
|
|
192
196
|
error: 0,
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { CodeSmell } from './types/index.js';
|
|
2
|
+
export interface BaselineRecord {
|
|
3
|
+
timestamp: string;
|
|
4
|
+
commit?: string;
|
|
5
|
+
totalSmells: number;
|
|
6
|
+
byType: Record<string, number>;
|
|
7
|
+
smells: CodeSmell[];
|
|
8
|
+
}
|
|
9
|
+
export interface BaselineData {
|
|
10
|
+
version: '1.0';
|
|
11
|
+
records: BaselineRecord[];
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Initialize baseline tracking
|
|
15
|
+
*/
|
|
16
|
+
export declare function initializeBaseline(projectRoot: string): void;
|
|
17
|
+
/**
|
|
18
|
+
* Record current analysis result
|
|
19
|
+
*/
|
|
20
|
+
export declare function recordBaseline(projectRoot: string, smells: CodeSmell[], commit?: string): BaselineRecord;
|
|
21
|
+
/**
|
|
22
|
+
* Get trend analysis compared to previous baseline
|
|
23
|
+
*/
|
|
24
|
+
export declare function getTrendAnalysis(projectRoot: string): {
|
|
25
|
+
improved: number;
|
|
26
|
+
worsened: number;
|
|
27
|
+
trend: 'improving' | 'worsening' | 'stable';
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Get baseline history
|
|
31
|
+
*/
|
|
32
|
+
export declare function getBaselineHistory(projectRoot: string): BaselineRecord[];
|
|
33
|
+
/**
|
|
34
|
+
* Format trend report
|
|
35
|
+
*/
|
|
36
|
+
export declare function formatTrendReport(projectRoot: string): string;
|
|
37
|
+
//# sourceMappingURL=baseline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"baseline.d.ts","sourceRoot":"","sources":["../src/baseline.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,MAAM,EAAE,SAAS,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,KAAK,CAAC;IACf,OAAO,EAAE,cAAc,EAAE,CAAC;CAC3B;AAID;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAa5D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAC5B,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,SAAS,EAAE,EACnB,MAAM,CAAC,EAAE,MAAM,GACd,cAAc,CAgChB;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,WAAW,EAAE,MAAM,GAClB;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,WAAW,GAAG,WAAW,GAAG,QAAQ,CAAA;CAAE,CAuBrF;AAED;;GAEG;AACH,wBAAgB,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,cAAc,EAAE,CAQxE;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,CAsB7D"}
|
package/dist/baseline.js
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
const BASELINE_FILE = '.smellrc-baseline.json';
|
|
4
|
+
/**
|
|
5
|
+
* Initialize baseline tracking
|
|
6
|
+
*/
|
|
7
|
+
export function initializeBaseline(projectRoot) {
|
|
8
|
+
const baselinePath = path.join(projectRoot, BASELINE_FILE);
|
|
9
|
+
try {
|
|
10
|
+
const stats = fs.statSync(baselinePath);
|
|
11
|
+
// File exists
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
// File doesn't exist, create it
|
|
15
|
+
const data = {
|
|
16
|
+
version: '1.0',
|
|
17
|
+
records: [],
|
|
18
|
+
};
|
|
19
|
+
fs.writeFileSync(baselinePath, JSON.stringify(data, null, 2));
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Record current analysis result
|
|
24
|
+
*/
|
|
25
|
+
export function recordBaseline(projectRoot, smells, commit) {
|
|
26
|
+
const baselinePath = path.join(projectRoot, BASELINE_FILE);
|
|
27
|
+
let data;
|
|
28
|
+
try {
|
|
29
|
+
const content = fs.readFileSync(baselinePath, 'utf-8');
|
|
30
|
+
data = JSON.parse(content);
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
data = { version: '1.0', records: [] };
|
|
34
|
+
}
|
|
35
|
+
const byType = {};
|
|
36
|
+
for (const smell of smells) {
|
|
37
|
+
byType[smell.type] = (byType[smell.type] || 0) + 1;
|
|
38
|
+
}
|
|
39
|
+
const record = {
|
|
40
|
+
timestamp: new Date().toISOString(),
|
|
41
|
+
commit,
|
|
42
|
+
totalSmells: smells.length,
|
|
43
|
+
byType,
|
|
44
|
+
smells: smells.slice(0, 100), // Keep first 100 for history
|
|
45
|
+
};
|
|
46
|
+
data.records.push(record);
|
|
47
|
+
// Keep last 50 records
|
|
48
|
+
if (data.records.length > 50) {
|
|
49
|
+
data.records = data.records.slice(-50);
|
|
50
|
+
}
|
|
51
|
+
fs.writeFileSync(baselinePath, JSON.stringify(data, null, 2));
|
|
52
|
+
return record;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Get trend analysis compared to previous baseline
|
|
56
|
+
*/
|
|
57
|
+
export function getTrendAnalysis(projectRoot) {
|
|
58
|
+
const baselinePath = path.join(projectRoot, BASELINE_FILE);
|
|
59
|
+
if (!fs.existsSync(baselinePath)) {
|
|
60
|
+
return { improved: 0, worsened: 0, trend: 'stable' };
|
|
61
|
+
}
|
|
62
|
+
const data = JSON.parse(fs.readFileSync(baselinePath, 'utf-8'));
|
|
63
|
+
if (data.records.length < 2) {
|
|
64
|
+
return { improved: 0, worsened: 0, trend: 'stable' };
|
|
65
|
+
}
|
|
66
|
+
const current = data.records[data.records.length - 1];
|
|
67
|
+
const previous = data.records[data.records.length - 2];
|
|
68
|
+
const diff = previous.totalSmells - current.totalSmells;
|
|
69
|
+
const improved = Math.max(0, diff);
|
|
70
|
+
const worsened = Math.max(0, -diff);
|
|
71
|
+
let trend = 'stable';
|
|
72
|
+
if (improved > worsened)
|
|
73
|
+
trend = 'improving';
|
|
74
|
+
if (worsened > improved)
|
|
75
|
+
trend = 'worsening';
|
|
76
|
+
return { improved, worsened, trend };
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Get baseline history
|
|
80
|
+
*/
|
|
81
|
+
export function getBaselineHistory(projectRoot) {
|
|
82
|
+
const baselinePath = path.join(projectRoot, BASELINE_FILE);
|
|
83
|
+
try {
|
|
84
|
+
const data = JSON.parse(fs.readFileSync(baselinePath, 'utf-8'));
|
|
85
|
+
return data.records;
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Format trend report
|
|
93
|
+
*/
|
|
94
|
+
export function formatTrendReport(projectRoot) {
|
|
95
|
+
const trend = getTrendAnalysis(projectRoot);
|
|
96
|
+
const history = getBaselineHistory(projectRoot);
|
|
97
|
+
if (history.length === 0) {
|
|
98
|
+
return 'No baseline history available yet.';
|
|
99
|
+
}
|
|
100
|
+
const latest = history[history.length - 1];
|
|
101
|
+
let report = `\n📊 Code Smell Trend Analysis\n`;
|
|
102
|
+
report += `──────────────────────────\n`;
|
|
103
|
+
report += `Latest Run: ${latest.totalSmells} total smells\n`;
|
|
104
|
+
report += `Trend: ${trend.trend.toUpperCase()}\n`;
|
|
105
|
+
if (history.length >= 2) {
|
|
106
|
+
const previous = history[history.length - 2];
|
|
107
|
+
report += `Previous Run: ${previous.totalSmells} smells\n`;
|
|
108
|
+
report += `Improved: ${trend.improved} issues fixed\n`;
|
|
109
|
+
report += `Worsened: ${trend.worsened} new issues\n`;
|
|
110
|
+
}
|
|
111
|
+
return report;
|
|
112
|
+
}
|
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,8 @@ import { generateHTMLReport } from './htmlReporter.js';
|
|
|
9
9
|
import { fixFile, isFixable } from './fixer.js';
|
|
10
10
|
import { startWatch } from './watcher.js';
|
|
11
11
|
import { getAllModifiedFiles, filterReactFiles, getGitInfo } from './git.js';
|
|
12
|
+
import { initializeBaseline, recordBaseline, formatTrendReport } from './baseline.js';
|
|
13
|
+
import { sendWebhookNotification, getWebhookConfig } from './webhooks.js';
|
|
12
14
|
import fs from 'fs/promises';
|
|
13
15
|
const program = new Command();
|
|
14
16
|
program
|
|
@@ -30,6 +32,11 @@ program
|
|
|
30
32
|
.option('--include <patterns>', 'Glob patterns to include (comma-separated)')
|
|
31
33
|
.option('--exclude <patterns>', 'Glob patterns to exclude (comma-separated)')
|
|
32
34
|
.option('-o, --output <file>', 'Write output to file')
|
|
35
|
+
.option('--baseline', 'Enable baseline tracking and trend analysis', false)
|
|
36
|
+
.option('--slack <url>', 'Slack webhook URL for notifications')
|
|
37
|
+
.option('--discord <url>', 'Discord webhook URL for notifications')
|
|
38
|
+
.option('--webhook <url>', 'Generic webhook URL for notifications')
|
|
39
|
+
.option('--webhook-threshold <number>', 'Only notify if smells exceed this threshold', parseInt)
|
|
33
40
|
.action(async (directory, options) => {
|
|
34
41
|
const rootDir = path.resolve(process.cwd(), directory);
|
|
35
42
|
// Check if directory exists
|
|
@@ -104,6 +111,10 @@ program
|
|
|
104
111
|
config,
|
|
105
112
|
});
|
|
106
113
|
spinner.stop();
|
|
114
|
+
// Initialize baseline if enabled
|
|
115
|
+
if (options.baseline) {
|
|
116
|
+
initializeBaseline(rootDir);
|
|
117
|
+
}
|
|
107
118
|
// Fix mode - apply auto-fixes
|
|
108
119
|
if (options.fix) {
|
|
109
120
|
const fixableSmells = result.files.flatMap(f => f.smells.filter(isFixable).map(s => ({ ...s, file: f.file })));
|
|
@@ -156,6 +167,27 @@ program
|
|
|
156
167
|
else {
|
|
157
168
|
console.log(output);
|
|
158
169
|
}
|
|
170
|
+
// Record baseline and show trend analysis
|
|
171
|
+
if (options.baseline) {
|
|
172
|
+
const gitInfo = getGitInfo(rootDir);
|
|
173
|
+
const baselineRecord = recordBaseline(rootDir, result.files.flatMap(f => f.smells), gitInfo.currentCommit);
|
|
174
|
+
console.log(formatTrendReport(rootDir));
|
|
175
|
+
}
|
|
176
|
+
// Send webhook notification
|
|
177
|
+
const webhookConfig = getWebhookConfig(options.slack, options.discord, options.webhook);
|
|
178
|
+
if (webhookConfig) {
|
|
179
|
+
webhookConfig.threshold = options.webhookThreshold;
|
|
180
|
+
const gitInfo = getGitInfo(rootDir);
|
|
181
|
+
const metadata = {
|
|
182
|
+
branch: gitInfo.branch,
|
|
183
|
+
commit: gitInfo.currentCommit,
|
|
184
|
+
author: gitInfo.authorName,
|
|
185
|
+
};
|
|
186
|
+
const sent = await sendWebhookNotification(webhookConfig, result.files.flatMap(f => f.smells), path.basename(rootDir), metadata);
|
|
187
|
+
if (sent) {
|
|
188
|
+
console.log(chalk.green('✓ Notification sent to webhook'));
|
|
189
|
+
}
|
|
190
|
+
}
|
|
159
191
|
// CI/CD exit code handling
|
|
160
192
|
const { smellsBySeverity } = result.summary;
|
|
161
193
|
let shouldFail = false;
|
|
@@ -19,4 +19,5 @@ export { detectAccessibilityIssues } from './accessibility.js';
|
|
|
19
19
|
export { detectComplexity, calculateComplexityMetrics } from './complexity.js';
|
|
20
20
|
export { detectMemoryLeaks } from './memoryLeak.js';
|
|
21
21
|
export { detectImportIssues, analyzeImports } from './imports.js';
|
|
22
|
+
export { detectUnusedCode } from './unusedCode.js';
|
|
22
23
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/detectors/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AACjF,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,4BAA4B,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAE/D,OAAO,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/detectors/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,gBAAgB,CAAC;AACxD,OAAO,EAAE,kBAAkB,EAAE,wBAAwB,EAAE,MAAM,mBAAmB,CAAC;AACjF,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,EAAE,4BAA4B,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,EAAE,2BAA2B,EAAE,MAAM,sBAAsB,CAAC;AACnE,OAAO,EAAE,qBAAqB,EAAE,MAAM,oBAAoB,CAAC;AAC3D,OAAO,EAAE,cAAc,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACpE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,uBAAuB,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,EAAE,kBAAkB,EAAE,MAAM,aAAa,CAAC;AACjD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AACzD,OAAO,EAAE,sBAAsB,EAAE,MAAM,iBAAiB,CAAC;AAEzD,OAAO,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AACnD,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC;AACrD,OAAO,EAAE,yBAAyB,EAAE,MAAM,oBAAoB,CAAC;AAE/D,OAAO,EAAE,gBAAgB,EAAE,0BAA0B,EAAE,MAAM,iBAAiB,CAAC;AAC/E,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,kBAAkB,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;AAClE,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC"}
|
package/dist/detectors/index.js
CHANGED
|
@@ -22,3 +22,4 @@ export { detectAccessibilityIssues } from './accessibility.js';
|
|
|
22
22
|
export { detectComplexity, calculateComplexityMetrics } from './complexity.js';
|
|
23
23
|
export { detectMemoryLeaks } from './memoryLeak.js';
|
|
24
24
|
export { detectImportIssues, analyzeImports } from './imports.js';
|
|
25
|
+
export { detectUnusedCode } from './unusedCode.js';
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { ParsedComponent } from '../parser/index.js';
|
|
2
|
+
import { CodeSmell, DetectorConfig } from '../types/index.js';
|
|
3
|
+
/**
|
|
4
|
+
* Detect unused exports and dead imports
|
|
5
|
+
*/
|
|
6
|
+
export declare function detectUnusedCode(component: ParsedComponent, filePath: string, sourceCode: string, config: DetectorConfig): CodeSmell[];
|
|
7
|
+
//# sourceMappingURL=unusedCode.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"unusedCode.d.ts","sourceRoot":"","sources":["../../src/detectors/unusedCode.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAI9D;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,cAAc,GACrB,SAAS,EAAE,CAoEb"}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import * as t from '@babel/types';
|
|
2
|
+
import _traverse from '@babel/traverse';
|
|
3
|
+
const traverse = typeof _traverse === 'function' ? _traverse : _traverse.default;
|
|
4
|
+
/**
|
|
5
|
+
* Detect unused exports and dead imports
|
|
6
|
+
*/
|
|
7
|
+
export function detectUnusedCode(component, filePath, sourceCode, config) {
|
|
8
|
+
if (!config.checkUnusedCode)
|
|
9
|
+
return [];
|
|
10
|
+
const smells = [];
|
|
11
|
+
// Track all exported values
|
|
12
|
+
const exportedNames = new Set();
|
|
13
|
+
const importedNames = new Map(); // name -> usage count
|
|
14
|
+
// Collect all exports
|
|
15
|
+
component.path.traverse({
|
|
16
|
+
ExportNamedDeclaration(path) {
|
|
17
|
+
if (t.isFunctionDeclaration(path.node.declaration) && path.node.declaration.id) {
|
|
18
|
+
exportedNames.add(path.node.declaration.id.name);
|
|
19
|
+
}
|
|
20
|
+
if (t.isVariableDeclaration(path.node.declaration)) {
|
|
21
|
+
path.node.declaration.declarations.forEach(decl => {
|
|
22
|
+
if (t.isIdentifier(decl.id)) {
|
|
23
|
+
exportedNames.add(decl.id.name);
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
if (t.isClassDeclaration(path.node.declaration) && path.node.declaration.id) {
|
|
28
|
+
exportedNames.add(path.node.declaration.id.name);
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
ExportDefaultDeclaration(path) {
|
|
32
|
+
if (t.isFunctionDeclaration(path.node.declaration) && path.node.declaration.id) {
|
|
33
|
+
exportedNames.add(path.node.declaration.id.name);
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
// Collect all names (for finding unused)
|
|
38
|
+
const allNames = new Set();
|
|
39
|
+
component.path.traverse({
|
|
40
|
+
FunctionDeclaration(path) {
|
|
41
|
+
if (path.node.id)
|
|
42
|
+
allNames.add(path.node.id.name);
|
|
43
|
+
},
|
|
44
|
+
VariableDeclarator(path) {
|
|
45
|
+
if (t.isIdentifier(path.node.id)) {
|
|
46
|
+
allNames.add(path.node.id.name);
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
// Check for unused exported functions/variables
|
|
51
|
+
for (const name of exportedNames) {
|
|
52
|
+
// Count usages (rough check)
|
|
53
|
+
const usageCount = (sourceCode.match(new RegExp(`\\b${name}\\b`, 'g')) || []).length;
|
|
54
|
+
// If only mentioned once (the export itself), it's unused
|
|
55
|
+
if (usageCount <= 2) {
|
|
56
|
+
const loc = findLocationByName(sourceCode, name);
|
|
57
|
+
smells.push({
|
|
58
|
+
type: 'unused-export',
|
|
59
|
+
severity: 'info',
|
|
60
|
+
message: `Exported "${name}" is never used`,
|
|
61
|
+
file: filePath,
|
|
62
|
+
line: loc.line,
|
|
63
|
+
column: 0,
|
|
64
|
+
suggestion: 'Remove unused export or import it elsewhere in the project.',
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return smells;
|
|
69
|
+
}
|
|
70
|
+
function findLocationByName(source, name) {
|
|
71
|
+
const lines = source.split('\n');
|
|
72
|
+
for (let i = 0; i < lines.length; i++) {
|
|
73
|
+
if (lines[i].includes(`export`) && lines[i].includes(name)) {
|
|
74
|
+
return { line: i + 1 };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { line: 1 };
|
|
78
|
+
}
|
package/dist/git.d.ts
CHANGED
package/dist/git.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,OAAO;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,
|
|
1
|
+
{"version":3,"file":"git.d.ts","sourceRoot":"","sources":["../src/git.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,OAAO;IACtB,SAAS,EAAE,OAAO,CAAC;IACnB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,WAAW,EAAE,MAAM,EAAE,CAAC;IACtB,cAAc,EAAE,MAAM,EAAE,CAAC;CAC1B;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAiFnD;AAED;;GAEG;AACH,wBAAgB,aAAa,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,MAAM,EAAE,CAcpE;AAED;;GAEG;AACH,wBAAgB,uBAAuB,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,GAAE,MAAU,GAAG,MAAM,EAAE,CAcpF;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,EAAE,CAW7D;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,CAG1D"}
|
package/dist/git.js
CHANGED
|
@@ -14,6 +14,16 @@ export function getGitInfo(rootDir) {
|
|
|
14
14
|
cwd: rootDir,
|
|
15
15
|
encoding: 'utf-8',
|
|
16
16
|
}).trim();
|
|
17
|
+
// Get current commit hash
|
|
18
|
+
const currentCommit = execSync('git rev-parse HEAD', {
|
|
19
|
+
cwd: rootDir,
|
|
20
|
+
encoding: 'utf-8',
|
|
21
|
+
}).trim();
|
|
22
|
+
// Get author name
|
|
23
|
+
const authorName = execSync('git config user.name', {
|
|
24
|
+
cwd: rootDir,
|
|
25
|
+
encoding: 'utf-8',
|
|
26
|
+
}).trim();
|
|
17
27
|
// Get changed files (modified but not staged)
|
|
18
28
|
const changedOutput = execSync('git diff --name-only', {
|
|
19
29
|
cwd: rootDir,
|
|
@@ -41,6 +51,16 @@ export function getGitInfo(rootDir) {
|
|
|
41
51
|
.split('\n')
|
|
42
52
|
.filter(f => f.trim())
|
|
43
53
|
.map(f => path.resolve(rootDir, f));
|
|
54
|
+
return {
|
|
55
|
+
isGitRepo: true,
|
|
56
|
+
currentBranch,
|
|
57
|
+
branch: currentBranch,
|
|
58
|
+
currentCommit,
|
|
59
|
+
authorName,
|
|
60
|
+
changedFiles,
|
|
61
|
+
stagedFiles,
|
|
62
|
+
untrackedFiles,
|
|
63
|
+
};
|
|
44
64
|
return {
|
|
45
65
|
isGitRepo: true,
|
|
46
66
|
currentBranch,
|
package/dist/reporter.js
CHANGED
|
@@ -268,6 +268,9 @@ function formatSmellType(type) {
|
|
|
268
268
|
'barrel-file-import': '📦 Barrel File Import',
|
|
269
269
|
'namespace-import': '📦 Namespace Import',
|
|
270
270
|
'excessive-imports': '📦 Excessive Imports',
|
|
271
|
+
// Unused Code
|
|
272
|
+
'unused-export': '🗑️ Unused Export',
|
|
273
|
+
'dead-import': '🗑️ Dead Import',
|
|
271
274
|
};
|
|
272
275
|
return labels[type] || type;
|
|
273
276
|
}
|
package/dist/types/index.d.ts
CHANGED
|
@@ -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';
|
|
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';
|
|
3
3
|
export interface CodeSmell {
|
|
4
4
|
type: SmellType;
|
|
5
5
|
severity: SmellSeverity;
|
|
@@ -81,6 +81,12 @@ export interface DetectorConfig {
|
|
|
81
81
|
maxNestingDepth: number;
|
|
82
82
|
checkMemoryLeaks: boolean;
|
|
83
83
|
checkImports: boolean;
|
|
84
|
+
checkUnusedCode: boolean;
|
|
85
|
+
baselineEnabled: boolean;
|
|
86
|
+
baselineThreshold?: number;
|
|
87
|
+
webhookUrl?: string;
|
|
88
|
+
webhookType?: 'slack' | 'discord' | 'generic';
|
|
89
|
+
webhookThreshold?: number;
|
|
84
90
|
}
|
|
85
91
|
export declare const DEFAULT_CONFIG: DetectorConfig;
|
|
86
92
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,aAAa,GAAG,OAAO,GAAG,SAAS,GAAG,MAAM,CAAC;AAEzD,MAAM,MAAM,SAAS,GACjB,mBAAmB,GACnB,eAAe,GACf,iBAAiB,GACjB,wBAAwB,GACxB,oBAAoB,GACpB,eAAe,GACf,sBAAsB,GACtB,cAAc,GACd,aAAa,GACb,uBAAuB,GACvB,wBAAwB,GACxB,gBAAgB,GAChB,WAAW,GACX,aAAa,GAEb,iBAAiB,GACjB,cAAc,GAEd,cAAc,GACd,eAAe,GACf,kBAAkB,GAElB,kBAAkB,GAClB,oBAAoB,GACpB,uBAAuB,GACvB,eAAe,GACf,eAAe,GAEf,+BAA+B,GAC/B,yBAAyB,GACzB,0BAA0B,GAC1B,sBAAsB,GAEtB,iBAAiB,GACjB,0BAA0B,GAC1B,sBAAsB,GAEtB,sBAAsB,GACtB,0BAA0B,GAC1B,gBAAgB,GAChB,+BAA+B,GAE/B,cAAc,GACd,mBAAmB,GACnB,sBAAsB,GACtB,qBAAqB,GAErB,cAAc,GACd,wBAAwB,GACxB,uBAAuB,GACvB,mBAAmB,GAEnB,4BAA4B,GAC5B,2BAA2B,GAE3B,4BAA4B,GAC5B,0BAA0B,GAC1B,mBAAmB,GACnB,mBAAmB,GAEnB,qBAAqB,GACrB,oBAAoB,GACpB,kBAAkB,GAClB,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
|
package/dist/types/index.js
CHANGED
|
@@ -31,4 +31,13 @@ export const DEFAULT_CONFIG = {
|
|
|
31
31
|
checkMemoryLeaks: true,
|
|
32
32
|
// Import analysis
|
|
33
33
|
checkImports: true,
|
|
34
|
+
// Unused code analysis
|
|
35
|
+
checkUnusedCode: true,
|
|
36
|
+
// Baseline tracking
|
|
37
|
+
baselineEnabled: true,
|
|
38
|
+
baselineThreshold: 5,
|
|
39
|
+
// Webhook notifications
|
|
40
|
+
webhookUrl: undefined,
|
|
41
|
+
webhookType: 'slack',
|
|
42
|
+
webhookThreshold: 10,
|
|
34
43
|
};
|
|
@@ -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,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "react-code-smell-detector",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.1",
|
|
4
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": {
|
|
@@ -37,6 +37,8 @@
|
|
|
37
37
|
"chokidar": "^5.0.0",
|
|
38
38
|
"commander": "^11.1.0",
|
|
39
39
|
"fast-glob": "^3.3.2",
|
|
40
|
+
"fs-extra": "^11.3.3",
|
|
41
|
+
"node-fetch": "^3.3.2",
|
|
40
42
|
"ora": "^8.0.1"
|
|
41
43
|
},
|
|
42
44
|
"devDependencies": {
|