react-code-smell-detector 1.1.1 → 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 (66) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +115 -11
  3. package/dist/analyzer.d.ts.map +1 -1
  4. package/dist/analyzer.js +65 -2
  5. package/dist/cli.js +134 -33
  6. package/dist/detectors/accessibility.d.ts +12 -0
  7. package/dist/detectors/accessibility.d.ts.map +1 -0
  8. package/dist/detectors/accessibility.js +191 -0
  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/debug.d.ts +10 -0
  13. package/dist/detectors/debug.d.ts.map +1 -0
  14. package/dist/detectors/debug.js +87 -0
  15. package/dist/detectors/imports.d.ts +22 -0
  16. package/dist/detectors/imports.d.ts.map +1 -0
  17. package/dist/detectors/imports.js +210 -0
  18. package/dist/detectors/index.d.ts +6 -0
  19. package/dist/detectors/index.d.ts.map +1 -1
  20. package/dist/detectors/index.js +8 -0
  21. package/dist/detectors/memoryLeak.d.ts +7 -0
  22. package/dist/detectors/memoryLeak.d.ts.map +1 -0
  23. package/dist/detectors/memoryLeak.js +111 -0
  24. package/dist/detectors/security.d.ts +12 -0
  25. package/dist/detectors/security.d.ts.map +1 -0
  26. package/dist/detectors/security.js +161 -0
  27. package/dist/fixer.d.ts +23 -0
  28. package/dist/fixer.d.ts.map +1 -0
  29. package/dist/fixer.js +133 -0
  30. package/dist/git.d.ts +28 -0
  31. package/dist/git.d.ts.map +1 -0
  32. package/dist/git.js +117 -0
  33. package/dist/htmlReporter.d.ts +6 -0
  34. package/dist/htmlReporter.d.ts.map +1 -0
  35. package/dist/htmlReporter.js +453 -0
  36. package/dist/reporter.js +26 -0
  37. package/dist/types/index.d.ts +10 -1
  38. package/dist/types/index.d.ts.map +1 -1
  39. package/dist/types/index.js +13 -0
  40. package/dist/watcher.d.ts +16 -0
  41. package/dist/watcher.d.ts.map +1 -0
  42. package/dist/watcher.js +89 -0
  43. package/package.json +8 -2
  44. package/src/analyzer.ts +0 -269
  45. package/src/cli.ts +0 -125
  46. package/src/detectors/deadCode.ts +0 -163
  47. package/src/detectors/dependencyArray.ts +0 -176
  48. package/src/detectors/hooksRules.ts +0 -101
  49. package/src/detectors/index.ts +0 -16
  50. package/src/detectors/javascript.ts +0 -169
  51. package/src/detectors/largeComponent.ts +0 -63
  52. package/src/detectors/magicValues.ts +0 -114
  53. package/src/detectors/memoization.ts +0 -177
  54. package/src/detectors/missingKey.ts +0 -105
  55. package/src/detectors/nestedTernary.ts +0 -75
  56. package/src/detectors/nextjs.ts +0 -124
  57. package/src/detectors/nodejs.ts +0 -199
  58. package/src/detectors/propDrilling.ts +0 -103
  59. package/src/detectors/reactNative.ts +0 -154
  60. package/src/detectors/typescript.ts +0 -151
  61. package/src/detectors/useEffect.ts +0 -117
  62. package/src/index.ts +0 -4
  63. package/src/parser/index.ts +0 -195
  64. package/src/reporter.ts +0 -278
  65. package/src/types/index.ts +0 -144
  66. package/tsconfig.json +0 -19
