react-code-smell-detector 1.0.0 → 1.1.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 (59) hide show
  1. package/dist/analyzer.d.ts.map +1 -1
  2. package/dist/analyzer.js +43 -1
  3. package/dist/cli.js +0 -0
  4. package/dist/detectors/deadCode.d.ts +14 -0
  5. package/dist/detectors/deadCode.d.ts.map +1 -0
  6. package/dist/detectors/deadCode.js +141 -0
  7. package/dist/detectors/dependencyArray.d.ts +7 -0
  8. package/dist/detectors/dependencyArray.d.ts.map +1 -0
  9. package/dist/detectors/dependencyArray.js +164 -0
  10. package/dist/detectors/hooksRules.d.ts +7 -0
  11. package/dist/detectors/hooksRules.d.ts.map +1 -0
  12. package/dist/detectors/hooksRules.js +81 -0
  13. package/dist/detectors/index.d.ts +11 -0
  14. package/dist/detectors/index.d.ts.map +1 -1
  15. package/dist/detectors/index.js +12 -0
  16. package/dist/detectors/javascript.d.ts +11 -0
  17. package/dist/detectors/javascript.d.ts.map +1 -0
  18. package/dist/detectors/javascript.js +148 -0
  19. package/dist/detectors/magicValues.d.ts +7 -0
  20. package/dist/detectors/magicValues.d.ts.map +1 -0
  21. package/dist/detectors/magicValues.js +99 -0
  22. package/dist/detectors/missingKey.d.ts +7 -0
  23. package/dist/detectors/missingKey.d.ts.map +1 -0
  24. package/dist/detectors/missingKey.js +93 -0
  25. package/dist/detectors/nestedTernary.d.ts +7 -0
  26. package/dist/detectors/nestedTernary.d.ts.map +1 -0
  27. package/dist/detectors/nestedTernary.js +58 -0
  28. package/dist/detectors/nextjs.d.ts +11 -0
  29. package/dist/detectors/nextjs.d.ts.map +1 -0
  30. package/dist/detectors/nextjs.js +103 -0
  31. package/dist/detectors/nodejs.d.ts +11 -0
  32. package/dist/detectors/nodejs.d.ts.map +1 -0
  33. package/dist/detectors/nodejs.js +169 -0
  34. package/dist/detectors/reactNative.d.ts +10 -0
  35. package/dist/detectors/reactNative.d.ts.map +1 -0
  36. package/dist/detectors/reactNative.js +135 -0
  37. package/dist/detectors/typescript.d.ts +11 -0
  38. package/dist/detectors/typescript.d.ts.map +1 -0
  39. package/dist/detectors/typescript.js +135 -0
  40. package/dist/reporter.js +30 -0
  41. package/dist/types/index.d.ts +14 -1
  42. package/dist/types/index.d.ts.map +1 -1
  43. package/dist/types/index.js +14 -0
  44. package/package.json +1 -1
  45. package/src/analyzer.ts +53 -0
  46. package/src/detectors/deadCode.ts +163 -0
  47. package/src/detectors/dependencyArray.ts +176 -0
  48. package/src/detectors/hooksRules.ts +101 -0
  49. package/src/detectors/index.ts +12 -0
  50. package/src/detectors/javascript.ts +169 -0
  51. package/src/detectors/magicValues.ts +114 -0
  52. package/src/detectors/missingKey.ts +105 -0
  53. package/src/detectors/nestedTernary.ts +75 -0
  54. package/src/detectors/nextjs.ts +124 -0
  55. package/src/detectors/nodejs.ts +199 -0
  56. package/src/detectors/reactNative.ts +154 -0
  57. package/src/detectors/typescript.ts +151 -0
  58. package/src/reporter.ts +30 -0
  59. package/src/types/index.ts +59 -1
