react-code-smell-detector 1.2.0 → 1.3.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 (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +46 -0
  3. package/dist/analyzer.d.ts.map +1 -1
  4. package/dist/analyzer.js +18 -1
  5. package/dist/cli.js +93 -26
  6. package/dist/detectors/complexity.d.ts +17 -0
  7. package/dist/detectors/complexity.d.ts.map +1 -0
  8. package/dist/detectors/complexity.js +69 -0
  9. package/dist/detectors/imports.d.ts +22 -0
  10. package/dist/detectors/imports.d.ts.map +1 -0
  11. package/dist/detectors/imports.js +210 -0
  12. package/dist/detectors/index.d.ts +3 -0
  13. package/dist/detectors/index.d.ts.map +1 -1
  14. package/dist/detectors/index.js +4 -0
  15. package/dist/detectors/memoryLeak.d.ts +7 -0
  16. package/dist/detectors/memoryLeak.d.ts.map +1 -0
  17. package/dist/detectors/memoryLeak.js +111 -0
  18. package/dist/fixer.d.ts +23 -0
  19. package/dist/fixer.d.ts.map +1 -0
  20. package/dist/fixer.js +133 -0
  21. package/dist/git.d.ts +28 -0
  22. package/dist/git.d.ts.map +1 -0
  23. package/dist/git.js +117 -0
  24. package/dist/reporter.js +13 -0
  25. package/dist/types/index.d.ts +7 -1
  26. package/dist/types/index.d.ts.map +1 -1
  27. package/dist/types/index.js +9 -0
  28. package/dist/watcher.d.ts +16 -0
  29. package/dist/watcher.d.ts.map +1 -0
  30. package/dist/watcher.js +89 -0
  31. package/package.json +8 -2
  32. package/src/analyzer.ts +0 -324
  33. package/src/cli.ts +0 -159
  34. package/src/detectors/accessibility.ts +0 -212
  35. package/src/detectors/deadCode.ts +0 -163
  36. package/src/detectors/debug.ts +0 -103
  37. package/src/detectors/dependencyArray.ts +0 -176
  38. package/src/detectors/hooksRules.ts +0 -101
  39. package/src/detectors/index.ts +0 -20
  40. package/src/detectors/javascript.ts +0 -169
  41. package/src/detectors/largeComponent.ts +0 -63
  42. package/src/detectors/magicValues.ts +0 -114
  43. package/src/detectors/memoization.ts +0 -177
  44. package/src/detectors/missingKey.ts +0 -105
  45. package/src/detectors/nestedTernary.ts +0 -75
  46. package/src/detectors/nextjs.ts +0 -124
  47. package/src/detectors/nodejs.ts +0 -199
  48. package/src/detectors/propDrilling.ts +0 -103
  49. package/src/detectors/reactNative.ts +0 -154
  50. package/src/detectors/security.ts +0 -179
  51. package/src/detectors/typescript.ts +0 -151
  52. package/src/detectors/useEffect.ts +0 -117
  53. package/src/htmlReporter.ts +0 -464
  54. package/src/index.ts +0 -4
  55. package/src/parser/index.ts +0 -195
  56. package/src/reporter.ts +0 -291
  57. package/src/types/index.ts +0 -165
  58. package/tsconfig.json +0 -19
@@ -0,0 +1,111 @@
1
+ import * as t from '@babel/types';
2
+ /**
3
+ * Detect potential memory leaks in React components
4
+ */
5
+ export function detectMemoryLeaks(component, filePath, sourceCode, config) {
6
+ if (!config.checkMemoryLeaks)
7
+ return [];
8
+ const smells = [];
9
+ // Check for setInterval without cleanup
10
+ component.path.traverse({
11
+ CallExpression(path) {
12
+ const node = path.node;
13
+ const { callee } = node;
14
+ if (t.isIdentifier(callee) && callee.name === 'setInterval') {
15
+ // Check if result is stored
16
+ const parent = path.parentPath;
17
+ if (!parent || !t.isVariableDeclarator(parent.node)) {
18
+ const loc = node.loc;
19
+ smells.push({
20
+ type: 'memory-leak-timer',
21
+ severity: 'error',
22
+ message: `setInterval without storing ID for cleanup in "${component.name}"`,
23
+ file: filePath,
24
+ line: loc?.start.line || 0,
25
+ column: loc?.start.column || 0,
26
+ suggestion: 'Store the interval ID and call clearInterval in cleanup.',
27
+ });
28
+ }
29
+ }
30
+ },
31
+ });
32
+ // Check useEffect hooks for cleanup issues
33
+ for (const effect of component.hooks.useEffect) {
34
+ const hasCleanup = checkEffectHasCleanup(effect);
35
+ const hasSubscription = checkEffectHasSubscription(effect, component);
36
+ const hasEventListener = checkEffectHasEventListener(effect, component);
37
+ if (hasEventListener && !hasCleanup) {
38
+ const loc = effect.loc;
39
+ smells.push({
40
+ type: 'memory-leak-event-listener',
41
+ severity: 'error',
42
+ message: `useEffect adds event listener without cleanup in "${component.name}"`,
43
+ file: filePath,
44
+ line: loc?.start.line || 0,
45
+ column: 0,
46
+ suggestion: 'Return a cleanup function that calls removeEventListener.',
47
+ });
48
+ }
49
+ if (hasSubscription && !hasCleanup) {
50
+ const loc = effect.loc;
51
+ smells.push({
52
+ type: 'memory-leak-subscription',
53
+ severity: 'error',
54
+ message: `useEffect creates subscription without cleanup in "${component.name}"`,
55
+ file: filePath,
56
+ line: loc?.start.line || 0,
57
+ column: 0,
58
+ suggestion: 'Return a cleanup function that unsubscribes.',
59
+ });
60
+ }
61
+ }
62
+ return smells;
63
+ }
64
+ function checkEffectHasCleanup(effect) {
65
+ const args = effect.arguments;
66
+ if (args.length === 0)
67
+ return false;
68
+ const callback = args[0];
69
+ if (!t.isArrowFunctionExpression(callback) && !t.isFunctionExpression(callback)) {
70
+ return false;
71
+ }
72
+ // Check if callback body has a return statement
73
+ if (t.isBlockStatement(callback.body)) {
74
+ for (const stmt of callback.body.body) {
75
+ if (t.isReturnStatement(stmt) && stmt.argument) {
76
+ return true;
77
+ }
78
+ }
79
+ }
80
+ return false;
81
+ }
82
+ function checkEffectHasEventListener(effect, component) {
83
+ let hasListener = false;
84
+ const args = effect.arguments;
85
+ if (args.length === 0)
86
+ return false;
87
+ const callback = args[0];
88
+ if (!t.isArrowFunctionExpression(callback) && !t.isFunctionExpression(callback)) {
89
+ return false;
90
+ }
91
+ if (t.isBlockStatement(callback.body)) {
92
+ const sourceCode = JSON.stringify(callback.body);
93
+ hasListener = sourceCode.includes('addEventListener');
94
+ }
95
+ return hasListener;
96
+ }
97
+ function checkEffectHasSubscription(effect, component) {
98
+ let hasSubscription = false;
99
+ const args = effect.arguments;
100
+ if (args.length === 0)
101
+ return false;
102
+ const callback = args[0];
103
+ if (!t.isArrowFunctionExpression(callback) && !t.isFunctionExpression(callback)) {
104
+ return false;
105
+ }
106
+ if (t.isBlockStatement(callback.body)) {
107
+ const sourceCode = JSON.stringify(callback.body);
108
+ hasSubscription = sourceCode.includes('subscribe') || sourceCode.includes('addListener');
109
+ }
110
+ return hasSubscription;
111
+ }
@@ -0,0 +1,23 @@
1
+ import { CodeSmell, SmellType } from './types/index.js';
2
+ export interface FixResult {
3
+ file: string;
4
+ fixedSmells: CodeSmell[];
5
+ skippedSmells: CodeSmell[];
6
+ }
7
+ /**
8
+ * Check if a smell type can be auto-fixed
9
+ */
10
+ export declare function isFixable(smell: CodeSmell): boolean;
11
+ /**
12
+ * Apply fixes to a file based on detected smells
13
+ */
14
+ export declare function fixFile(filePath: string, smells: CodeSmell[]): Promise<FixResult>;
15
+ /**
16
+ * Get list of fixable smell types
17
+ */
18
+ export declare function getFixableTypes(): SmellType[];
19
+ /**
20
+ * Describe what fix will be applied for a smell type
21
+ */
22
+ export declare function describeFixAction(type: SmellType): string;
23
+ //# sourceMappingURL=fixer.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fixer.d.ts","sourceRoot":"","sources":["../src/fixer.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAExD,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,SAAS,EAAE,CAAC;IACzB,aAAa,EAAE,SAAS,EAAE,CAAC;CAC5B;AAUD;;GAEG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,SAAS,GAAG,OAAO,CAEnD;AAED;;GAEG;AACH,wBAAsB,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,SAAS,EAAE,GAAG,OAAO,CAAC,SAAS,CAAC,CAuCvF;AAyED;;GAEG;AACH,wBAAgB,eAAe,IAAI,SAAS,EAAE,CAE7C;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,SAAS,GAAG,MAAM,CAazD"}
package/dist/fixer.js ADDED
@@ -0,0 +1,133 @@
1
+ import fs from 'fs/promises';
2
+ // Smell types that can be auto-fixed
3
+ const FIXABLE_TYPES = [
4
+ 'debug-statement',
5
+ 'js-var-usage',
6
+ 'js-loose-equality',
7
+ 'a11y-missing-alt',
8
+ ];
9
+ /**
10
+ * Check if a smell type can be auto-fixed
11
+ */
12
+ export function isFixable(smell) {
13
+ return FIXABLE_TYPES.includes(smell.type);
14
+ }
15
+ /**
16
+ * Apply fixes to a file based on detected smells
17
+ */
18
+ export async function fixFile(filePath, smells) {
19
+ const fixableSmells = smells.filter(isFixable);
20
+ const skippedSmells = smells.filter(s => !isFixable(s));
21
+ if (fixableSmells.length === 0) {
22
+ return { file: filePath, fixedSmells: [], skippedSmells };
23
+ }
24
+ let content = await fs.readFile(filePath, 'utf-8');
25
+ const lines = content.split('\n');
26
+ const fixedSmells = [];
27
+ // Sort by line descending to fix from bottom up (preserves line numbers)
28
+ const sortedSmells = [...fixableSmells].sort((a, b) => b.line - a.line);
29
+ for (const smell of sortedSmells) {
30
+ const lineIndex = smell.line - 1;
31
+ if (lineIndex < 0 || lineIndex >= lines.length)
32
+ continue;
33
+ const originalLine = lines[lineIndex];
34
+ const fixedLine = applyFix(smell, originalLine);
35
+ if (fixedLine !== originalLine) {
36
+ if (fixedLine === null) {
37
+ // Remove the line entirely
38
+ lines.splice(lineIndex, 1);
39
+ }
40
+ else {
41
+ lines[lineIndex] = fixedLine;
42
+ }
43
+ fixedSmells.push(smell);
44
+ }
45
+ }
46
+ // Write back if any fixes were applied
47
+ if (fixedSmells.length > 0) {
48
+ await fs.writeFile(filePath, lines.join('\n'), 'utf-8');
49
+ }
50
+ return { file: filePath, fixedSmells, skippedSmells };
51
+ }
52
+ /**
53
+ * Apply a specific fix to a line of code
54
+ * Returns the fixed line, or null to remove the line
55
+ */
56
+ function applyFix(smell, line) {
57
+ switch (smell.type) {
58
+ case 'debug-statement':
59
+ return fixDebugStatement(line);
60
+ case 'js-var-usage':
61
+ return fixVarUsage(line);
62
+ case 'js-loose-equality':
63
+ return fixLooseEquality(line);
64
+ case 'a11y-missing-alt':
65
+ return fixMissingAlt(line);
66
+ default:
67
+ return line;
68
+ }
69
+ }
70
+ /**
71
+ * Remove console.log, console.debug, debugger statements
72
+ */
73
+ function fixDebugStatement(line) {
74
+ const trimmed = line.trim();
75
+ // If line is just a console statement or debugger, remove it
76
+ if (/^console\.(log|debug|info|warn|error|trace|dir)\s*\(.*\);?\s*$/.test(trimmed)) {
77
+ return null;
78
+ }
79
+ if (/^debugger;?\s*$/.test(trimmed)) {
80
+ return null;
81
+ }
82
+ // If console is part of a larger expression, comment it out
83
+ if (/console\.(log|debug|info|warn|error|trace|dir)\s*\(/.test(line)) {
84
+ return line.replace(/console\.(log|debug|info|warn|error|trace|dir)\s*\([^)]*\);?/g, '/* $& */');
85
+ }
86
+ return line;
87
+ }
88
+ /**
89
+ * Replace var with let or const
90
+ */
91
+ function fixVarUsage(line) {
92
+ // Simple heuristic: if reassigned later, use let; otherwise const
93
+ // For auto-fix, default to let (safer)
94
+ return line.replace(/\bvar\s+/, 'let ');
95
+ }
96
+ /**
97
+ * Replace == with === and != with !==
98
+ */
99
+ function fixLooseEquality(line) {
100
+ return line
101
+ .replace(/([^=!])={2}([^=])/g, '$1===$2')
102
+ .replace(/!={1}([^=])/g, '!==$1');
103
+ }
104
+ /**
105
+ * Add alt="" to img tags missing alt attribute
106
+ */
107
+ function fixMissingAlt(line) {
108
+ // Add alt="" to <img> tags without alt
109
+ return line.replace(/<img\s+(?![^>]*\balt\b)([^>]*?)(\/?>)/gi, '<img $1alt="" $2');
110
+ }
111
+ /**
112
+ * Get list of fixable smell types
113
+ */
114
+ export function getFixableTypes() {
115
+ return [...FIXABLE_TYPES];
116
+ }
117
+ /**
118
+ * Describe what fix will be applied for a smell type
119
+ */
120
+ export function describeFixAction(type) {
121
+ switch (type) {
122
+ case 'debug-statement':
123
+ return 'Remove console.log/debugger statements';
124
+ case 'js-var-usage':
125
+ return 'Replace var with let';
126
+ case 'js-loose-equality':
127
+ return 'Replace == with === and != with !==';
128
+ case 'a11y-missing-alt':
129
+ return 'Add alt="" to images';
130
+ default:
131
+ return 'Not auto-fixable';
132
+ }
133
+ }
package/dist/git.d.ts ADDED
@@ -0,0 +1,28 @@
1
+ export interface GitInfo {
2
+ isGitRepo: boolean;
3
+ currentBranch?: string;
4
+ changedFiles: string[];
5
+ stagedFiles: string[];
6
+ untrackedFiles: string[];
7
+ }
8
+ /**
9
+ * Get git information for a directory
10
+ */
11
+ export declare function getGitInfo(rootDir: string): GitInfo;
12
+ /**
13
+ * Get files changed since a specific commit or branch
14
+ */
15
+ export declare function getFilesSince(rootDir: string, ref: string): string[];
16
+ /**
17
+ * Get files changed in the last N commits
18
+ */
19
+ export declare function getFilesFromLastCommits(rootDir: string, count?: number): string[];
20
+ /**
21
+ * Get all modified files (changed + staged + untracked)
22
+ */
23
+ export declare function getAllModifiedFiles(rootDir: string): string[];
24
+ /**
25
+ * Filter file list to only include React/JS/TS files
26
+ */
27
+ export declare function filterReactFiles(files: string[]): string[];
28
+ //# sourceMappingURL=git.d.ts.map
@@ -0,0 +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"}
package/dist/git.js ADDED
@@ -0,0 +1,117 @@
1
+ import { execSync } from 'child_process';
2
+ import path from 'path';
3
+ /**
4
+ * Get git information for a directory
5
+ */
6
+ export function getGitInfo(rootDir) {
7
+ try {
8
+ // Check if it's a git repo
9
+ execSync('git rev-parse --is-inside-work-tree', {
10
+ cwd: rootDir,
11
+ stdio: 'pipe',
12
+ });
13
+ const currentBranch = execSync('git branch --show-current', {
14
+ cwd: rootDir,
15
+ encoding: 'utf-8',
16
+ }).trim();
17
+ // Get changed files (modified but not staged)
18
+ const changedOutput = execSync('git diff --name-only', {
19
+ cwd: rootDir,
20
+ encoding: 'utf-8',
21
+ });
22
+ const changedFiles = changedOutput
23
+ .split('\n')
24
+ .filter(f => f.trim())
25
+ .map(f => path.resolve(rootDir, f));
26
+ // Get staged files
27
+ const stagedOutput = execSync('git diff --cached --name-only', {
28
+ cwd: rootDir,
29
+ encoding: 'utf-8',
30
+ });
31
+ const stagedFiles = stagedOutput
32
+ .split('\n')
33
+ .filter(f => f.trim())
34
+ .map(f => path.resolve(rootDir, f));
35
+ // Get untracked files
36
+ const untrackedOutput = execSync('git ls-files --others --exclude-standard', {
37
+ cwd: rootDir,
38
+ encoding: 'utf-8',
39
+ });
40
+ const untrackedFiles = untrackedOutput
41
+ .split('\n')
42
+ .filter(f => f.trim())
43
+ .map(f => path.resolve(rootDir, f));
44
+ return {
45
+ isGitRepo: true,
46
+ currentBranch,
47
+ changedFiles,
48
+ stagedFiles,
49
+ untrackedFiles,
50
+ };
51
+ }
52
+ catch {
53
+ return {
54
+ isGitRepo: false,
55
+ changedFiles: [],
56
+ stagedFiles: [],
57
+ untrackedFiles: [],
58
+ };
59
+ }
60
+ }
61
+ /**
62
+ * Get files changed since a specific commit or branch
63
+ */
64
+ export function getFilesSince(rootDir, ref) {
65
+ try {
66
+ const output = execSync(`git diff --name-only ${ref}`, {
67
+ cwd: rootDir,
68
+ encoding: 'utf-8',
69
+ });
70
+ return output
71
+ .split('\n')
72
+ .filter(f => f.trim())
73
+ .map(f => path.resolve(rootDir, f));
74
+ }
75
+ catch {
76
+ return [];
77
+ }
78
+ }
79
+ /**
80
+ * Get files changed in the last N commits
81
+ */
82
+ export function getFilesFromLastCommits(rootDir, count = 1) {
83
+ try {
84
+ const output = execSync(`git diff --name-only HEAD~${count}`, {
85
+ cwd: rootDir,
86
+ encoding: 'utf-8',
87
+ });
88
+ return output
89
+ .split('\n')
90
+ .filter(f => f.trim())
91
+ .map(f => path.resolve(rootDir, f));
92
+ }
93
+ catch {
94
+ return [];
95
+ }
96
+ }
97
+ /**
98
+ * Get all modified files (changed + staged + untracked)
99
+ */
100
+ export function getAllModifiedFiles(rootDir) {
101
+ const info = getGitInfo(rootDir);
102
+ if (!info.isGitRepo)
103
+ return [];
104
+ const allFiles = new Set([
105
+ ...info.changedFiles,
106
+ ...info.stagedFiles,
107
+ ...info.untrackedFiles,
108
+ ]);
109
+ return Array.from(allFiles);
110
+ }
111
+ /**
112
+ * Filter file list to only include React/JS/TS files
113
+ */
114
+ export function filterReactFiles(files) {
115
+ const extensions = ['.tsx', '.jsx', '.ts', '.js'];
116
+ return files.filter(f => extensions.some(ext => f.endsWith(ext)));
117
+ }
package/dist/reporter.js CHANGED
@@ -255,6 +255,19 @@ function formatSmellType(type) {
255
255
  'a11y-interactive-role': '♿ Interactive Role',
256
256
  'a11y-keyboard': '♿ Keyboard Handler',
257
257
  'a11y-semantic': '♿ Semantic HTML',
258
+ // Complexity
259
+ 'high-cyclomatic-complexity': '🧮 High Cyclomatic Complexity',
260
+ 'high-cognitive-complexity': '🧠 High Cognitive Complexity',
261
+ // Memory Leaks
262
+ 'memory-leak-event-listener': 'šŸ’§ Event Listener Leak',
263
+ 'memory-leak-subscription': 'šŸ’§ Subscription Leak',
264
+ 'memory-leak-timer': 'šŸ’§ Timer Leak',
265
+ 'memory-leak-async': 'šŸ’§ Async Leak Risk',
266
+ // Import Issues
267
+ 'circular-dependency': 'šŸ”„ Circular Dependency',
268
+ 'barrel-file-import': 'šŸ“¦ Barrel File Import',
269
+ 'namespace-import': 'šŸ“¦ Namespace Import',
270
+ 'excessive-imports': 'šŸ“¦ Excessive Imports',
258
271
  };
259
272
  return labels[type] || type;
260
273
  }
