react-code-smell-detector 1.1.1 → 1.2.0

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.
package/README.md CHANGED
@@ -7,7 +7,11 @@ A CLI tool that analyzes React projects and detects common code smells, providin
7
7
  - 🔍 **Detect Code Smells**: Identifies common React anti-patterns
8
8
  - 📊 **Technical Debt Score**: Grades your codebase from A to F
9
9
  - 💡 **Refactoring Suggestions**: Actionable recommendations for each issue
10
- - 📝 **Multiple Output Formats**: Console (colored), JSON, and Markdown
10
+ - 📝 **Multiple Output Formats**: Console (colored), JSON, Markdown, and HTML
11
+ - 🔒 **Security Scanning**: Detects XSS vulnerabilities, eval usage, exposed secrets
12
+ - ♿ **Accessibility Checks**: Missing alt text, labels, ARIA attributes
13
+ - 🐛 **Debug Cleanup**: console.log, debugger, TODO/FIXME detection
14
+ - 🤖 **CI/CD Ready**: Exit codes and flags for pipeline integration
11
15
 
12
16
  ### Detected Code Smells
13
17
 
@@ -18,6 +22,10 @@ A CLI tool that analyzes React projects and detects common code smells, providin
18
22
  | **Large Components** | Components over 300 lines, deep JSX nesting |
19
23
  | **Unmemoized Calculations** | `.map()`, `.filter()`, `.reduce()`, `JSON.parse()` without `useMemo` |
20
24
  | **Inline Functions** | Callbacks defined inline in JSX props |
25
+ | **Security Issues** | dangerouslySetInnerHTML, eval(), innerHTML, exposed API keys |
26
+ | **Accessibility** | Missing alt text, form labels, keyboard handlers, semantic HTML |
27
+ | **Debug Statements** | console.log, debugger statements, TODO/FIXME comments |
28
+ | **Framework-Specific** | Next.js, React Native, Node.js, TypeScript issues |
21
29
 
22
30
  ## Installation
23
31
 
@@ -56,6 +64,9 @@ react-smell ./src -f json
56
64
 
57
65
  # Markdown (for documentation)
58
66
  react-smell ./src -f markdown -o report.md
67
+
68
+ # HTML (beautiful visual report)
69
+ react-smell ./src -f html -o report.html
59
70
  ```
60
71
 
61
72
  ### Configuration
@@ -81,9 +92,11 @@ Or create manually:
81
92
 
82
93
  | Option | Description | Default |
83
94
  |--------|-------------|---------|
84
- | `-f, --format <format>` | Output format: console, json, markdown | `console` |
95
+ | `-f, --format <format>` | Output format: console, json, markdown, html | `console` |
85
96
  | `-s, --snippets` | Show code snippets in output | `false` |
86
97
  | `-c, --config <file>` | Path to config file | `.smellrc.json` |
98
+ | `--ci` | CI mode: exit with code 1 if any issues | `false` |
99
+ | `--fail-on <severity>` | Exit code 1 threshold: error, warning, info | `error` |
87
100
  | `--max-effects <number>` | Max useEffects per component | `3` |
88
101
  | `--max-props <number>` | Max props before warning | `7` |
89
102
  | `--max-lines <number>` | Max lines per component | `300` |
@@ -143,17 +156,62 @@ const report = reportResults(result, {
143
156
 
144
157
  ## CI/CD Integration
145
158
 
146
- Use the JSON output format for CI pipelines:
159
+ The tool provides flexible exit codes for CI/CD pipelines:
160
+
161
+ ```bash
162
+ # Fail on any issues (strict mode)
163
+ react-smell ./src --ci
164
+
165
+ # Fail on warnings and errors (default: errors only)
166
+ react-smell ./src --fail-on warning
167
+
168
+ # Generate HTML report and fail on errors
169
+ react-smell ./src -f html -o report.html --fail-on error
170
+ ```
171
+
172
+ ### GitHub Actions Example
147
173
 
148
174
  ```yaml
