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 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:
@@ -1 +1 @@
1
- {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AA4BA,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;AA8PD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC"}
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"}
@@ -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"}
@@ -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
@@ -1,6 +1,9 @@
1
1
  export interface GitInfo {
2
2
  isGitRepo: boolean;
3
3
  currentBranch?: string;
4
+ branch?: string;
5
+ currentCommit?: string;
6
+ authorName?: string;
4
7
  changedFiles: string[];
5
8
  stagedFiles: string[];
6
9
  untrackedFiles: string[];
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,CA0DnD;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"}
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
  }
@@ -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":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/types/index.ts"],"names":[],"mappings":"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"}
@@ -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"}
@@ -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.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": {