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/src/cli.ts DELETED
@@ -1,159 +0,0 @@
1
- #!/usr/bin/env node
2
-
3
- import { Command } from 'commander';
4
- import chalk from 'chalk';
5
- import ora from 'ora';
6
- import path from 'path';
7
- import { analyzeProject, DEFAULT_CONFIG } from './analyzer.js';
8
- import { reportResults } from './reporter.js';
9
- import { generateHTMLReport } from './htmlReporter.js';
10
- import fs from 'fs/promises';
11
-
12
- const program = new Command();
13
-
14
- program
15
- .name('react-smell')
16
- .description('Detect code smells in React projects')
17
- .version('1.2.0')
18
- .argument('[directory]', 'Directory to analyze', '.')
19
- .option('-f, --format <format>', 'Output format: console, json, markdown, html', 'console')
20
- .option('-s, --snippets', 'Show code snippets in output', false)
21
- .option('-c, --config <file>', 'Path to config file')
22
- .option('--ci', 'CI mode: exit with code 1 if any issues found')
23
- .option('--fail-on <severity>', 'Exit with code 1 if issues of this severity or higher (error, warning, info)', 'error')
24
- .option('--max-effects <number>', 'Max useEffects per component', parseInt)
25
- .option('--max-props <number>', 'Max props before warning', parseInt)
26
- .option('--max-lines <number>', 'Max lines per component', parseInt)
27
- .option('--include <patterns>', 'Glob patterns to include (comma-separated)')
28
- .option('--exclude <patterns>', 'Glob patterns to exclude (comma-separated)')
29
- .option('-o, --output <file>', 'Write output to file')
30
- .action(async (directory, options) => {
31
- const rootDir = path.resolve(process.cwd(), directory);
32
-
33
- // Check if directory exists
34
- try {
35
- await fs.access(rootDir);
36
- } catch {
37
- console.error(chalk.red(`Error: Directory "${rootDir}" does not exist.`));
38
- process.exit(1);
39
- }
40
-
41
- const spinner = ora('Analyzing React project...').start();
42
-
43
- try {
44
- // Load config file if specified
45
- let fileConfig = {};
46
- if (options.config) {
47
- try {
48
- const configPath = path.resolve(process.cwd(), options.config);
49
- const configContent = await fs.readFile(configPath, 'utf-8');
50
- fileConfig = JSON.parse(configContent);
51
- } catch (error) {
52
- spinner.fail(`Could not load config file: ${(error as Error).message}`);
53
- process.exit(1);
54
- }
55
- }
56
-
57
- // Build config from options
58
- const config = {
59
- ...DEFAULT_CONFIG,
60
- ...fileConfig,
61
- ...(options.maxEffects && { maxUseEffectsPerComponent: options.maxEffects }),
62
- ...(options.maxProps && { maxPropsCount: options.maxProps }),
63
- ...(options.maxLines && { maxComponentLines: options.maxLines }),
64
- };
65
-
66
- const include = options.include?.split(',').map((p: string) => p.trim()) || undefined;
67
- const exclude = options.exclude?.split(',').map((p: string) => p.trim()) || undefined;
68
-
69
- const result = await analyzeProject({
70
- rootDir,
71
- include,
72
- exclude,
73
- config,
74
- });
75
-
76
- spinner.stop();
77
-
78
- let output: string;
79
- if (options.format === 'html') {
80
- output = generateHTMLReport(result, rootDir);
81
- // Auto-set output file for HTML if not specified
82
- if (!options.output) {
83
- options.output = 'code-smell-report.html';
84
- }
85
- } else {
86
- output = reportResults(result, {
87
- format: options.format,
88
- showCodeSnippets: options.snippets,
89
- rootDir,
90
- });
91
- }
92
-
93
- if (options.output) {
94
- const outputPath = path.resolve(process.cwd(), options.output);
95
- await fs.writeFile(outputPath, output, 'utf-8');
96
- console.log(chalk.green(`✓ Report written to ${outputPath}`));
97
- } else {
98
- console.log(output);
99
- }
100
-
101
- // CI/CD exit code handling
102
- const { smellsBySeverity } = result.summary;
103
- let shouldFail = false;
104
-
105
- if (options.ci) {
106
- // CI mode: fail on any issues
107
- shouldFail = result.summary.totalSmells > 0;
108
- } else {
109
- // Check fail-on threshold
110
- switch (options.failOn) {
111
- case 'info':
112
- shouldFail = smellsBySeverity.info > 0 || smellsBySeverity.warning > 0 || smellsBySeverity.error > 0;
113
- break;
114
- case 'warning':
115
- shouldFail = smellsBySeverity.warning > 0 || smellsBySeverity.error > 0;
116
- break;
117
- case 'error':
118
- default:
119
- shouldFail = smellsBySeverity.error > 0;
120
- break;
121
- }
122
- }
123
-
124
- if (shouldFail) {
125
- process.exit(1);
126
- }
127
- } catch (error) {
128
- spinner.fail(`Analysis failed: ${(error as Error).message}`);
129
- if (process.env.DEBUG) {
130
- console.error(error);
131
- }
132
- process.exit(1);
133
- }
134
- });
135
-
136
- // Init command to create config file
137
- program
138
- .command('init')
139
- .description('Create a configuration file')
140
- .action(async () => {
141
- const configContent = JSON.stringify({
142
- maxUseEffectsPerComponent: DEFAULT_CONFIG.maxUseEffectsPerComponent,
143
- maxPropDrillingDepth: DEFAULT_CONFIG.maxPropDrillingDepth,
144
- maxComponentLines: DEFAULT_CONFIG.maxComponentLines,
145
- maxPropsCount: DEFAULT_CONFIG.maxPropsCount,
146
- }, null, 2);
147
-
148
- const configPath = path.join(process.cwd(), '.smellrc.json');
149
-
150
- try {
151
- await fs.access(configPath);
152
- console.log(chalk.yellow('Config file already exists at .smellrc.json'));
153
- } catch {
154
- await fs.writeFile(configPath, configContent, 'utf-8');
155
- console.log(chalk.green('✓ Created .smellrc.json'));
156
- }
157
- });
158
-
159
- program.parse();
@@ -1,212 +0,0 @@
1
- import * as t from '@babel/types';
2
- import { ParsedComponent, getCodeSnippet } from '../parser/index.js';
3
- import { CodeSmell, DetectorConfig, DEFAULT_CONFIG } from '../types/index.js';
4
-
5
- /**
6
- * Detects accessibility (a11y) issues:
7
- * - Images without alt text
8
- * - Form inputs without labels
9
- * - Missing ARIA attributes on interactive elements
10
- * - Click handlers without keyboard support
11
- * - Improper heading hierarchy
12
- */
13
- export function detectAccessibilityIssues(
14
- component: ParsedComponent,
15
- filePath: string,
16
- sourceCode: string,
17
- config: DetectorConfig = DEFAULT_CONFIG
18
- ): CodeSmell[] {
19
- if (!config.checkAccessibility) return [];
20
-
21
- const smells: CodeSmell[] = [];
22
-
23
- component.path.traverse({
24
- JSXOpeningElement(path) {
25
- if (!t.isJSXIdentifier(path.node.name)) return;
26
-
27
- const elementName = path.node.name.name;
28
- const attributes = path.node.attributes;
29
- const loc = path.node.loc;
30
- const line = loc?.start.line || 0;
31
-
32
- // Helper to check if attribute exists
33
- const hasAttr = (name: string): boolean => {
34
- return attributes.some(attr => {
35
- if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name)) {
36
- return attr.name.name === name;
37
- }
38
- // Handle spread attributes - assume they might contain the attribute
39
- return t.isJSXSpreadAttribute(attr);
40
- });
41
- };
42
-
43
- // Helper to get attribute value
44
- const getAttrValue = (name: string): string | null => {
45
- for (const attr of attributes) {
46
- if (t.isJSXAttribute(attr) && t.isJSXIdentifier(attr.name) && attr.name.name === name) {
47
- if (t.isStringLiteral(attr.value)) {
48
- return attr.value.value;
49
- }
50
- }
51
- }
52
- return null;
53
- };
54
-
55
- // Check for images without alt text
56
- if (elementName === 'img') {
57
- if (!hasAttr('alt')) {
58
- smells.push({
59
- type: 'a11y-missing-alt',
60
- severity: 'error',
61
- message: `<img> missing alt attribute in "${component.name}"`,
62
- file: filePath,
63
- line,
64
- column: loc?.start.column || 0,
65
- suggestion: 'Add alt="description" for content images, or alt="" for decorative images',
66
- codeSnippet: getCodeSnippet(sourceCode, line),
67
- });
68
- } else {
69
- const altValue = getAttrValue('alt');
70
- if (altValue !== null && altValue.toLowerCase().includes('image')) {
71
- smells.push({
72
- type: 'a11y-missing-alt',
73
- severity: 'info',
74
- message: `<img> alt text shouldn't contain "image" - it's redundant`,
75
- file: filePath,
76
- line,
77
- column: loc?.start.column || 0,
78
- suggestion: 'Describe what the image shows, not that it is an image',
79
- codeSnippet: getCodeSnippet(sourceCode, line),
80
- });
81
- }
82
- }
83
- }
84
-
85
- // Check for form inputs without associated labels
86
- if (elementName === 'input' || elementName === 'textarea' || elementName === 'select') {
87
- const inputType = getAttrValue('type') || 'text';
88
-
89
- // Skip hidden inputs
90
- if (inputType === 'hidden') return;
91
-
92
- const hasLabel = hasAttr('aria-label') ||
93
- hasAttr('aria-labelledby') ||
94
- hasAttr('id'); // Assume id might be linked to a label
95
-
96
- if (!hasLabel) {
97
- smells.push({
98
- type: 'a11y-missing-label',
99
- severity: 'warning',
100
- message: `<${elementName}> without accessible label in "${component.name}"`,
101
- file: filePath,
102
- line,
103
- column: loc?.start.column || 0,
104
- suggestion: 'Add aria-label, aria-labelledby, or associate with a <label> element',
105
- codeSnippet: getCodeSnippet(sourceCode, line),
106
- });
107
- }
108
- }
109
-
110
- // Check for interactive divs/spans without proper role and keyboard support
111
- if (elementName === 'div' || elementName === 'span') {
112
- const hasOnClick = hasAttr('onClick');
113
- const hasRole = hasAttr('role');
114
- const hasTabIndex = hasAttr('tabIndex');
115
- const hasKeyboardHandler = hasAttr('onKeyDown') || hasAttr('onKeyUp') || hasAttr('onKeyPress');
116
-
117
- if (hasOnClick) {
118
- if (!hasRole) {
119
- smells.push({
120
- type: 'a11y-interactive-role',
121
- severity: 'warning',
122
- message: `Clickable <${elementName}> without role attribute in "${component.name}"`,
123
- file: filePath,
124
- line,
125
- column: loc?.start.column || 0,
126
- suggestion: 'Add role="button" or use a <button> element instead',
127
- codeSnippet: getCodeSnippet(sourceCode, line),
128
- });
129
- }
130
-
131
- if (!hasTabIndex) {
132
- smells.push({
133
- type: 'a11y-interactive-role',
134
- severity: 'warning',
135
- message: `Clickable <${elementName}> not focusable in "${component.name}"`,
136
- file: filePath,
137
- line,
138
- column: loc?.start.column || 0,
139
- suggestion: 'Add tabIndex={0} to make the element focusable',
140
- codeSnippet: getCodeSnippet(sourceCode, line),
141
- });
142
- }
143
-
144
- if (!hasKeyboardHandler) {
145
- smells.push({
146
- type: 'a11y-keyboard',
147
- severity: 'info',
148
- message: `Clickable <${elementName}> without keyboard handler in "${component.name}"`,
149
- file: filePath,
150
- line,
151
- column: loc?.start.column || 0,
152
- suggestion: 'Add onKeyDown to handle Enter/Space for keyboard users',
153
- codeSnippet: getCodeSnippet(sourceCode, line),
154
- });
155
- }
156
- }
157
- }
158
-
159
- // Check for anchor tags without href (should be buttons)
160
- if (elementName === 'a') {
161
- if (!hasAttr('href')) {
162
- smells.push({
163
- type: 'a11y-semantic',
164
- severity: 'warning',
165
- message: `<a> without href should be a <button> in "${component.name}"`,
166
- file: filePath,
167
- line,
168
- column: loc?.start.column || 0,
169
- suggestion: 'Use <button> for actions and <a href="..."> for navigation',
170
- codeSnippet: getCodeSnippet(sourceCode, line),
171
- });
172
- }
173
- }
174
-
175
- // Check for proper button usage
176
- if (elementName === 'button') {
177
- if (!hasAttr('type')) {
178
- smells.push({
179
- type: 'a11y-semantic',
180
- severity: 'info',
181
- message: `<button> should have explicit type attribute`,
182
- file: filePath,
183
- line,
184
- column: loc?.start.column || 0,
185
- suggestion: 'Add type="button" or type="submit" to clarify button behavior',
186
- codeSnippet: getCodeSnippet(sourceCode, line),
187
- });
188
- }
189
- }
190
-
191
- // Check for icons that might need labels
192
- if (elementName === 'svg' || elementName === 'Icon' || elementName.endsWith('Icon')) {
193
- const hasAriaLabel = hasAttr('aria-label') || hasAttr('aria-hidden') || hasAttr('title');
194
-
195
- if (!hasAriaLabel) {
196
- smells.push({
197
- type: 'a11y-missing-label',
198
- severity: 'info',
199
- message: `Icon/SVG may need aria-label or aria-hidden in "${component.name}"`,
200
- file: filePath,
201
- line,
202
- column: loc?.start.column || 0,
203
- suggestion: 'Add aria-label for meaningful icons, or aria-hidden="true" for decorative ones',
204
- codeSnippet: getCodeSnippet(sourceCode, line),
205
- });
206
- }
207
- }
208
- },
209
- });
210
-
211
- return smells;
212
- }
@@ -1,163 +0,0 @@
1
- import * as t from '@babel/types';
2
- import { ParsedComponent, getCodeSnippet } from '../parser/index.js';
3
- import { CodeSmell, DetectorConfig, DEFAULT_CONFIG } from '../types/index.js';
4
-
5
- /**
6
- * Detects potentially dead code: unused variables, imports, and functions
7
- */
8
- export function detectDeadCode(
9
- component: ParsedComponent,
10
- filePath: string,
11
- sourceCode: string,
12
- config: DetectorConfig = DEFAULT_CONFIG
13
- ): CodeSmell[] {
14
- if (!config.checkDeadCode) return [];
15
-
16
- const smells: CodeSmell[] = [];
17
-
18
- // Track declared and used identifiers within the component
19
- const declared = new Map<string, { line: number; column: number; type: string }>();
20
- const used = new Set<string>();
21
-
22
- // Collect all declared variables in the component
23
- component.path.traverse({
24
- VariableDeclarator(path) {
25
- if (t.isIdentifier(path.node.id)) {
26
- const loc = path.node.loc;
27
- declared.set(path.node.id.name, {
28
- line: loc?.start.line || 0,
29
- column: loc?.start.column || 0,
30
- type: 'variable',
31
- });
32
- } else if (t.isObjectPattern(path.node.id)) {
33
- // Destructured variables
34
- path.node.id.properties.forEach(prop => {
35
- if (t.isObjectProperty(prop) && t.isIdentifier(prop.value)) {
36
- const loc = prop.loc;
37
- declared.set(prop.value.name, {
38
- line: loc?.start.line || 0,
39
- column: loc?.start.column || 0,
40
- type: 'variable',
41
- });
42
- } else if (t.isRestElement(prop) && t.isIdentifier(prop.argument)) {
43
- const loc = prop.loc;
44
- declared.set(prop.argument.name, {
45
- line: loc?.start.line || 0,
46
- column: loc?.start.column || 0,
47
- type: 'variable',
48
- });
49
- }
50
- });
51
- } else if (t.isArrayPattern(path.node.id)) {
52
- // Array destructured variables
53
- path.node.id.elements.forEach(elem => {
54
- if (t.isIdentifier(elem)) {
55
- const loc = elem.loc;
56
- declared.set(elem.name, {
57
- line: loc?.start.line || 0,
58
- column: loc?.start.column || 0,
59
- type: 'variable',
60
- });
61
- }
62
- });
63
- }
64
- },
65
-
66
- FunctionDeclaration(path) {
67
- if (path.node.id) {
68
- const loc = path.node.loc;
69
- declared.set(path.node.id.name, {
70
- line: loc?.start.line || 0,
71
- column: loc?.start.column || 0,
72
- type: 'function',
73
- });
74
- }
75
- },
76
- });
77
-
78
- // Collect all used identifiers
79
- component.path.traverse({
80
- Identifier(path) {
81
- // Skip if it's a declaration
82
- if (
83
- t.isVariableDeclarator(path.parent) && path.parent.id === path.node ||
84
- t.isFunctionDeclaration(path.parent) && path.parent.id === path.node ||
85
- t.isObjectProperty(path.parent) && path.parent.key === path.node ||
86
- t.isImportSpecifier(path.parent) ||
87
- t.isImportDefaultSpecifier(path.parent)
88
- ) {
89
- return;
90
- }
91
-
92
- // Skip property access (x.y - y is not a reference)
93
- if (t.isMemberExpression(path.parent) && path.parent.property === path.node && !path.parent.computed) {
94
- return;
95
- }
96
-
97
- used.add(path.node.name);
98
- },
99
-
100
- JSXIdentifier(path) {
101
- // Components used in JSX
102
- if (t.isJSXOpeningElement(path.parent) || t.isJSXClosingElement(path.parent)) {
103
- used.add(path.node.name);
104
- }
105
- },
106
- });
107
-
108
- // Find unused declarations
109
- declared.forEach((info, name) => {
110
- // Skip hooks and common patterns
111
- if (name.startsWith('_') || name.startsWith('set')) return;
112
-
113
- // Skip if it's the component name itself
114
- if (name === component.name) return;
115
-
116
- if (!used.has(name)) {
117
- smells.push({
118
- type: 'dead-code',
119
- severity: 'info',
120
- message: `Unused ${info.type} "${name}" in "${component.name}"`,
121
- file: filePath,
122
- line: info.line,
123
- column: info.column,
124
- suggestion: `Remove the unused ${info.type} or use it in the component.`,
125
- codeSnippet: getCodeSnippet(sourceCode, info.line),
126
- });
127
- }
128
- });
129
-
130
- return smells;
131
- }
132
-
133
- /**
134
- * Detects unused imports at the file level
135
- */
136
- export function detectUnusedImports(
137
- imports: Map<string, { source: string; line: number }>,
138
- usedInFile: Set<string>,
139
- filePath: string,
140
- sourceCode: string
141
- ): CodeSmell[] {
142
- const smells: CodeSmell[] = [];
143
-
144
- imports.forEach((info, name) => {
145
- // Skip React imports as they might be used implicitly
146
- if (name === 'React' || info.source === 'react') return;
147
-
148
- if (!usedInFile.has(name)) {
149
- smells.push({
150
- type: 'dead-code',
151
- severity: 'info',
152
- message: `Unused import "${name}" from "${info.source}"`,
153
- file: filePath,
154
- line: info.line,
155
- column: 0,
156
- suggestion: `Remove the unused import: import { ${name} } from '${info.source}'`,
157
- codeSnippet: getCodeSnippet(sourceCode, info.line),
158
- });
159
- }
160
- });
161
-
162
- return smells;
163
- }
@@ -1,103 +0,0 @@
1
- import * as t from '@babel/types';
2
- import { ParsedComponent, getCodeSnippet } from '../parser/index.js';
3
- import { CodeSmell, DetectorConfig, DEFAULT_CONFIG } from '../types/index.js';
4
-
5
- /**
6
- * Detects debug statements that should be removed:
7
- * - console.log/warn/error/debug
8
- * - debugger statements
9
- * - TODO/FIXME/HACK comments (detected via source code)
10
- */
11
- export function detectDebugStatements(
12
- component: ParsedComponent,
13
- filePath: string,
14
- sourceCode: string,
15
- config: DetectorConfig = DEFAULT_CONFIG
16
- ): CodeSmell[] {
17
- if (!config.checkDebugStatements) return [];
18
-
19
- const smells: CodeSmell[] = [];
20
- const reportedLines = new Set<number>();
21
-
22
- // Detect console.* calls
23
- component.path.traverse({
24
- CallExpression(path) {
25
- const { callee } = path.node;
26
-
27
- if (t.isMemberExpression(callee) &&
28
- t.isIdentifier(callee.object) &&
29
- callee.object.name === 'console') {
30
-
31
- const method = t.isIdentifier(callee.property) ? callee.property.name : '';
32
- const debugMethods = ['log', 'warn', 'error', 'debug', 'info', 'trace', 'dir', 'table'];
33
-
34
- if (debugMethods.includes(method)) {
35
- const loc = path.node.loc;
36
- const line = loc?.start.line || 0;
37
-
38
- if (!reportedLines.has(line)) {
39
- reportedLines.add(line);
40
- smells.push({
41
- type: 'debug-statement',
42
- severity: 'warning',
43
- message: `console.${method}() should be removed before production`,
44
- file: filePath,
45
- line,
46
- column: loc?.start.column || 0,
47
- suggestion: 'Remove console statement or use a logging library with environment-based filtering',
48
- codeSnippet: getCodeSnippet(sourceCode, line),
49
- });
50
- }
51
- }
52
- }
53
- },
54
-
55
- // Detect debugger statements
56
- DebuggerStatement(path) {
57
- const loc = path.node.loc;
58
- smells.push({
59
- type: 'debug-statement',
60
- severity: 'error',
61
- message: 'debugger statement must be removed before production',
62
- file: filePath,
63
- line: loc?.start.line || 0,
64
- column: loc?.start.column || 0,
65
- suggestion: 'Remove the debugger statement',
66
- codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
67
- });
68
- },
69
- });
70
-
71
- // Detect TODO/FIXME/HACK comments in source
72
- const todoPatterns = [
73
- { pattern: /\/\/\s*(TODO|FIXME|HACK|XXX|BUG)[:.\s]/gi, type: 'todo-comment' as const },
74
- { pattern: /\/\*\s*(TODO|FIXME|HACK|XXX|BUG)[:.\s]/gi, type: 'todo-comment' as const },
75
- ];
76
-
77
- const lines = sourceCode.split('\n');
78
- lines.forEach((line, index) => {
79
- const lineNum = index + 1;
80
-
81
- // Only check within component bounds
82
- if (lineNum < component.startLine || lineNum > component.endLine) return;
83
-
84
- todoPatterns.forEach(({ pattern }) => {
85
- const match = line.match(pattern);
86
- if (match) {
87
- const tag = match[0].replace(/[/\*\s:]/g, '').toUpperCase();
88
- smells.push({
89
- type: 'todo-comment',
90
- severity: 'info',
91
- message: `${tag} comment found in "${component.name}"`,
92
- file: filePath,
93
- line: lineNum,
94
- column: 0,
95
- suggestion: `Address the ${tag} or create a ticket to track it`,
96
- codeSnippet: getCodeSnippet(sourceCode, lineNum),
97
- });
98
- }
99
- });
100
- });
101
-
102
- return smells;
103
- }