149
- # GitHub Actions example
150
- - name: Check code smells
151
- run: |
152
- npx react-smell ./src -f json > smell-report.json
153
- if [ $(jq '.summary.smellsBySeverity.error' smell-report.json) -gt 0 ]; then
154
- echo "Code smell errors found!"
155
- exit 1
156
- fi
175
+ name: Code Quality
176
+
177
+ on: [push, pull_request]
178
+
179
+ jobs:
180
+ code-smells:
181
+ runs-on: ubuntu-latest
182
+ steps:
183
+ - uses: actions/checkout@v4
184
+ - uses: actions/setup-node@v4
185
+ with:
186
+ node-version: '20'
187
+
188
+ - name: Install dependencies
189
+ run: npm ci
190
+
191
+ - name: Check code smells
192
+ run: npx react-smell ./src -f html -o smell-report.html --fail-on warning
193
+
194
+ - name: Upload report
195
+ uses: actions/upload-artifact@v4
196
+ if: always()
197
+ with:
198
+ name: code-smell-report
199
+ path: smell-report.html
200
+ ```
201
+
202
+ ## Ignoring Issues
203
+
204
+ Use `@smell-ignore` comments to suppress specific issues:
205
+
206
+ ```tsx
207
+ // @smell-ignore
208
+ console.log('This debug statement is ignored');
209
+
210
+ // @smell-ignore debug-statement
211
+ console.log('Only ignores debug-statement type');
212
+
213
+ // @smell-ignore *
214
+ const risky = eval('1+1'); // All issues on next line ignored
157
215
  ```
158
216
 
159
217
  ## Technical Debt Score
@@ -1 +1 @@
1
- {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAqBA,OAAO,EACL,cAAc,EAMd,cAAc,EAIf,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;CAClC;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CA0CtF;AAyLD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC"}
1
+ {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAwBA,OAAO,EACL,cAAc,EAMd,cAAc,EAIf,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,MAAM,CAAC,EAAE,OAAO,CAAC,cAAc,CAAC,CAAC;CAClC;AAED,wBAAsB,cAAc,CAAC,OAAO,EAAE,eAAe,GAAG,OAAO,CAAC,cAAc,CAAC,CA0CtF;AA6OD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC"}
package/dist/analyzer.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import fg from 'fast-glob';
2
2
  import path from 'path';
3
3
  import { parseFile } from './parser/index.js';
4
- import { detectUseEffectOveruse, detectPropDrilling, analyzePropDrillingDepth, detectLargeComponent, detectUnmemoizedCalculations, detectMissingKeys, detectHooksRulesViolations, detectDependencyArrayIssues, detectNestedTernaries, detectDeadCode, detectMagicValues, detectNextjsIssues, detectReactNativeIssues, detectNodejsIssues, detectJavascriptIssues, detectTypescriptIssues, } from './detectors/index.js';
4
+ import { detectUseEffectOveruse, detectPropDrilling, analyzePropDrillingDepth, detectLargeComponent, detectUnmemoizedCalculations, detectMissingKeys, detectHooksRulesViolations, detectDependencyArrayIssues, detectNestedTernaries, detectDeadCode, detectMagicValues, detectNextjsIssues, detectReactNativeIssues, detectNodejsIssues, detectJavascriptIssues, detectTypescriptIssues, detectDebugStatements, detectSecurityIssues, detectAccessibilityIssues, } from './detectors/index.js';
5
5
  import { DEFAULT_CONFIG, } from './types/index.js';
6
6
  export async function analyzeProject(options) {
7
7
  const { rootDir, include = ['**/*.tsx', '**/*.jsx'], exclude = ['**/node_modules/**', '**/dist/**', '**/build/**', '**/*.test.*', '**/*.spec.*'], config: userConfig = {}, } = options;
@@ -74,16 +74,49 @@ function analyzeFile(parseResult, filePath, config) {
74
74
  smells.push(...detectNodejsIssues(component, filePath, sourceCode, config, imports));
75
75
  smells.push(...detectJavascriptIssues(component, filePath, sourceCode, config));
76
76
  smells.push(...detectTypescriptIssues(component, filePath, sourceCode, config));
77
+ // Debug, Security, Accessibility
78
+ smells.push(...detectDebugStatements(component, filePath, sourceCode, config));
79
+ smells.push(...detectSecurityIssues(component, filePath, sourceCode, config));
80
+ smells.push(...detectAccessibilityIssues(component, filePath, sourceCode, config));
77
81
  });
78
82
  // Run cross-component analysis
79
83
  smells.push(...analyzePropDrillingDepth(components, filePath, sourceCode, config));
84
+ // Filter out smells with @smell-ignore comments
85
+ const filteredSmells = filterIgnoredSmells(smells, sourceCode);
80
86
  return {
81
87
  file: filePath,
82
88
  components: componentInfos,
83
- smells,
89
+ smells: filteredSmells,
84
90
  imports,
85
91
  };
86
92
  }
93
+ /**
94
+ * Filter out smells that have @smell-ignore comment on the preceding line
95
+ * Supports: // @smell-ignore, block comments with @smell-ignore, @smell-ignore [type]
96
+ */
97
+ function filterIgnoredSmells(smells, sourceCode) {
98
+ const lines = sourceCode.split('\n');
99
+ return smells.filter(smell => {
100
+ if (smell.line <= 1)
101
+ return true;
102
+ // Check the line before and the same line for ignore comments
103
+ const lineIndex = smell.line - 1; // Convert to 0-indexed
104
+ const prevLine = lines[lineIndex - 1]?.trim() || '';
105
+ const currentLine = lines[lineIndex]?.trim() || '';
106
+ // Check for @smell-ignore patterns
107
+ const ignorePatterns = [
108
+ /@smell-ignore\s*$/, // @smell-ignore (ignore all)
109
+ /@smell-ignore\s+\*/, // @smell-ignore * (ignore all)
110
+ new RegExp(`@smell-ignore\\s+${smell.type}`), // @smell-ignore [specific-type]
111
+ ];
112
+ for (const pattern of ignorePatterns) {
113
+ if (pattern.test(prevLine) || pattern.test(currentLine)) {
114
+ return false; // Filter out this smell
115
+ }
116
+ }
117
+ return true; // Keep this smell
118
+ });
119
+ }
87
120
  function calculateSummary(files) {
88
121
  const smellsByType = {
89
122
  'useEffect-overuse': 0,
@@ -124,6 +157,19 @@ function calculateSummary(files) {
124
157
  'ts-missing-return-type': 0,
125
158
  'ts-non-null-assertion': 0,
126
159
  'ts-type-assertion': 0,
160
+ // Debug statements
161
+ 'debug-statement': 0,
162
+ 'todo-comment': 0,
163
+ // Security
164
+ 'security-xss': 0,
165
+ 'security-eval': 0,
166
+ 'security-secrets': 0,
167
+ // Accessibility
168
+ 'a11y-missing-alt': 0,
169
+ 'a11y-missing-label': 0,
170
+ 'a11y-interactive-role': 0,
171
+ 'a11y-keyboard': 0,
172
+ 'a11y-semantic': 0,
127
173
  };
128
174
  const smellsBySeverity = {
129
175
  error: 0,
package/dist/cli.js CHANGED
@@ -5,16 +5,19 @@ import ora from 'ora';
5
5
  import path from 'path';
6
6
  import { analyzeProject, DEFAULT_CONFIG } from './analyzer.js';
7
7
  import { reportResults } from './reporter.js';
8
+ import { generateHTMLReport } from './htmlReporter.js';
8
9
  import fs from 'fs/promises';
9
10
  const program = new Command();
10
11
  program
11
12
  .name('react-smell')
12
13
  .description('Detect code smells in React projects')
13
- .version('1.0.0')
14
+ .version('1.2.0')
14
15
  .argument('[directory]', 'Directory to analyze', '.')
15
- .option('-f, --format <format>', 'Output format: console, json, markdown', 'console')
16
+ .option('-f, --format <format>', 'Output format: console, json, markdown, html', 'console')
16
17
  .option('-s, --snippets', 'Show code snippets in output', false)
17
18
  .option('-c, --config <file>', 'Path to config file')
19
+ .option('--ci', 'CI mode: exit with code 1 if any issues found')
20
+ .option('--fail-on <severity>', 'Exit with code 1 if issues of this severity or higher (error, warning, info)', 'error')
18
21
  .option('--max-effects <number>', 'Max useEffects per component', parseInt)
19
22
  .option('--max-props <number>', 'Max props before warning', parseInt)
20
23
  .option('--max-lines <number>', 'Max lines per component', parseInt)
@@ -63,11 +66,21 @@ program
63
66
  config,
64
67
  });
65
68
  spinner.stop();
66
- const output = reportResults(result, {
67
- format: options.format,
68
- showCodeSnippets: options.snippets,
69
- rootDir,
70
- });
69
+ let output;
70
+ if (options.format === 'html') {
71
+ output = generateHTMLReport(result, rootDir);
72
+ // Auto-set output file for HTML if not specified
73
+ if (!options.output) {
74
+ options.output = 'code-smell-report.html';
75
+ }
76
+ }
77
+ else {
78
+ output = reportResults(result, {
79
+ format: options.format,
80
+ showCodeSnippets: options.snippets,
81
+ rootDir,
82
+ });
83
+ }
71
84
  if (options.output) {
72
85
  const outputPath = path.resolve(process.cwd(), options.output);
73
86
  await fs.writeFile(outputPath, output, 'utf-8');
@@ -76,8 +89,29 @@ program
76
89
  else {
77
90
  console.log(output);
78
91
  }
79
- // Exit with error code if there are errors
80
- if (result.summary.smellsBySeverity.error > 0) {
92
+ // CI/CD exit code handling
93
+ const { smellsBySeverity } = result.summary;
94
+ let shouldFail = false;
95
+ if (options.ci) {
96
+ // CI mode: fail on any issues
97
+ shouldFail = result.summary.totalSmells > 0;
98
+ }
99
+ else {
100
+ // Check fail-on threshold
101
+ switch (options.failOn) {
102
+ case 'info':
103
+ shouldFail = smellsBySeverity.info > 0 || smellsBySeverity.warning > 0 || smellsBySeverity.error > 0;
104
+ break;
105
+ case 'warning':
106
+ shouldFail = smellsBySeverity.warning > 0 || smellsBySeverity.error > 0;
107
+ break;
108
+ case 'error':
109
+ default:
110
+ shouldFail = smellsBySeverity.error > 0;
111
+ break;
112
+ }
113
+ }
114
+ if (shouldFail) {
81
115
  process.exit(1);
82
116
  }
83
117
  }
@@ -0,0 +1,12 @@
1
+ import { ParsedComponent } from '../parser/index.js';
2
+ import { CodeSmell, DetectorConfig } from '../types/index.js';
3
+ /**
4
+ * Detects accessibility (a11y) issues:
5
+ * - Images without alt text
6
+ * - Form inputs without labels
7
+ * - Missing ARIA attributes on interactive elements
8
+ * - Click handlers without keyboard support
9
+ * - Improper heading hierarchy
10
+ */
11
+ export declare function detectAccessibilityIssues(component: ParsedComponent, filePath: string, sourceCode: string, config?: DetectorConfig): CodeSmell[];
12
+ //# sourceMappingURL=accessibility.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accessibility.d.ts","sourceRoot":"","sources":["../../src/detectors/accessibility.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAkB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAkB,MAAM,mBAAmB,CAAC;AAE9E;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CACvC,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,cAA+B,GACtC,SAAS,EAAE,CAkMb"}
@@ -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,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
+ }
@@ -13,4 +13,7 @@ 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';
16
19
  //# 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"}
@@ -14,3 +14,7 @@ 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';
@@ -0,0 +1,12 @@
1
+ import { ParsedComponent } from '../parser/index.js';
2
+ import { CodeSmell, DetectorConfig } from '../types/index.js';
3
+ /**
4
+ * Detects security vulnerabilities:
5
+ * - dangerouslySetInnerHTML usage
6
+ * - eval() and Function() constructor
7
+ * - innerHTML assignments
8
+ * - Unsafe URLs (javascript:, data:)
9
+ * - Exposed secrets/API keys
10
+ */
11
+ export declare function detectSecurityIssues(component: ParsedComponent, filePath: string, sourceCode: string, config?: DetectorConfig): CodeSmell[];
12
+ //# sourceMappingURL=security.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"security.d.ts","sourceRoot":"","sources":["../../src/detectors/security.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAkB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAkB,MAAM,mBAAmB,CAAC;AAE9E;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAClC,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,cAA+B,GACtC,SAAS,EAAE,CAiKb"}