@@ -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';
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';
3
3
  export interface CodeSmell {
4
4
  type: SmellType;
5
5
  severity: SmellSeverity;
@@ -75,6 +75,12 @@ export interface DetectorConfig {
75
75
  checkDebugStatements: boolean;
76
76
  checkSecurity: boolean;
77
77
  checkAccessibility: boolean;
78
+ checkComplexity: boolean;
79
+ maxCyclomaticComplexity: number;
80
+ maxCognitiveComplexity: number;
81
+ maxNestingDepth: number;
82
+ checkMemoryLeaks: boolean;
83
+ checkImports: boolean;
78
84
  }
79
85
  export declare const DEFAULT_CONFIG: DetectorConfig;
80
86
  //# 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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"}
@@ -22,4 +22,13 @@ export const DEFAULT_CONFIG = {
22
22
  checkDebugStatements: true,
23
23
  checkSecurity: true,
24
24
  checkAccessibility: true,
25
+ // Complexity
26
+ checkComplexity: true,
27
+ maxCyclomaticComplexity: 10,
28
+ maxCognitiveComplexity: 15,
29
+ maxNestingDepth: 4,
30
+ // Memory leaks
31
+ checkMemoryLeaks: true,
32
+ // Import analysis
33
+ checkImports: true,
25
34
  };
@@ -0,0 +1,16 @@
1
+ import { DetectorConfig } from './types/index.js';
2
+ export interface WatchOptions {
3
+ rootDir: string;
4
+ include: string[];
5
+ exclude: string[];
6
+ config: DetectorConfig;
7
+ showSnippets: boolean;
8
+ onAnalysis?: (fileCount: number, issueCount: number) => void;
9
+ }
10
+ /**
11
+ * Start watching a project for changes and re-analyze on each change
12
+ */
13
+ export declare function startWatch(options: WatchOptions): {
14
+ close: () => void;
15
+ };
16
+ //# sourceMappingURL=watcher.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"watcher.d.ts","sourceRoot":"","sources":["../src/watcher.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,cAAc,EAAkB,MAAM,kBAAkB,CAAC;AAElE,MAAM,WAAW,YAAY;IAC3B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,MAAM,EAAE,cAAc,CAAC;IACvB,YAAY,EAAE,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,KAAK,IAAI,CAAC;CAC9D;AAED;;GAEG;AACH,wBAAgB,UAAU,CAAC,OAAO,EAAE,YAAY,GAAG;IAAE,KAAK,EAAE,MAAM,IAAI,CAAA;CAAE,CAgGvE"}
@@ -0,0 +1,89 @@
1
+ import chokidar from 'chokidar';
2
+ import path from 'path';
3
+ import chalk from 'chalk';
4
+ import { analyzeProject } from './analyzer.js';
5
+ import { reportResults } from './reporter.js';
6
+ /**
7
+ * Start watching a project for changes and re-analyze on each change
8
+ */
9
+ export function startWatch(options) {
10
+ const { rootDir, include = ['**/*.tsx', '**/*.jsx'], exclude = ['**/node_modules/**', '**/dist/**', '**/build/**'], config, showSnippets, onAnalysis, } = options;
11
+ console.log(chalk.cyan('\nšŸ‘€ Watch mode started'));
12
+ console.log(chalk.dim(` Watching: ${rootDir}`));
13
+ console.log(chalk.dim(` Patterns: ${include.join(', ')}`));
14
+ console.log(chalk.dim(' Press Ctrl+C to stop\n'));
15
+ // Debounce timer
16
+ let debounceTimer = null;
17
+ let isAnalyzing = false;
18
+ const runAnalysis = async () => {
19
+ if (isAnalyzing)
20
+ return;
21
+ isAnalyzing = true;
22
+ console.log(chalk.dim('\n─────────────────────────────────────────'));
23
+ console.log(chalk.cyan('šŸ”„ Re-analyzing...'));
24
+ try {
25
+ const result = await analyzeProject({
26
+ rootDir,
27
+ include,
28
+ exclude,
29
+ config,
30
+ });
31
+ // Clear console and show results
32
+ console.clear();
33
+ console.log(chalk.cyan('\nšŸ‘€ Watch mode - Last update: ' + new Date().toLocaleTimeString()));
34
+ console.log(chalk.dim(' Press Ctrl+C to stop\n'));
35
+ const output = reportResults(result, {
36
+ format: 'console',
37
+ showCodeSnippets: showSnippets,
38
+ rootDir,
39
+ });
40
+ console.log(output);
41
+ if (onAnalysis) {
42
+ onAnalysis(result.summary.totalFiles, result.summary.totalSmells);
43
+ }
44
+ }
45
+ catch (error) {
46
+ console.error(chalk.red('Analysis error:'), error.message);
47
+ }
48
+ finally {
49
+ isAnalyzing = false;
50
+ }
51
+ };
52
+ // Debounced analysis trigger
53
+ const triggerAnalysis = () => {
54
+ if (debounceTimer) {
55
+ clearTimeout(debounceTimer);
56
+ }
57
+ debounceTimer = setTimeout(runAnalysis, 300);
58
+ };
59
+ // Set up file watcher
60
+ const watchPatterns = include.map(p => path.join(rootDir, p));
61
+ const watcher = chokidar.watch(watchPatterns, {
62
+ ignored: exclude.map(p => {
63
+ if (p.startsWith('**/'))
64
+ return p;
65
+ return path.join(rootDir, p);
66
+ }),
67
+ persistent: true,
68
+ ignoreInitial: false,
69
+ awaitWriteFinish: {
70
+ stabilityThreshold: 100,
71
+ pollInterval: 50,
72
+ },
73
+ });
74
+ watcher
75
+ .on('add', () => triggerAnalysis())
76
+ .on('change', () => triggerAnalysis())
77
+ .on('unlink', () => triggerAnalysis())
78
+ .on('error', error => console.error(chalk.red('Watcher error:'), error));
79
+ // Initial analysis
80
+ triggerAnalysis();
81
+ return {
82
+ close: () => {
83
+ if (debounceTimer)
84
+ clearTimeout(debounceTimer);
85
+ watcher.close();
86
+ console.log(chalk.yellow('\nšŸ‘‹ Watch mode stopped'));
87
+ },
88
+ };
89
+ }
package/package.json CHANGED
@@ -1,12 +1,17 @@
1
1
  {
2
2
  "name": "react-code-smell-detector",
3
- "version": "1.2.0",
4
- "description": "Detect code smells in React projects - useEffect overuse, prop drilling, large components, security issues, accessibility, and more",
3
+ "version": "1.3.1",
4
+ "description": "Detect code smells in React projects - useEffect overuse, prop drilling, large components, security issues, accessibility, memory leaks, and more",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
7
7
  "react-smell": "dist/cli.js"
8
8
  },
9
9
  "type": "module",
10
+ "files": [
11
+ "dist",
12
+ "README.md",
13
+ "LICENSE"
14
+ ],
10
15
  "scripts": {
11
16
  "build": "tsc",
12
17
  "dev": "tsc --watch",
@@ -29,6 +34,7 @@
29
34
  "@babel/traverse": "^7.23.0",
30
35
  "@babel/types": "^7.23.0",
31
36
  "chalk": "^5.3.0",
37
+ "chokidar": "^5.0.0",
32
38
  "commander": "^11.1.0",
33
39
  "fast-glob": "^3.3.2",
34
40
  "ora": "^8.0.1"