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.
Files changed (67) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +200 -4
  3. package/dist/analyzer.d.ts.map +1 -1
  4. package/dist/analyzer.js +22 -1
  5. package/dist/baseline.d.ts +37 -0
  6. package/dist/baseline.d.ts.map +1 -0
  7. package/dist/baseline.js +112 -0
  8. package/dist/cli.js +125 -26
  9. package/dist/detectors/complexity.d.ts +17 -0
  10. package/dist/detectors/complexity.d.ts.map +1 -0
  11. package/dist/detectors/complexity.js +69 -0
  12. package/dist/detectors/imports.d.ts +22 -0
  13. package/dist/detectors/imports.d.ts.map +1 -0
  14. package/dist/detectors/imports.js +210 -0
  15. package/dist/detectors/index.d.ts +4 -0
  16. package/dist/detectors/index.d.ts.map +1 -1
  17. package/dist/detectors/index.js +5 -0
  18. package/dist/detectors/memoryLeak.d.ts +7 -0
  19. package/dist/detectors/memoryLeak.d.ts.map +1 -0
  20. package/dist/detectors/memoryLeak.js +111 -0
  21. package/dist/detectors/unusedCode.d.ts +7 -0
  22. package/dist/detectors/unusedCode.d.ts.map +1 -0
  23. package/dist/detectors/unusedCode.js +78 -0
  24. package/dist/fixer.d.ts +23 -0
  25. package/dist/fixer.d.ts.map +1 -0
  26. package/dist/fixer.js +133 -0
  27. package/dist/git.d.ts +31 -0
  28. package/dist/git.d.ts.map +1 -0
  29. package/dist/git.js +137 -0
  30. package/dist/reporter.js +16 -0
  31. package/dist/types/index.d.ts +13 -1
  32. package/dist/types/index.d.ts.map +1 -1
  33. package/dist/types/index.js +18 -0
  34. package/dist/watcher.d.ts +16 -0
  35. package/dist/watcher.d.ts.map +1 -0
  36. package/dist/watcher.js +89 -0
  37. package/dist/webhooks.d.ts +20 -0
  38. package/dist/webhooks.d.ts.map +1 -0
  39. package/dist/webhooks.js +199 -0
  40. package/package.json +10 -2
  41. package/src/analyzer.ts +0 -324
  42. package/src/cli.ts +0 -159
  43. package/src/detectors/accessibility.ts +0 -212
  44. package/src/detectors/deadCode.ts +0 -163
  45. package/src/detectors/debug.ts +0 -103
  46. package/src/detectors/dependencyArray.ts +0 -176
  47. package/src/detectors/hooksRules.ts +0 -101
  48. package/src/detectors/index.ts +0 -20
  49. package/src/detectors/javascript.ts +0 -169
  50. package/src/detectors/largeComponent.ts +0 -63
  51. package/src/detectors/magicValues.ts +0 -114
  52. package/src/detectors/memoization.ts +0 -177
  53. package/src/detectors/missingKey.ts +0 -105
  54. package/src/detectors/nestedTernary.ts +0 -75
  55. package/src/detectors/nextjs.ts +0 -124
  56. package/src/detectors/nodejs.ts +0 -199
  57. package/src/detectors/propDrilling.ts +0 -103
  58. package/src/detectors/reactNative.ts +0 -154
  59. package/src/detectors/security.ts +0 -179
  60. package/src/detectors/typescript.ts +0 -151
  61. package/src/detectors/useEffect.ts +0 -117
  62. package/src/htmlReporter.ts +0 -464
  63. package/src/index.ts +0 -4
  64. package/src/parser/index.ts +0 -195
  65. package/src/reporter.ts +0 -291
  66. package/src/types/index.ts +0 -165
  67. package/tsconfig.json +0 -19
package/dist/cli.js CHANGED
@@ -6,24 +6,37 @@ import path from 'path';
6
6
  import { analyzeProject, DEFAULT_CONFIG } from './analyzer.js';
7
7
  import { reportResults } from './reporter.js';
8
8
  import { generateHTMLReport } from './htmlReporter.js';
