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
@@ -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,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
+ }
@@ -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,31 @@
1
+ export interface GitInfo {
2
+ isGitRepo: boolean;
3
+ currentBranch?: string;
4
+ branch?: string;
5
+ currentCommit?: string;
6
+ authorName?: string;
7
+ changedFiles: string[];
8
+ stagedFiles: string[];
9
+ untrackedFiles: string[];
10
+ }
11
+ /**
12
+ * Get git information for a directory
13
+ */
14
+ export declare function getGitInfo(rootDir: string): GitInfo;
15
+ /**
16
+ * Get files changed since a specific commit or branch
17
+ */
18
+ export declare function getFilesSince(rootDir: string, ref: string): string[];
19
+ /**
20
+ * Get files changed in the last N commits
21
+ */
22
+ export declare function getFilesFromLastCommits(rootDir: string, count?: number): string[];
23
+ /**
24
+ * Get all modified files (changed + staged + untracked)
25
+ */
26
+ export declare function getAllModifiedFiles(rootDir: string): string[];
27
+ /**
28
+ * Filter file list to only include React/JS/TS files
29
+ */
30
+ export declare function filterReactFiles(files: string[]): string[];
31
+ //# 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,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 ADDED
@@ -0,0 +1,137 @@
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 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();
27
+ // Get changed files (modified but not staged)
28
+ const changedOutput = execSync('git diff --name-only', {
29
+ cwd: rootDir,
30
+ encoding: 'utf-8',
31
+ });
32
+ const changedFiles = changedOutput
33
+ .split('\n')
34
+ .filter(f => f.trim())
35
+ .map(f => path.resolve(rootDir, f));
36
+ // Get staged files
37
+ const stagedOutput = execSync('git diff --cached --name-only', {
38
+ cwd: rootDir,
39
+ encoding: 'utf-8',
40
+ });
41
+ const stagedFiles = stagedOutput
42
+ .split('\n')
43
+ .filter(f => f.trim())
44
+ .map(f => path.resolve(rootDir, f));
45
+ // Get untracked files
46
+ const untrackedOutput = execSync('git ls-files --others --exclude-standard', {
47
+ cwd: rootDir,
48
+ encoding: 'utf-8',
49
+ });
50
+ const untrackedFiles = untrackedOutput
51
+ .split('\n')
52
+ .filter(f => f.trim())
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
+ };
64
+ return {
65
+ isGitRepo: true,
66
+ currentBranch,
67
+ changedFiles,
68
+ stagedFiles,
69
+ untrackedFiles,
70
+ };
71
+ }
72
+ catch {
73
+ return {
74
+ isGitRepo: false,
75
+ changedFiles: [],
76
+ stagedFiles: [],
77
+ untrackedFiles: [],
78
+ };
79
+ }
80
+ }
81
+ /**
82
+ * Get files changed since a specific commit or branch
83
+ */
84
+ export function getFilesSince(rootDir, ref) {
85
+ try {
86
+ const output = execSync(`git diff --name-only ${ref}`, {
87
+ cwd: rootDir,
88
+ encoding: 'utf-8',
89
+ });
90
+ return output
91
+ .split('\n')
92
+ .filter(f => f.trim())
93
+ .map(f => path.resolve(rootDir, f));
94
+ }
95
+ catch {
96
+ return [];
97
+ }
98
+ }
99
+ /**
100
+ * Get files changed in the last N commits
101
+ */
102
+ export function getFilesFromLastCommits(rootDir, count = 1) {
103
+ try {
104
+ const output = execSync(`git diff --name-only HEAD~${count}`, {
105
+ cwd: rootDir,
106
+ encoding: 'utf-8',
107
+ });
108
+ return output
109
+ .split('\n')
110
+ .filter(f => f.trim())
111
+ .map(f => path.resolve(rootDir, f));
112
+ }
113
+ catch {
114
+ return [];
115
+ }
116
+ }
117
+ /**
118
+ * Get all modified files (changed + staged + untracked)
119
+ */
120
+ export function getAllModifiedFiles(rootDir) {
121
+ const info = getGitInfo(rootDir);
122
+ if (!info.isGitRepo)
123
+ return [];
124
+ const allFiles = new Set([
125
+ ...info.changedFiles,
126
+ ...info.stagedFiles,
127
+ ...info.untrackedFiles,
128
+ ]);
129
+ return Array.from(allFiles);
130
+ }
131
+ /**
132
+ * Filter file list to only include React/JS/TS files
133
+ */
134
+ export function filterReactFiles(files) {
135
+ const extensions = ['.tsx', '.jsx', '.ts', '.js'];
136
+ return files.filter(f => extensions.some(ext => f.endsWith(ext)));
137
+ }
package/dist/reporter.js CHANGED
@@ -255,6 +255,22 @@ 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',
271
+ // Unused Code
272
+ 'unused-export': '🗑️ Unused Export',
273
+ 'dead-import': '🗑️ Dead Import',
258
274
  };
259
275
  return labels[type] || type;
260
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';
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;
@@ -75,6 +75,18 @@ 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;
84
+ checkUnusedCode: boolean;
85
+ baselineEnabled: boolean;
86
+ baselineThreshold?: number;
87
+ webhookUrl?: string;
88
+ webhookType?: 'slack' | 'discord' | 'generic';
89
+ webhookThreshold?: number;
78
90
  }
79
91
  export declare const DEFAULT_CONFIG: DetectorConfig;
80
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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"}
@@ -22,4 +22,22 @@ 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,
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,
25
43
  };
@@ -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"}