@@ -0,0 +1,191 @@
1
+ import * as t from '@babel/types';
2
+ import { getCodeSnippet } from '../parser/index.js';
3
+ import { DEFAULT_CONFIG } from '../types/index.js';
4
+ /**
5
+ * Detects accessibility (a11y) issues:
6
+ * - Images without alt text
7
+ * - Form inputs without labels
8
+ * - Missing ARIA attributes on interactive elements
9
+ * - Click handlers without keyboard support
10
+ * - Improper heading hierarchy
11
+ */
12
+ export function detectAccessibilityIssues(component, filePath, sourceCode, config = DEFAULT_CONFIG) {
13
+ if (!config.checkAccessibility)
14
+ return [];
15
+ const smells = [];
16
+ component.path.traverse({
17
+ JSXOpeningElement(path) {
18
+ if (!t.isJSXIdentifier(path.node.name))
19
+ return;
20
+ const elementName = path.node.name.name;
21
+ const attributes = path.node.attributes;
22
+ const loc = path.node.loc;
23
+ const line = loc?.start.line || 0;
24
+ // Helper to check if attribute exists
25
+ const hasAttr = (name) => {
26
+ return attributes.some(attr => {
27
+ if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) {
28
+ return attr.name.name === name;
29
+ }
30
+ // Handle spread attributes - assume they might contain the attribute
31
+ return t.isJSXSpreadAttribute(attr);
32
+ });
33
+ };
34
+ // Helper to get attribute value
35
+ const getAttrValue = (name) => {
36
+ for (const attr of attributes) {
37
+ if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && attr.name.name === name) {
38
+ if (t.isStringLiteral(attr.value)) {
39
+ return attr.value.value;
40
+ }
41
+ }
42
+ }
43
+ return null;
44
+ };
45
+ // Check for images without alt text
46
+ if (elementName === 'img') {
47
+ if (!hasAttr('alt')) {
48
+ smells.push({
49
+ type: 'a11y-missing-alt',
50
+ severity: 'error',
51
+ message: `<img> missing alt attribute in "${component.name}"`,
52
+ file: filePath,
53
+ line,
54
+ column: loc?.start.column || 0,
55
+ suggestion: 'Add alt="description" for content images, or alt="" for decorative images',
56
+ codeSnippet: getCodeSnippet(sourceCode, line),
57
+ });
58
+ }
59
+ else {
60
+ const altValue = getAttrValue('alt');
61
+ if (altValue !== null && altValue.toLowerCase().includes('image')) {
62
+ smells.push({
63
+ type: 'a11y-missing-alt',
64
+ severity: 'info',
65
+ message: `<img> alt text shouldn't contain "image" - it's redundant`,
66
+ file: filePath,
67
+ line,
68
+ column: loc?.start.column || 0,
69
+ suggestion: 'Describe what the image shows, not that it is an image',
70
+ codeSnippet: getCodeSnippet(sourceCode, line),
71
+ });
72
+ }
73
+ }
74
+ }
75
+ // Check for form inputs without associated labels
76
+ if (elementName === 'input' || elementName === 'textarea' || elementName === 'select') {
77
+ const inputType = getAttrValue('type') || 'text';
78
+ // Skip hidden inputs
79
+ if (inputType === 'hidden')
80
+ return;
81
+ const hasLabel = hasAttr('aria-label') ||
82
+ hasAttr('aria-labelledby') ||
83
+ hasAttr('id'); // Assume id might be linked to a label
84
+ if (!hasLabel) {
85
+ smells.push({
86
+ type: 'a11y-missing-label',
87
+ severity: 'warning',
88
+ message: `<${elementName}> without accessible label in "${component.name}"`,
89
+ file: filePath,
90
+ line,
91
+ column: loc?.start.column || 0,
92
+ suggestion: 'Add aria-label, aria-labelledby, or associate with a <label> element',
93
+ codeSnippet: getCodeSnippet(sourceCode, line),
94
+ });
95
+ }
96
+ }
97
+ // Check for interactive divs/spans without proper role and keyboard support
98
+ if (elementName === 'div' || elementName === 'span') {
99
+ const hasOnClick = hasAttr('onClick');
100
+ const hasRole = hasAttr('role');
101
+ const hasTabIndex = hasAttr('tabIndex');
102
+ const hasKeyboardHandler = hasAttr('onKeyDown') || hasAttr('onKeyUp') || hasAttr('onKeyPress');
103
+ if (hasOnClick) {
104
+ if (!hasRole) {
105
+ smells.push({
106
+ type: 'a11y-interactive-role',
107
+ severity: 'warning',
108
+ message: `Clickable <${elementName}> without role attribute in "${component.name}"`,
109
+ file: filePath,
110
+ line,
111
+ column: loc?.start.column || 0,
112
+ suggestion: 'Add role="button" or use a <button> element instead',
113
+ codeSnippet: getCodeSnippet(sourceCode, line),
114
+ });
115
+ }
116
+ if (!hasTabIndex) {
117
+ smells.push({
118
+ type: 'a11y-interactive-role',
119
+ severity: 'warning',
120
+ message: `Clickable <${elementName}> not focusable in "${component.name}"`,
121
+ file: filePath,
122
+ line,
123
+ column: loc?.start.column || 0,
124
+ suggestion: 'Add tabIndex={0} to make the element focusable',
125
+ codeSnippet: getCodeSnippet(sourceCode, line),
126
+ });
127
+ }
128
+ if (!hasKeyboardHandler) {
129
+ smells.push({
130
+ type: 'a11y-keyboard',
131
+ severity: 'info',
132
+ message: `Clickable <${elementName}> without keyboard handler in "${component.name}"`,
133
+ file: filePath,
134
+ line,
135
+ column: loc?.start.column || 0,
136
+ suggestion: 'Add onKeyDown to handle Enter/Space for keyboard users',
137
+ codeSnippet: getCodeSnippet(sourceCode, line),
138
+ });
139
+ }
140
+ }
141
+ }
142
+ // Check for anchor tags without href (should be buttons)
143
+ if (elementName === 'a') {
144
+ if (!hasAttr('href')) {
145
+ smells.push({
146
+ type: 'a11y-semantic',
147
+ severity: 'warning',
148
+ message: `<a> without href should be a <button> in "${component.name}"`,
149
+ file: filePath,
150
+ line,
151
+ column: loc?.start.column || 0,
152
+ suggestion: 'Use <button> for actions and <a href="..."> for navigation',
153
+ codeSnippet: getCodeSnippet(sourceCode, line),
154
+ });
155
+ }
156
+ }
157
+ // Check for proper button usage
158
+ if (elementName === 'button') {
159
+ if (!hasAttr('type')) {
160
+ smells.push({
161
+ type: 'a11y-semantic',
162
+ severity: 'info',
163
+ message: `<button> should have explicit type attribute`,
164
+ file: filePath,
165
+ line,
166
+ column: loc?.start.column || 0,
167
+ suggestion: 'Add type="button" or type="submit" to clarify button behavior',
168
+ codeSnippet: getCodeSnippet(sourceCode, line),
169
+ });
170
+ }
171
+ }
172
+ // Check for icons that might need labels
173
+ if (elementName === 'svg' || elementName === 'Icon' || elementName.endsWith('Icon')) {
174
+ const hasAriaLabel = hasAttr('aria-label') || hasAttr('aria-hidden') || hasAttr('title');
175
+ if (!hasAriaLabel) {
176
+ smells.push({
177
+ type: 'a11y-missing-label',
178
+ severity: 'info',
179
+ message: `Icon/SVG may need aria-label or aria-hidden in "${component.name}"`,
180
+ file: filePath,
181
+ line,
182
+ column: loc?.start.column || 0,
183
+ suggestion: 'Add aria-label for meaningful icons, or aria-hidden="true" for decorative ones',
184
+ codeSnippet: getCodeSnippet(sourceCode, line),
185
+ });
186
+ }
187
+ }
188
+ },
189
+ });
190
+ return smells;
191
+ }
@@ -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,10 @@
1
+ import { ParsedComponent } from '../parser/index.js';
2
+ import { CodeSmell, DetectorConfig } from '../types/index.js';
3
+ /**
4
+ * Detects debug statements that should be removed:
5
+ * - console.log/warn/error/debug
6
+ * - debugger statements
7
+ * - TODO/FIXME/HACK comments (detected via source code)
8
+ */
9
+ export declare function detectDebugStatements(component: ParsedComponent, filePath: string, sourceCode: string, config?: DetectorConfig): CodeSmell[];
10
+ //# sourceMappingURL=debug.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"debug.d.ts","sourceRoot":"","sources":["../../src/detectors/debug.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAkB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAkB,MAAM,mBAAmB,CAAC;AAE9E;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,cAA+B,GACtC,SAAS,EAAE,CAuFb"}
@@ -0,0 +1,87 @@
1
+ import * as t from '@babel/types';
2
+ import { getCodeSnippet } from '../parser/index.js';
3
+ import { DEFAULT_CONFIG } from '../types/index.js';
4
+ /**
5
+ * Detects debug statements that should be removed:
6
+ * - console.log/warn/error/debug
7
+ * - debugger statements
8
+ * - TODO/FIXME/HACK comments (detected via source code)
9
+ */
10
+ export function detectDebugStatements(component, filePath, sourceCode, config = DEFAULT_CONFIG) {
11
+ if (!config.checkDebugStatements)
12
+ return [];
13
+ const smells = [];
14
+ const reportedLines = new Set();
15
+ // Detect console.* calls
16
+ component.path.traverse({
17
+ CallExpression(path) {
18
+ const { callee } = path.node;
19
+ if (t.isMemberExpression(callee) &&
20
+ t.isIdentifier(callee.object) &&
21
+ callee.object.name === 'console') {
22
+ const method = t.isIdentifier(callee.property) ? callee.property.name : '';
23
+ const debugMethods = ['log', 'warn', 'error', 'debug', 'info', 'trace', 'dir', 'table'];
24
+ if (debugMethods.includes(method)) {
25
+ const loc = path.node.loc;
26
+ const line = loc?.start.line || 0;
27
+ if (!reportedLines.has(line)) {
28
+ reportedLines.add(line);
29
+ smells.push({
30
+ type: 'debug-statement',
31
+ severity: 'warning',
32
+ message: `console.${method}() should be removed before production`,
33
+ file: filePath,
34
+ line,
35
+ column: loc?.start.column || 0,
36
+ suggestion: 'Remove console statement or use a logging library with environment-based filtering',
37
+ codeSnippet: getCodeSnippet(sourceCode, line),
38
+ });
39
+ }
40
+ }
41
+ }
42
+ },
43
+ // Detect debugger statements
44
+ DebuggerStatement(path) {
45
+ const loc = path.node.loc;
46
+ smells.push({
47
+ type: 'debug-statement',
48
+ severity: 'error',
49
+ message: 'debugger statement must be removed before production',
50
+ file: filePath,
51
+ line: loc?.start.line || 0,
52
+ column: loc?.start.column || 0,
53
+ suggestion: 'Remove the debugger statement',
54
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
55
+ });
56
+ },
57
+ });
58
+ // Detect TODO/FIXME/HACK comments in source
59
+ const todoPatterns = [
60
+ { pattern: /\/\/\s*(TODO|FIXME|HACK|XXX|BUG)[:.\s]/gi, type: 'todo-comment' },
61
+ { pattern: /\/\*\s*(TODO|FIXME|HACK|XXX|BUG)[:.\s]/gi, type: 'todo-comment' },
62
+ ];
63
+ const lines = sourceCode.split('\n');
64
+ lines.forEach((line, index) => {
65
+ const lineNum = index + 1;
66
+ // Only check within component bounds
67
+ if (lineNum < component.startLine || lineNum > component.endLine)
68
+ return;
69
+ todoPatterns.forEach(({ pattern }) => {
70
+ const match = line.match(pattern);
71
+ if (match) {
72
+ const tag = match[0].replace(/[/\*\s:]/g, '').toUpperCase();
73
+ smells.push({
74
+ type: 'todo-comment',
75
+ severity: 'info',
76
+ message: `${tag} comment found in "${component.name}"`,
77
+ file: filePath,
78
+ line: lineNum,
79
+ column: 0,
80
+ suggestion: `Address the ${tag} or create a ticket to track it`,
81
+ codeSnippet: getCodeSnippet(sourceCode, lineNum),
82
+ });
83
+ }
84
+ });
85
+ });
86
+ return smells;
87
+ }
@@ -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
+ }
@@ -13,4 +13,10 @@ export { detectReactNativeIssues } from './reactNative.js';
13
13
  export { detectNodejsIssues } from './nodejs.js';
14
14
  export { detectJavascriptIssues } from './javascript.js';
15
15
  export { detectTypescriptIssues } from './typescript.js';
16
+ export { detectDebugStatements } from './debug.js';
17
+ export { detectSecurityIssues } from './security.js';
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';
16
22
  //# 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"}
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"}
@@ -14,3 +14,11 @@ export { detectReactNativeIssues } from './reactNative.js';
14
14
  export { detectNodejsIssues } from './nodejs.js';
15
15
  export { detectJavascriptIssues } from './javascript.js';
16
16
  export { detectTypescriptIssues } from './typescript.js';
17
+ // Debug, Security, Accessibility
18
+ export { detectDebugStatements } from './debug.js';
19
+ export { detectSecurityIssues } from './security.js';
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';
@@ -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"}