9
+ import { fixFile, isFixable } from './fixer.js';
10
+ import { startWatch } from './watcher.js';
11
+ import { getAllModifiedFiles, filterReactFiles, getGitInfo } from './git.js';
12
+ import { initializeBaseline, recordBaseline, formatTrendReport } from './baseline.js';
13
+ import { sendWebhookNotification, getWebhookConfig } from './webhooks.js';
9
14
  import fs from 'fs/promises';
10
15
  const program = new Command();
11
16
  program
12
17
  .name('react-smell')
13
18
  .description('Detect code smells in React projects')
14
- .version('1.2.0')
19
+ .version('1.3.0')
15
20
  .argument('[directory]', 'Directory to analyze', '.')
16
21
  .option('-f, --format <format>', 'Output format: console, json, markdown, html', 'console')
17
22
  .option('-s, --snippets', 'Show code snippets in output', false)
18
23
  .option('-c, --config <file>', 'Path to config file')
19
24
  .option('--ci', 'CI mode: exit with code 1 if any issues found')
20
25
  .option('--fail-on <severity>', 'Exit with code 1 if issues of this severity or higher (error, warning, info)', 'error')
26
+ .option('--fix', 'Auto-fix simple issues (console.log, var, ==, missing alt)')
27
+ .option('--watch', 'Watch mode: re-analyze on file changes')
28
+ .option('--changed', 'Only analyze git-modified files')
21
29
  .option('--max-effects <number>', 'Max useEffects per component', parseInt)
22
30
  .option('--max-props <number>', 'Max props before warning', parseInt)
23
31
  .option('--max-lines <number>', 'Max lines per component', parseInt)
24
32
  .option('--include <patterns>', 'Glob patterns to include (comma-separated)')
25
33
  .option('--exclude <patterns>', 'Glob patterns to exclude (comma-separated)')