@@ -0,0 +1,148 @@
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 vanilla JavaScript code smells:
6
+ * - var usage (should use let/const)
7
+ * - Loose equality (== instead of ===)
8
+ * - Implicit type coercion
9
+ * - Global variable pollution
10
+ */
11
+ export function detectJavascriptIssues(component, filePath, sourceCode, config = DEFAULT_CONFIG) {
12
+ if (!config.checkJavascript)
13
+ return [];
14
+ const smells = [];
15
+ // Detect var usage (should use let/const)
16
+ component.path.traverse({
17
+ VariableDeclaration(path) {
18
+ if (path.node.kind === 'var') {
19
+ const loc = path.node.loc;
20
+ smells.push({
21
+ type: 'js-var-usage',
22
+ severity: 'warning',
23
+ message: `Using "var" instead of "let" or "const" in "${component.name}"`,
24
+ file: filePath,
25
+ line: loc?.start.line || 0,
26
+ column: loc?.start.column || 0,
27
+ suggestion: 'Use "const" for values that never change, "let" for reassignable variables',
28
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
29
+ });
30
+ }
31
+ },
32
+ });
33
+ // Detect loose equality (== and != instead of === and !==)
34
+ component.path.traverse({
35
+ BinaryExpression(path) {
36
+ const { operator, left, right } = path.node;
37
+ // Skip if comparing with null/undefined (sometimes intentional)
38
+ const isNullCheck = (t.isNullLiteral(left) || t.isNullLiteral(right)) ||
39
+ (t.isIdentifier(left) && left.name === 'undefined') ||
40
+ (t.isIdentifier(right) && right.name === 'undefined');
41
+ if (operator === '==' && !isNullCheck) {
42
+ const loc = path.node.loc;
43
+ smells.push({
44
+ type: 'js-loose-equality',
45
+ severity: 'warning',
46
+ message: `Using loose equality "==" in "${component.name}"`,
47
+ file: filePath,
48
+ line: loc?.start.line || 0,
49
+ column: loc?.start.column || 0,
50
+ suggestion: 'Use strict equality "===" to avoid type coercion bugs',
51
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
52
+ });
53
+ }
54
+ if (operator === '!=' && !isNullCheck) {
55
+ const loc = path.node.loc;
56
+ smells.push({
57
+ type: 'js-loose-equality',
58
+ severity: 'warning',
59
+ message: `Using loose inequality "!=" in "${component.name}"`,
60
+ file: filePath,
61
+ line: loc?.start.line || 0,
62
+ column: loc?.start.column || 0,
63
+ suggestion: 'Use strict inequality "!==" to avoid type coercion bugs',
64
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
65
+ });
66
+ }
67
+ },
68
+ });
69
+ // Detect implicit type coercion patterns
70
+ component.path.traverse({
71
+ // +string to convert to number
72
+ UnaryExpression(path) {
73
+ if (path.node.operator === '+' && t.isIdentifier(path.node.argument)) {
74
+ const loc = path.node.loc;
75
+ smells.push({
76
+ type: 'js-implicit-coercion',
77
+ severity: 'info',
78
+ message: `Implicit number coercion with unary + in "${component.name}"`,
79
+ file: filePath,
80
+ line: loc?.start.line || 0,
81
+ column: loc?.start.column || 0,
82
+ suggestion: 'Use explicit conversion: Number(value) or parseInt(value, 10)',
83
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
84
+ });
85
+ }
86
+ },
87
+ // !!value for boolean coercion - detected via UnaryExpression
88
+ // LogicalExpression handles &&, ||, ?? - skip for !! detection
89
+ // String concatenation with + that mixes types
90
+ BinaryExpression(path) {
91
+ if (path.node.operator === '+') {
92
+ const { left, right } = path.node;
93
+ const leftIsString = t.isStringLiteral(left) || t.isTemplateLiteral(left);
94
+ const rightIsString = t.isStringLiteral(right) || t.isTemplateLiteral(right);
95
+ const leftIsNumber = t.isNumericLiteral(left);
96
+ const rightIsNumber = t.isNumericLiteral(right);
97
+ // Mixed string + number concatenation
98
+ if ((leftIsString && rightIsNumber) || (leftIsNumber && rightIsString)) {
99
+ const loc = path.node.loc;
100
+ smells.push({
101
+ type: 'js-implicit-coercion',
102
+ severity: 'info',
103
+ message: `Implicit string coercion in "${component.name}"`,
104
+ file: filePath,
105
+ line: loc?.start.line || 0,
106
+ column: loc?.start.column || 0,
107
+ suggestion: 'Use template literal for clarity: `${value}` or String(value)',
108
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
109
+ });
110
+ }
111
+ }
112
+ },
113
+ });
114
+ // Detect potential global pollution (assignments without declaration)
115
+ component.path.traverse({
116
+ AssignmentExpression(path) {
117
+ const { left } = path.node;
118
+ if (t.isIdentifier(left)) {
119
+ // Check if this identifier is declared in scope
120
+ const binding = path.scope.getBinding(left.name);
121
+ if (!binding) {
122
+ // Check if it's a well-known global
123
+ const knownGlobals = [
124
+ 'window', 'document', 'console', 'localStorage', 'sessionStorage',
125
+ 'setTimeout', 'setInterval', 'clearTimeout', 'clearInterval',
126
+ 'fetch', 'XMLHttpRequest', 'WebSocket', 'module', 'exports',
127
+ 'require', 'process', 'global', '__dirname', '__filename',
128
+ 'Buffer', 'Promise', 'Map', 'Set', 'Symbol',
129
+ ];
130
+ if (!knownGlobals.includes(left.name)) {
131
+ const loc = path.node.loc;
132
+ smells.push({
133
+ type: 'js-global-pollution',
134
+ severity: 'error',
135
+ message: `Implicit global variable "${left.name}" in "${component.name}"`,
136
+ file: filePath,
137
+ line: loc?.start.line || 0,
138
+ column: loc?.start.column || 0,
139
+ suggestion: `Declare the variable: const ${left.name} = ... or let ${left.name} = ...`,
140
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
141
+ });
142
+ }
143
+ }
144
+ }
145
+ },
146
+ });
147
+ return smells;
148
+ }
@@ -0,0 +1,7 @@
1
+ import { ParsedComponent } from '../parser/index.js';
2
+ import { CodeSmell, DetectorConfig } from '../types/index.js';
3
+ /**
4
+ * Detects magic numbers and strings that should be constants
5
+ */
6
+ export declare function detectMagicValues(component: ParsedComponent, filePath: string, sourceCode: string, config?: DetectorConfig): CodeSmell[];
7
+ //# sourceMappingURL=magicValues.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"magicValues.d.ts","sourceRoot":"","sources":["../../src/detectors/magicValues.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAkB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAkB,MAAM,mBAAmB,CAAC;AAE9E;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,cAA+B,GACtC,SAAS,EAAE,CAqGb"}
@@ -0,0 +1,99 @@
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 magic numbers and strings that should be constants
6
+ */
7
+ export function detectMagicValues(component, filePath, sourceCode, config = DEFAULT_CONFIG) {
8
+ if (!config.checkMagicValues)
9
+ return [];
10
+ const smells = [];
11
+ const threshold = config.magicNumberThreshold;
12
+ // Track locations to avoid duplicate reports
13
+ const reportedLines = new Set();
14
+ component.path.traverse({
15
+ NumericLiteral(path) {
16
+ const value = path.node.value;
17
+ const loc = path.node.loc;
18
+ const line = loc?.start.line || 0;
19
+ // Skip common acceptable values
20
+ if (value === 0 || value === 1 || value === -1 || value === 2 || value === 100)
21
+ return;
22
+ // Skip array indices and small numbers
23
+ if (value < threshold && value > -threshold)
24
+ return;
25
+ // Skip if inside an object key position (e.g., style objects)
26
+ if (t.isObjectProperty(path.parent) && path.parent.key === path.node)
27
+ return;
28
+ // Skip if it's a port number or similar config
29
+ if (value === 3000 || value === 8080 || value === 80 || value === 443)
30
+ return;
31
+ // Skip if already reported this line
32
+ if (reportedLines.has(line))
33
+ return;
34
+ reportedLines.add(line);
35
+ // Check context - skip if it's in a variable declaration that names the constant
36
+ if (t.isVariableDeclarator(path.parent)) {
37
+ const varName = t.isIdentifier(path.parent.id) ? path.parent.id.name : '';
38
+ // If variable name is UPPER_CASE, it's already a constant
39
+ if (varName === varName.toUpperCase() && varName.includes('_'))
40
+ return;
41
+ }
42
+ smells.push({
43
+ type: 'magic-value',
44
+ severity: 'info',
45
+ message: `Magic number ${value} in "${component.name}"`,
46
+ file: filePath,
47
+ line,
48
+ column: loc?.start.column || 0,
49
+ suggestion: `Extract to a named constant: const DESCRIPTIVE_NAME = ${value}`,
50
+ codeSnippet: getCodeSnippet(sourceCode, line),
51
+ });
52
+ },
53
+ StringLiteral(path) {
54
+ const value = path.node.value;
55
+ const loc = path.node.loc;
56
+ const line = loc?.start.line || 0;
57
+ // Skip short strings, empty strings, common values
58
+ if (value.length < 10)
59
+ return;
60
+ // Skip if it looks like a URL, path, or import
61
+ if (value.startsWith('http') || value.startsWith('/') || value.startsWith('.'))
62
+ return;
63
+ // Skip if it's an import source
64
+ if (t.isImportDeclaration(path.parent))
65
+ return;
66
+ // Skip JSX text content (usually intentional)
67
+ if (t.isJSXText(path.parent) || t.isJSXExpressionContainer(path.parent))
68
+ return;
69
+ // Skip object property keys
70
+ if (t.isObjectProperty(path.parent) && path.parent.key === path.node)
71
+ return;
72
+ // Skip if already reported this line
73
+ if (reportedLines.has(line))
74
+ return;
75
+ // Skip common patterns
76
+ if (value.includes('className') ||
77
+ value.includes('px') ||
78
+ value.includes('rem') ||
79
+ value.includes('%'))
80
+ return;
81
+ // Check if it looks like a user-facing message or error
82
+ const looksLikeMessage = /^[A-Z][a-z]+.*[.!?]?$/.test(value) || value.includes(' ');
83
+ if (looksLikeMessage && value.length > 30) {
84
+ reportedLines.add(line);
85
+ smells.push({
86
+ type: 'magic-value',
87
+ severity: 'info',
88
+ message: `Hardcoded string in "${component.name}" should be in constants or i18n`,
89
+ file: filePath,
90
+ line,
91
+ column: loc?.start.column || 0,
92
+ suggestion: `Extract to a constants file or use i18n: "${value.substring(0, 30)}..."`,
93
+ codeSnippet: getCodeSnippet(sourceCode, line),
94
+ });
95
+ }
96
+ },
97
+ });
98
+ return smells;
99
+ }
@@ -0,0 +1,7 @@
1
+ import { ParsedComponent } from '../parser/index.js';
2
+ import { CodeSmell, DetectorConfig } from '../types/index.js';
3
+ /**
4
+ * Detects .map() calls that render JSX without a key prop
5
+ */
6
+ export declare function detectMissingKeys(component: ParsedComponent, filePath: string, sourceCode: string, config?: DetectorConfig): CodeSmell[];
7
+ //# sourceMappingURL=missingKey.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"missingKey.d.ts","sourceRoot":"","sources":["../../src/detectors/missingKey.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAkB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAkB,MAAM,mBAAmB,CAAC;AAE9E;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,cAA+B,GACtC,SAAS,EAAE,CA4Fb"}
@@ -0,0 +1,93 @@
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 .map() calls that render JSX without a key prop
6
+ */
7
+ export function detectMissingKeys(component, filePath, sourceCode, config = DEFAULT_CONFIG) {
8
+ if (!config.checkMissingKeys)
9
+ return [];
10
+ const smells = [];
11
+ component.path.traverse({
12
+ CallExpression(path) {
13
+ // Check if it's a .map() call
14
+ if (!t.isMemberExpression(path.node.callee))
15
+ return;
16
+ const prop = path.node.callee.property;
17
+ if (!t.isIdentifier(prop) || prop.name !== 'map')
18
+ return;
19
+ // Check if the callback returns JSX
20
+ const callback = path.node.arguments[0];
21
+ if (!callback)
22
+ return;
23
+ let callbackBody = null;
24
+ if (t.isArrowFunctionExpression(callback)) {
25
+ callbackBody = callback.body;
26
+ }
27
+ else if (t.isFunctionExpression(callback)) {
28
+ callbackBody = callback.body;
29
+ }
30
+ if (!callbackBody)
31
+ return;
32
+ // Find JSX elements in the callback
33
+ let hasJSX = false;
34
+ let hasKey = false;
35
+ let jsxLine = path.node.loc?.start.line || 0;
36
+ const checkForKey = (node) => {
37
+ if (t.isJSXElement(node)) {
38
+ hasJSX = true;
39
+ jsxLine = node.loc?.start.line || jsxLine;
40
+ // Check if the opening element has a key prop
41
+ const openingElement = node.openingElement;
42
+ const keyAttr = openingElement.attributes.find(attr => {
43
+ if (t.isJSXAttribute(attr)) {
44
+ return t.isJSXIdentifier(attr.name) && attr.name.name === 'key';
45
+ }
46
+ return false;
47
+ });
48
+ if (keyAttr) {
49
+ hasKey = true;
50
+ }
51
+ }
52
+ else if (t.isJSXFragment(node)) {
53
+ hasJSX = true;
54
+ // Fragments can't have keys directly, so check children
55
+ }
56
+ };
57
+ // Traverse the callback body to find JSX
58
+ if (t.isJSXElement(callbackBody) || t.isJSXFragment(callbackBody)) {
59
+ checkForKey(callbackBody);
60
+ }
61
+ else if (t.isBlockStatement(callbackBody)) {
62
+ path.traverse({
63
+ ReturnStatement(returnPath) {
64
+ if (returnPath.node.argument) {
65
+ checkForKey(returnPath.node.argument);
66
+ }
67
+ },
68
+ JSXElement(jsxPath) {
69
+ // Only check top-level JSX in the map callback
70
+ if (jsxPath.parent === callbackBody ||
71
+ (t.isReturnStatement(jsxPath.parent) && jsxPath.parent.argument === jsxPath.node)) {
72
+ checkForKey(jsxPath.node);
73
+ }
74
+ },
75
+ });
76
+ }
77
+ if (hasJSX && !hasKey) {
78
+ const loc = path.node.loc;
79
+ smells.push({
80
+ type: 'missing-key',
81
+ severity: 'error',
82
+ message: `Missing "key" prop in .map() that renders JSX in "${component.name}"`,
83
+ file: filePath,
84
+ line: jsxLine || loc?.start.line || 0,
85
+ column: loc?.start.column || 0,
86
+ suggestion: 'Add a unique "key" prop to the root element: <Element key={item.id}>',
87
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
88
+ });
89
+ }
90
+ },
91
+ });
92
+ return smells;
93
+ }
@@ -0,0 +1,7 @@
1
+ import { ParsedComponent } from '../parser/index.js';
2
+ import { CodeSmell, DetectorConfig } from '../types/index.js';
3
+ /**
4
+ * Detects deeply nested ternary expressions (complex conditional rendering)
5
+ */
6
+ export declare function detectNestedTernaries(component: ParsedComponent, filePath: string, sourceCode: string, config?: DetectorConfig): CodeSmell[];
7
+ //# sourceMappingURL=nestedTernary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nestedTernary.d.ts","sourceRoot":"","sources":["../../src/detectors/nestedTernary.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAkB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAkB,MAAM,mBAAmB,CAAC;AAE9E;;GAEG;AACH,wBAAgB,qBAAqB,CACnC,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,cAA+B,GACtC,SAAS,EAAE,CA4Bb"}
@@ -0,0 +1,58 @@
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 deeply nested ternary expressions (complex conditional rendering)
6
+ */
7
+ export function detectNestedTernaries(component, filePath, sourceCode, config = DEFAULT_CONFIG) {
8
+ const smells = [];
9
+ const maxDepth = config.maxTernaryDepth;
10
+ component.path.traverse({
11
+ ConditionalExpression(path) {
12
+ // Only check the outermost ternary
13
+ if (isNestedInTernary(path))
14
+ return;
15
+ const depth = getTernaryDepth(path.node);
16
+ if (depth > maxDepth) {
17
+ const loc = path.node.loc;
18
+ smells.push({
19
+ type: 'nested-ternary',
20
+ severity: depth > maxDepth + 1 ? 'warning' : 'info',
21
+ message: `Nested ternary expression (depth: ${depth}) in "${component.name}"`,
22
+ file: filePath,
23
+ line: loc?.start.line || 0,
24
+ column: loc?.start.column || 0,
25
+ suggestion: `Refactor to use if/else, switch, or extract into separate components. Max recommended depth: ${maxDepth}`,
26
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
27
+ });
28
+ }
29
+ },
30
+ });
31
+ return smells;
32
+ }
33
+ /**
34
+ * Check if a ternary is nested inside another ternary
35
+ */
36
+ function isNestedInTernary(path) {
37
+ let current = path.parentPath;
38
+ while (current) {
39
+ if (t.isConditionalExpression(current.node)) {
40
+ return true;
41
+ }
42
+ current = current.parentPath;
43
+ }
44
+ return false;
45
+ }
46
+ /**
47
+ * Calculate the depth of nested ternary expressions
48
+ */
49
+ function getTernaryDepth(node) {
50
+ let maxChildDepth = 0;
51
+ if (t.isConditionalExpression(node.consequent)) {
52
+ maxChildDepth = Math.max(maxChildDepth, getTernaryDepth(node.consequent));
53
+ }
54
+ if (t.isConditionalExpression(node.alternate)) {
55
+ maxChildDepth = Math.max(maxChildDepth, getTernaryDepth(node.alternate));
56
+ }
57
+ return 1 + maxChildDepth;
58
+ }
@@ -0,0 +1,11 @@
1
+ import { ParsedComponent } from '../parser/index.js';
2
+ import { CodeSmell, DetectorConfig } from '../types/index.js';
3
+ /**
4
+ * Detects Next.js-specific code smells:
5
+ * - Missing 'use client' / 'use server' directives
6
+ * - Unoptimized images (using <img> instead of next/image)
7
+ * - Router misuse patterns
8
+ * - Missing metadata exports
9
+ */
10
+ export declare function detectNextjsIssues(component: ParsedComponent, filePath: string, sourceCode: string, config?: DetectorConfig, imports?: string[]): CodeSmell[];
11
+ //# sourceMappingURL=nextjs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nextjs.d.ts","sourceRoot":"","sources":["../../src/detectors/nextjs.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,eAAe,EAAkB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAkB,MAAM,mBAAmB,CAAC;AAE9E;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,cAA+B,EACvC,OAAO,GAAE,MAAM,EAAO,GACrB,SAAS,EAAE,CA0Gb"}
@@ -0,0 +1,103 @@
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 Next.js-specific code smells:
6
+ * - Missing 'use client' / 'use server' directives
7
+ * - Unoptimized images (using <img> instead of next/image)
8
+ * - Router misuse patterns
9
+ * - Missing metadata exports
10
+ */
11
+ export function detectNextjsIssues(component, filePath, sourceCode, config = DEFAULT_CONFIG, imports = []) {
12
+ if (!config.checkNextjs)
13
+ return [];
14
+ // Only run on Next.js projects (check for next imports)
15
+ const isNextProject = imports.some(imp => imp.includes('next/') || imp.includes('next'));
16
+ // Also check file path patterns
17
+ const isAppRouter = filePath.includes('/app/') &&
18
+ (filePath.endsWith('page.tsx') || filePath.endsWith('page.jsx') ||
19
+ filePath.endsWith('layout.tsx') || filePath.endsWith('layout.jsx'));
20
+ const smells = [];
21
+ // Check for unoptimized images (using <img> instead of next/image)
22
+ component.path.traverse({
23
+ JSXOpeningElement(path) {
24
+ if (t.isJSXIdentifier(path.node.name) && path.node.name.name === 'img') {
25
+ const loc = path.node.loc;
26
+ smells.push({
27
+ type: 'nextjs-image-unoptimized',
28
+ severity: 'warning',
29
+ message: `Using native <img> instead of next/image in "${component.name}"`,
30
+ file: filePath,
31
+ line: loc?.start.line || 0,
32
+ column: loc?.start.column || 0,
33
+ suggestion: 'Use next/image for automatic image optimization: import Image from "next/image"',
34
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
35
+ });
36
+ }
37
+ },
38
+ });
39
+ // Check for client-side hooks in server components (App Router)
40
+ if (isAppRouter && !sourceCode.includes("'use client'") && !sourceCode.includes('"use client"')) {
41
+ const clientHooks = ['useState', 'useEffect', 'useContext', 'useReducer', 'useRef'];
42
+ const usedClientHooks = [];
43
+ component.path.traverse({
44
+ CallExpression(path) {
45
+ if (t.isIdentifier(path.node.callee) && clientHooks.includes(path.node.callee.name)) {
46
+ usedClientHooks.push(path.node.callee.name);
47
+ }
48
+ },
49
+ });
50
+ if (usedClientHooks.length > 0) {
51
+ const loc = component.node.loc;
52
+ smells.push({
53
+ type: 'nextjs-client-server-boundary',
54
+ severity: 'error',
55
+ message: `Client hooks (${usedClientHooks.join(', ')}) used without 'use client' directive in "${component.name}"`,
56
+ file: filePath,
57
+ line: loc?.start.line || 1,
58
+ column: 0,
59
+ suggestion: "Add 'use client' at the top of the file, or move client logic to a separate component",
60
+ codeSnippet: getCodeSnippet(sourceCode, 1),
61
+ });
62
+ }
63
+ }
64
+ // Check for missing metadata in page/layout files
65
+ if (isAppRouter && filePath.includes('page.')) {
66
+ // This would require checking exports, which needs file-level analysis
67
+ const hasMetadata = sourceCode.includes('export const metadata') ||
68
+ sourceCode.includes('export function generateMetadata');
69
+ if (!hasMetadata && component.name === 'default') {
70
+ smells.push({
71
+ type: 'nextjs-missing-metadata',
72
+ severity: 'info',
73
+ message: 'Page component missing metadata export',
74
+ file: filePath,
75
+ line: 1,
76
+ column: 0,
77
+ suggestion: 'Add metadata for SEO: export const metadata = { title: "...", description: "..." }',
78
+ });
79
+ }
80
+ }
81
+ // Check for router misuse (using window.location instead of next/router)
82
+ component.path.traverse({
83
+ MemberExpression(path) {
84
+ if (t.isIdentifier(path.node.object) &&
85
+ path.node.object.name === 'window' &&
86
+ t.isIdentifier(path.node.property) &&
87
+ path.node.property.name === 'location') {
88
+ const loc = path.node.loc;
89
+ smells.push({
90
+ type: 'nextjs-router-misuse',
91
+ severity: 'warning',
92
+ message: `Using window.location instead of Next.js router in "${component.name}"`,
93
+ file: filePath,
94
+ line: loc?.start.line || 0,
95
+ column: loc?.start.column || 0,
96
+ suggestion: 'Use next/navigation: import { useRouter } from "next/navigation"',
97
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
98
+ });
99
+ }
100
+ },
101
+ });
102
+ return smells;
103
+ }
@@ -0,0 +1,11 @@
1
+ import { ParsedComponent } from '../parser/index.js';
2
+ import { CodeSmell, DetectorConfig } from '../types/index.js';
3
+ /**
4
+ * Detects Node.js-specific code smells:
5
+ * - Callback hell (deeply nested callbacks)
6
+ * - Unhandled promise rejections
7
+ * - Synchronous I/O operations
8
+ * - Missing error handling
9
+ */
10
+ export declare function detectNodejsIssues(component: ParsedComponent, filePath: string, sourceCode: string, config?: DetectorConfig, imports?: string[]): CodeSmell[];
11
+ //# sourceMappingURL=nodejs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"nodejs.d.ts","sourceRoot":"","sources":["../../src/detectors/nodejs.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAkB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAkB,MAAM,mBAAmB,CAAC;AAE9E;;;;;;GAMG;AACH,wBAAgB,kBAAkB,CAChC,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,cAA+B,EACvC,OAAO,GAAE,MAAM,EAAO,GACrB,SAAS,EAAE,CA8Jb"}