26
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)
27
40
  .action(async (directory, options) => {
28
41
  const rootDir = path.resolve(process.cwd(), directory);
29
42
  // Check if directory exists
@@ -34,38 +47,103 @@ program
34
47
  console.error(chalk.red(`Error: Directory "${rootDir}" does not exist.`));
35
48
  process.exit(1);
36
49
  }
37
- const spinner = ora('Analyzing React project...').start();
38
- try {
39
- // Load config file if specified
40
- let fileConfig = {};
41
- if (options.config) {
42
- try {
43
- const configPath = path.resolve(process.cwd(), options.config);
44
- const configContent = await fs.readFile(configPath, 'utf-8');
45
- fileConfig = JSON.parse(configContent);
46
- }
47
- catch (error) {
48
- spinner.fail(`Could not load config file: ${error.message}`);
49
- process.exit(1);
50
- }
50
+ // Load config file if specified
51
+ let fileConfig = {};
52
+ if (options.config) {
53
+ try {
54
+ const configPath = path.resolve(process.cwd(), options.config);
55
+ const configContent = await fs.readFile(configPath, 'utf-8');
56
+ fileConfig = JSON.parse(configContent);
51
57
  }
52
- // Build config from options
53
- const config = {
54
- ...DEFAULT_CONFIG,
55
- ...fileConfig,
56
- ...(options.maxEffects && { maxUseEffectsPerComponent: options.maxEffects }),
57
- ...(options.maxProps && { maxPropsCount: options.maxProps }),
58
- ...(options.maxLines && { maxComponentLines: options.maxLines }),
59
- };
60
- const include = options.include?.split(',').map((p) => p.trim()) || undefined;
61
- const exclude = options.exclude?.split(',').map((p) => p.trim()) || undefined;
62
- const result = await analyzeProject({
58
+ catch (error) {
59
+ console.error(chalk.red(`Could not load config file: ${error.message}`));
60
+ process.exit(1);
61
+ }
62
+ }
63
+ // Build config from options
64
+ const config = {
65
+ ...DEFAULT_CONFIG,
66
+ ...fileConfig,
67
+ ...(options.maxEffects && { maxUseEffectsPerComponent: options.maxEffects }),
68
+ ...(options.maxProps && { maxPropsCount: options.maxProps }),
69
+ ...(options.maxLines && { maxComponentLines: options.maxLines }),
70
+ };
71
+ const include = options.include?.split(',').map((p) => p.trim()) || ['**/*.tsx', '**/*.jsx'];
72
+ const exclude = options.exclude?.split(',').map((p) => p.trim()) || ['**/node_modules/**', '**/dist/**'];
73
+ // Watch mode
74
+ if (options.watch) {
75
+ const watcher = startWatch({
63
76
  rootDir,
64
77
  include,
65
78
  exclude,
66
79
  config,
80
+ showSnippets: options.snippets,
81
+ });
82
+ // Handle Ctrl+C gracefully
83
+ process.on('SIGINT', () => {
84
+ watcher.close();
85
+ process.exit(0);
86
+ });
87
+ return; // Don't continue to regular analysis
88
+ }
89
+ // Git changed files mode
90
+ let filesToAnalyze;
91
+ if (options.changed) {
92
+ const gitInfo = getGitInfo(rootDir);
93
+ if (!gitInfo.isGitRepo) {
94
+ console.error(chalk.red('Error: --changed requires a git repository'));
95
+ process.exit(1);
96
+ }
97
+ const modifiedFiles = getAllModifiedFiles(rootDir);
98
+ filesToAnalyze = filterReactFiles(modifiedFiles);
99
+ if (filesToAnalyze.length === 0) {
100
+ console.log(chalk.green('✓ No modified React files to analyze'));
101
+ process.exit(0);
102
+ }
103
+ console.log(chalk.cyan(`\n📝 Analyzing ${filesToAnalyze.length} modified file(s)...\n`));
104
+ }
105
+ const spinner = ora('Analyzing React project...').start();
106
+ try {
107
+ const result = await analyzeProject({
108
+ rootDir,
109
+ include: filesToAnalyze ? undefined : include,
110
+ exclude: filesToAnalyze ? undefined : exclude,
111
+ config,
67
112
  });
68
113
  spinner.stop();
114
+ // Initialize baseline if enabled
115
+ if (options.baseline) {
116
+ initializeBaseline(rootDir);
117
+ }
118
+ // Fix mode - apply auto-fixes
119
+ if (options.fix) {
120
+ const fixableSmells = result.files.flatMap(f => f.smells.filter(isFixable).map(s => ({ ...s, file: f.file })));
121
+ if (fixableSmells.length > 0) {
122
+ console.log(chalk.cyan(`\n🔧 Auto-fixing ${fixableSmells.length} issue(s)...\n`));
123
+ // Group by file
124
+ const smellsByFile = new Map();
125
+ fixableSmells.forEach(smell => {
126
+ const existing = smellsByFile.get(smell.file) || [];
127
+ existing.push(smell);
128
+ smellsByFile.set(smell.file, existing);
129
+ });
130
+ let totalFixed = 0;
131
+ for (const [file, smells] of smellsByFile) {
132
+ const fixResult = await fixFile(file, smells);
133
+ if (fixResult.fixedSmells.length > 0) {
134
+ console.log(chalk.green(` ✓ Fixed ${fixResult.fixedSmells.length} issue(s) in ${path.relative(rootDir, file)}`));
135
+ totalFixed += fixResult.fixedSmells.length;
136
+ }
137
+ }
138
+ console.log(chalk.green(`\n✓ Fixed ${totalFixed} issue(s) total\n`));
139
+ // Re-analyze after fixes
140
+ const newResult = await analyzeProject({ rootDir, include, exclude, config });
141
+ console.log(chalk.dim(`Remaining issues: ${newResult.summary.totalSmells}\n`));
142
+ }
143
+ else {
144
+ console.log(chalk.yellow('\nNo auto-fixable issues found\n'));
145
+ }
146
+ }
69
147
  let output;
70
148
  if (options.format === 'html') {
71
149
  output = generateHTMLReport(result, rootDir);
@@ -89,6 +167,27 @@ program
89
167
  else {
90
168
  console.log(output);
91
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
+ }
92
191
  // CI/CD exit code handling
93
192
  const { smellsBySeverity } = result.summary;
94
193
  let shouldFail = false;
@@ -0,0 +1,17 @@
1
+ import { ParsedComponent } from '../parser/index.js';
2
+ import { CodeSmell, DetectorConfig } from '../types/index.js';
3
+ export interface ComplexityMetrics {
4
+ cyclomaticComplexity: number;
5
+ cognitiveComplexity: number;
6
+ maxNestingDepth: number;
7
+ linesOfCode: number;
8
+ }
9
+ /**
10
+ * Detect code complexity issues in a component
11
+ */
12
+ export declare function detectComplexity(component: ParsedComponent, filePath: string, sourceCode: string, config: DetectorConfig): CodeSmell[];
13
+ /**
14
+ * Calculate complexity metrics for a component
15
+ */
16
+ export declare function calculateComplexityMetrics(component: ParsedComponent): ComplexityMetrics;
17
+ //# sourceMappingURL=complexity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"complexity.d.ts","sourceRoot":"","sources":["../../src/detectors/complexity.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAE9D,MAAM,WAAW,iBAAiB;IAChC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,mBAAmB,EAAE,MAAM,CAAC;IAC5B,eAAe,EAAE,MAAM,CAAC;IACxB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED;;GAEG;AACH,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,cAAc,GACrB,SAAS,EAAE,CAqCb;AAED;;GAEG;AACH,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,eAAe,GAAG,iBAAiB,CA6BxF"}
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Detect code complexity issues in a component
3
+ */
4
+ export function detectComplexity(component, filePath, sourceCode, config) {
5
+ if (!config.checkComplexity)
6
+ return [];
7
+ const smells = [];
8
+ const metrics = calculateComplexityMetrics(component);
9
+ const thresholds = {
10
+ cyclomatic: config.maxCyclomaticComplexity || 10,
11
+ cognitive: config.maxCognitiveComplexity || 15,
12
+ nesting: config.maxNestingDepth || 4,
13
+ };
14
+ if (metrics.cyclomaticComplexity > thresholds.cyclomatic) {
15
+ smells.push({
16
+ type: 'high-cyclomatic-complexity',
17
+ severity: metrics.cyclomaticComplexity > thresholds.cyclomatic * 1.5 ? 'error' : 'warning',
18
+ message: `Component "${component.name}" has cyclomatic complexity of ${metrics.cyclomaticComplexity} (threshold: ${thresholds.cyclomatic})`,
19
+ file: filePath,
20
+ line: component.startLine,
21
+ column: 0,
22
+ suggestion: 'Break down complex logic into smaller functions.',
23
+ });
24
+ }
25
+ if (metrics.cognitiveComplexity > thresholds.cognitive) {
26
+ smells.push({
27
+ type: 'high-cognitive-complexity',
28
+ severity: metrics.cognitiveComplexity > thresholds.cognitive * 1.5 ? 'error' : 'warning',
29
+ message: `Component "${component.name}" has cognitive complexity of ${metrics.cognitiveComplexity} (threshold: ${thresholds.cognitive})`,
30
+ file: filePath,
31
+ line: component.startLine,
32
+ column: 0,
33
+ suggestion: 'Simplify nested conditions and flatten control flow.',
34
+ });
35
+ }
36
+ return smells;
37
+ }
38
+ /**
39
+ * Calculate complexity metrics for a component
40
+ */
41
+ export function calculateComplexityMetrics(component) {
42
+ let cyclomaticComplexity = 1;
43
+ let cognitiveComplexity = 0;
44
+ component.path.traverse({
45
+ IfStatement() { cyclomaticComplexity++; cognitiveComplexity++; },
46
+ ConditionalExpression() { cyclomaticComplexity++; cognitiveComplexity++; },
47
+ LogicalExpression(path) {
48
+ if (path.node.operator === '&&' || path.node.operator === '||') {
49
+ cyclomaticComplexity++;
50
+ }
51
+ },
52
+ SwitchCase(path) {
53
+ if (path.node.test !== null)
54
+ cyclomaticComplexity++;
55
+ },
56
+ ForStatement() { cyclomaticComplexity++; cognitiveComplexity += 2; },
57
+ ForInStatement() { cyclomaticComplexity++; cognitiveComplexity += 2; },
58
+ ForOfStatement() { cyclomaticComplexity++; cognitiveComplexity += 2; },
59
+ WhileStatement() { cyclomaticComplexity++; cognitiveComplexity += 2; },
60
+ DoWhileStatement() { cyclomaticComplexity++; cognitiveComplexity += 2; },
61
+ CatchClause() { cyclomaticComplexity++; cognitiveComplexity++; },
62
+ });
63
+ return {
64
+ cyclomaticComplexity,
65
+ cognitiveComplexity,
66
+ maxNestingDepth: component.jsxDepth,
67
+ linesOfCode: component.endLine - component.startLine + 1,
68
+ };
69
+ }
@@ -0,0 +1,22 @@
1
+ import { CodeSmell, DetectorConfig } from '../types/index.js';
2
+ export interface ImportInfo {
3
+ source: string;
4
+ specifiers: string[];
5
+ line: number;
6
+ isDefault: boolean;
7
+ isNamespace: boolean;
8
+ }
9
+ export interface ImportAnalysisResult {
10
+ smells: CodeSmell[];
11
+ importGraph: Map<string, string[]>;
12
+ circularDeps: string[][];
13
+ }
14
+ /**
15
+ * Analyze imports across multiple files for issues
16
+ */
17
+ export declare function analyzeImports(files: string[], rootDir: string, config: DetectorConfig): Promise<ImportAnalysisResult>;
18
+ /**
19
+ * Standalone detection function matching other detector signatures
20
+ */
21
+ export declare function detectImportIssues(component: any, filePath: string, sourceCode: string, config: DetectorConfig): CodeSmell[];
22
+ //# sourceMappingURL=imports.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"imports.d.ts","sourceRoot":"","sources":["../../src/detectors/imports.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAK9D,MAAM,WAAW,UAAU;IACzB,MAAM,EAAE,MAAM,CAAC;IACf,UAAU,EAAE,MAAM,EAAE,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,OAAO,CAAC;CACtB;AAED,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,SAAS,EAAE,CAAC;IACpB,WAAW,EAAE,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IACnC,YAAY,EAAE,MAAM,EAAE,EAAE,CAAC;CAC1B;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,EAAE,MAAM,EACf,MAAM,EAAE,cAAc,GACrB,OAAO,CAAC,oBAAoB,CAAC,CAkD/B;AA8KD;;GAEG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,GAAG,EACd,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,cAAc,GACrB,SAAS,EAAE,CAGb"}
@@ -0,0 +1,210 @@
1
+ import * as t from '@babel/types';
2
+ import _traverse from '@babel/traverse';
3
+ import { parse } from '@babel/parser';
4
+ import fs from 'fs/promises';
5
+ import path from 'path';
6
+ // Handle ESM/CJS interop
7
+ const traverse = typeof _traverse === 'function' ? _traverse : _traverse.default;
8
+ /**
9
+ * Analyze imports across multiple files for issues
10
+ */
11
+ export async function analyzeImports(files, rootDir, config) {
12
+ const smells = [];
13
+ const importGraph = new Map();
14
+ // Build import graph
15
+ for (const file of files) {
16
+ try {
17
+ const content = await fs.readFile(file, 'utf-8');
18
+ const imports = extractImports(content, file);
19
+ const resolvedImports = [];
20
+ for (const imp of imports) {
21
+ // Skip external packages
22
+ if (!imp.source.startsWith('.') && !imp.source.startsWith('/')) {
23
+ continue;
24
+ }
25
+ const resolvedPath = resolveImportPath(file, imp.source, rootDir);
26
+ if (resolvedPath) {
27
+ resolvedImports.push(resolvedPath);
28
+ }
29
+ }
30
+ importGraph.set(file, resolvedImports);
31
+ // Detect import-related smells in this file
32
+ smells.push(...detectImportSmells(imports, file, content, config));
33
+ }
34
+ catch {
35
+ // Skip files that can't be parsed
36
+ }
37
+ }
38
+ // Detect circular dependencies
39
+ const circularDeps = detectCircularDependencies(importGraph);
40
+ // Add circular dependency smells
41
+ for (const cycle of circularDeps) {
42
+ const cycleStr = cycle.map(f => path.basename(f)).join(' → ');
43
+ smells.push({
44
+ type: 'circular-dependency',
45
+ severity: 'warning',
46
+ message: `Circular dependency detected: ${cycleStr}`,
47
+ file: cycle[0],
48
+ line: 1,
49
+ column: 0,
50
+ suggestion: 'Refactor to break the cycle. Consider extracting shared logic to a separate module.',
51
+ });
52
+ }
53
+ return { smells, importGraph, circularDeps };
54
+ }
55
+ /**
56
+ * Extract all imports from a file
57
+ */
58
+ function extractImports(sourceCode, filePath) {
59
+ const imports = [];
60
+ try {
61
+ const ast = parse(sourceCode, {
62
+ sourceType: 'module',
63
+ plugins: ['jsx', 'typescript'],
64
+ });
65
+ traverse(ast, {
66
+ ImportDeclaration(nodePath) {
67
+ const node = nodePath.node;
68
+ const specifiers = node.specifiers.map(spec => {
69
+ if (t.isImportDefaultSpecifier(spec))
70
+ return 'default';
71
+ if (t.isImportNamespaceSpecifier(spec))
72
+ return '*';
73
+ return spec.local.name;
74
+ });
75
+ imports.push({
76
+ source: node.source.value,
77
+ specifiers,
78
+ line: node.loc?.start.line || 0,
79
+ isDefault: node.specifiers.some(t.isImportDefaultSpecifier),
80
+ isNamespace: node.specifiers.some(t.isImportNamespaceSpecifier),
81
+ });
82
+ },
83
+ });
84
+ }
85
+ catch {
86
+ // Parse error, skip
87
+ }
88
+ return imports;
89
+ }
90
+ /**
91
+ * Resolve an import path relative to the importing file
92
+ */
93
+ function resolveImportPath(fromFile, importSource, rootDir) {
94
+ if (!importSource.startsWith('.')) {
95
+ return null; // External package
96
+ }
97
+ const dir = path.dirname(fromFile);
98
+ let resolved = path.resolve(dir, importSource);
99
+ // Try common extensions
100
+ const extensions = ['.tsx', '.ts', '.jsx', '.js', '/index.tsx', '/index.ts', '/index.jsx', '/index.js'];
101
+ for (const ext of extensions) {
102
+ const tryPath = resolved + ext;
103
+ // We don't check existence here - just build the graph
104
+ if (!ext.includes('/')) {
105
+ return tryPath;
106
+ }
107
+ }
108
+ return resolved;
109
+ }
110
+ /**
111
+ * Detect circular dependencies in import graph using DFS
112
+ */
113
+ function detectCircularDependencies(graph) {
114
+ const cycles = [];
115
+ const visited = new Set();
116
+ const recursionStack = new Set();
117
+ const path = [];
118
+ function dfs(node) {
119
+ visited.add(node);
120
+ recursionStack.add(node);
121
+ path.push(node);
122
+ const neighbors = graph.get(node) || [];
123
+ for (const neighbor of neighbors) {
124
+ if (!visited.has(neighbor)) {
125
+ dfs(neighbor);
126
+ }
127
+ else if (recursionStack.has(neighbor)) {
128
+ // Found a cycle
129
+ const cycleStart = path.indexOf(neighbor);
130
+ if (cycleStart !== -1) {
131
+ const cycle = path.slice(cycleStart).concat(neighbor);
132
+ // Avoid duplicate cycles
133
+ const cycleKey = [...cycle].sort().join('|');
134
+ if (!cycles.some(c => [...c].sort().join('|') === cycleKey)) {
135
+ cycles.push(cycle);
136
+ }
137
+ }
138
+ }
139
+ }
140
+ path.pop();
141
+ recursionStack.delete(node);
142
+ }
143
+ for (const node of graph.keys()) {
144
+ if (!visited.has(node)) {
145
+ dfs(node);
146
+ }
147
+ }
148
+ return cycles;
149
+ }
150
+ /**
151
+ * Detect import-related code smells in a single file
152
+ */
153
+ function detectImportSmells(imports, filePath, sourceCode, config) {
154
+ const smells = [];
155
+ // Check for barrel file imports (importing from index files)
156
+ for (const imp of imports) {
157
+ if (imp.source.endsWith('/index') || imp.source === '.') {
158
+ smells.push({
159
+ type: 'barrel-file-import',
160
+ severity: 'info',
161
+ message: `Barrel file import from "${imp.source}" may impact tree-shaking`,
162
+ file: filePath,
163
+ line: imp.line,
164
+ column: 0,
165
+ suggestion: 'Import directly from the source file instead of barrel/index files for better build optimization.',
166
+ });
167
+ }
168
+ }
169
+ // Check for namespace imports (import * as)
170
+ for (const imp of imports) {
171
+ if (imp.isNamespace && !imp.source.startsWith('.')) {
172
+ smells.push({
173
+ type: 'namespace-import',
174
+ severity: 'info',
175
+ message: `Namespace import "* as" from "${imp.source}" may prevent tree-shaking`,
176
+ file: filePath,
177
+ line: imp.line,
178
+ column: 0,
179
+ suggestion: 'Import only the specific exports you need for better bundle size.',
180
+ });
181
+ }
182
+ }
183
+ // Check for too many imports from same source
184
+ const importCounts = new Map();
185
+ for (const imp of imports) {
186
+ const count = (importCounts.get(imp.source) || 0) + imp.specifiers.length;
187
+ importCounts.set(imp.source, count);
188
+ }
189
+ for (const [source, count] of importCounts) {
190
+ if (count > 10 && source.startsWith('.')) {
191
+ smells.push({
192
+ type: 'excessive-imports',
193
+ severity: 'warning',
194
+ message: `${count} imports from "${source}" suggests tight coupling`,
195
+ file: filePath,
196
+ line: imports.find(i => i.source === source)?.line || 1,
197
+ column: 0,
198
+ suggestion: 'Consider if this file has too many responsibilities or if modules should be reorganized.',
199
+ });
200
+ }
201
+ }
202
+ return smells;
203
+ }
204
+ /**
205
+ * Standalone detection function matching other detector signatures
206
+ */
207
+ export function detectImportIssues(component, filePath, sourceCode, config) {
208
+ const imports = extractImports(sourceCode, filePath);
209
+ return detectImportSmells(imports, filePath, sourceCode, config);
210
+ }
@@ -16,4 +16,8 @@ export { detectTypescriptIssues } from './typescript.js';
16
16
  export { detectDebugStatements } from './debug.js';
17
17
  export { detectSecurityIssues } from './security.js';
18
18
  export { detectAccessibilityIssues } from './accessibility.js';
19
+ export { detectComplexity, calculateComplexityMetrics } from './complexity.js';
20
+ export { detectMemoryLeaks } from './memoryLeak.js';
21
+ export { detectImportIssues, analyzeImports } from './imports.js';
22
+ export { detectUnusedCode } from './unusedCode.js';
19
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"}
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"}
@@ -18,3 +18,8 @@ export { detectTypescriptIssues } from './typescript.js';
18
18
  export { detectDebugStatements } from './debug.js';
19
19
  export { detectSecurityIssues } from './security.js';
20
20
  export { detectAccessibilityIssues } from './accessibility.js';
21
+ // Complexity, Memory Leaks, Imports
22
+ export { detectComplexity, calculateComplexityMetrics } from './complexity.js';
23
+ export { detectMemoryLeaks } from './memoryLeak.js';
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 potential memory leaks in React components
5
+ */
6
+ export declare function detectMemoryLeaks(component: ParsedComponent, filePath: string, sourceCode: string, config: DetectorConfig): CodeSmell[];
7
+ //# sourceMappingURL=memoryLeak.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"memoryLeak.d.ts","sourceRoot":"","sources":["../../src/detectors/memoryLeak.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AAE9D;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,cAAc,GACrB,SAAS,EAAE,CAgEb"}