react-code-smell-detector 1.0.1 → 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.
@@ -1 +1 @@
1
- {"version":3,"file":"analyzer.d.ts","sourceRoot":"","sources":["../src/analyzer.ts"],"names":[],"mappings":"AAgBA,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;AA2JD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAC"}
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"}
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, } from './detectors/index.js';
4
+ import { detectUseEffectOveruse, detectPropDrilling, analyzePropDrillingDepth, detectLargeComponent, detectUnmemoizedCalculations, detectMissingKeys, detectHooksRulesViolations, detectDependencyArrayIssues, detectNestedTernaries, detectDeadCode, detectMagicValues, detectNextjsIssues, detectReactNativeIssues, detectNodejsIssues, detectJavascriptIssues, detectTypescriptIssues, } 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;
@@ -68,6 +68,12 @@ function analyzeFile(parseResult, filePath, config) {
68
68
  smells.push(...detectNestedTernaries(component, filePath, sourceCode, config));
69
69
  smells.push(...detectDeadCode(component, filePath, sourceCode, config));
70
70
  smells.push(...detectMagicValues(component, filePath, sourceCode, config));
71
+ // Framework-specific detectors
72
+ smells.push(...detectNextjsIssues(component, filePath, sourceCode, config, imports));
73
+ smells.push(...detectReactNativeIssues(component, filePath, sourceCode, config, imports));
74
+ smells.push(...detectNodejsIssues(component, filePath, sourceCode, config, imports));
75
+ smells.push(...detectJavascriptIssues(component, filePath, sourceCode, config));
76
+ smells.push(...detectTypescriptIssues(component, filePath, sourceCode, config));
71
77
  });
72
78
  // Run cross-component analysis
73
79
  smells.push(...analyzePropDrillingDepth(components, filePath, sourceCode, config));
@@ -94,6 +100,30 @@ function calculateSummary(files) {
94
100
  'nested-ternary': 0,
95
101
  'dead-code': 0,
96
102
  'magic-value': 0,
103
+ // Next.js
104
+ 'nextjs-client-server-boundary': 0,
105
+ 'nextjs-missing-metadata': 0,
106
+ 'nextjs-image-unoptimized': 0,
107
+ 'nextjs-router-misuse': 0,
108
+ // React Native
109
+ 'rn-inline-style': 0,
110
+ 'rn-missing-accessibility': 0,
111
+ 'rn-performance-issue': 0,
112
+ // Node.js
113
+ 'nodejs-callback-hell': 0,
114
+ 'nodejs-unhandled-promise': 0,
115
+ 'nodejs-sync-io': 0,
116
+ 'nodejs-missing-error-handling': 0,
117
+ // JavaScript
118
+ 'js-var-usage': 0,
119
+ 'js-loose-equality': 0,
120
+ 'js-implicit-coercion': 0,
121
+ 'js-global-pollution': 0,
122
+ // TypeScript
123
+ 'ts-any-usage': 0,
124
+ 'ts-missing-return-type': 0,
125
+ 'ts-non-null-assertion': 0,
126
+ 'ts-type-assertion': 0,
97
127
  };
98
128
  const smellsBySeverity = {
99
129
  error: 0,
@@ -8,4 +8,9 @@ export { detectDependencyArrayIssues } from './dependencyArray.js';
8
8
  export { detectNestedTernaries } from './nestedTernary.js';
9
9
  export { detectDeadCode, detectUnusedImports } from './deadCode.js';
10
10
  export { detectMagicValues } from './magicValues.js';
11
+ export { detectNextjsIssues } from './nextjs.js';
12
+ export { detectReactNativeIssues } from './reactNative.js';
13
+ export { detectNodejsIssues } from './nodejs.js';
14
+ export { detectJavascriptIssues } from './javascript.js';
15
+ export { detectTypescriptIssues } from './typescript.js';
11
16
  //# 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"}
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"}
@@ -8,3 +8,9 @@ export { detectDependencyArrayIssues } from './dependencyArray.js';
8
8
  export { detectNestedTernaries } from './nestedTernary.js';
9
9
  export { detectDeadCode, detectUnusedImports } from './deadCode.js';
10
10
  export { detectMagicValues } from './magicValues.js';
11
+ // Framework-specific detectors
12
+ export { detectNextjsIssues } from './nextjs.js';
13
+ export { detectReactNativeIssues } from './reactNative.js';
14
+ export { detectNodejsIssues } from './nodejs.js';
15
+ export { detectJavascriptIssues } from './javascript.js';
16
+ export { detectTypescriptIssues } from './typescript.js';
@@ -0,0 +1,11 @@
1
+ import { ParsedComponent } from '../parser/index.js';
2
+ import { CodeSmell, DetectorConfig } from '../types/index.js';
3
+ /**
4
+ * Detects vanilla JavaScript code smells:
5
+ * - var usage (should use let/const)
6
+ * - Loose equality (== instead of ===)
7
+ * - Implicit type coercion
8
+ * - Global variable pollution
9
+ */
10
+ export declare function detectJavascriptIssues(component: ParsedComponent, filePath: string, sourceCode: string, config?: DetectorConfig): CodeSmell[];
11
+ //# sourceMappingURL=javascript.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"javascript.d.ts","sourceRoot":"","sources":["../../src/detectors/javascript.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,sBAAsB,CACpC,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,cAA+B,GACtC,SAAS,EAAE,CAwJb"}
@@ -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,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"}
@@ -0,0 +1,169 @@
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 Node.js-specific code smells:
6
+ * - Callback hell (deeply nested callbacks)
7
+ * - Unhandled promise rejections
8
+ * - Synchronous I/O operations
9
+ * - Missing error handling
10
+ */
11
+ export function detectNodejsIssues(component, filePath, sourceCode, config = DEFAULT_CONFIG, imports = []) {
12
+ if (!config.checkNodejs)
13
+ return [];
14
+ // Check if this looks like a Node.js file
15
+ const isNodeFile = imports.some(imp => imp.includes('fs') || imp.includes('path') || imp.includes('http') ||
16
+ imp.includes('express') || imp.includes('child_process') ||
17
+ imp.includes('crypto') || imp.includes('os') || imp.includes('stream')) || filePath.includes('.server.') || filePath.includes('/api/');
18
+ if (!isNodeFile)
19
+ return [];
20
+ const smells = [];
21
+ // Detect callback hell (nested callbacks > maxCallbackDepth)
22
+ component.path.traverse({
23
+ CallExpression(path) {
24
+ const depth = getCallbackDepth(path);
25
+ if (depth > config.maxCallbackDepth) {
26
+ const loc = path.node.loc;
27
+ smells.push({
28
+ type: 'nodejs-callback-hell',
29
+ severity: 'warning',
30
+ message: `Callback hell detected (depth: ${depth}) in "${component.name}"`,
31
+ file: filePath,
32
+ line: loc?.start.line || 0,
33
+ column: loc?.start.column || 0,
34
+ suggestion: 'Refactor to async/await or use Promise.all() for parallel operations',
35
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
36
+ });
37
+ }
38
+ },
39
+ });
40
+ // Detect unhandled promise rejections (Promise without .catch or try/catch)
41
+ component.path.traverse({
42
+ CallExpression(path) {
43
+ // Check for .then() without .catch()
44
+ if (t.isMemberExpression(path.node.callee) &&
45
+ t.isIdentifier(path.node.callee.property) &&
46
+ path.node.callee.property.name === 'then') {
47
+ // Check if followed by .catch() in chain
48
+ const parent = path.parent;
49
+ let hasCatch = false;
50
+ if (t.isMemberExpression(parent)) {
51
+ const prop = parent.property;
52
+ if (t.isIdentifier(prop) && prop.name === 'catch') {
53
+ hasCatch = true;
54
+ }
55
+ }
56
+ // Check if inside try block
57
+ let current = path;
58
+ while (current) {
59
+ if (t.isTryStatement(current.node)) {
60
+ hasCatch = true;
61
+ break;
62
+ }
63
+ current = current.parentPath;
64
+ }
65
+ if (!hasCatch) {
66
+ const loc = path.node.loc;
67
+ smells.push({
68
+ type: 'nodejs-unhandled-promise',
69
+ severity: 'warning',
70
+ message: `.then() without .catch() in "${component.name}"`,
71
+ file: filePath,
72
+ line: loc?.start.line || 0,
73
+ column: loc?.start.column || 0,
74
+ suggestion: 'Add .catch() to handle rejections, or use try/catch with async/await',
75
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
76
+ });
77
+ }
78
+ }
79
+ },
80
+ });
81
+ // Detect synchronous file operations
82
+ const syncMethods = ['readFileSync', 'writeFileSync', 'appendFileSync', 'readdirSync',
83
+ 'statSync', 'mkdirSync', 'rmdirSync', 'unlinkSync', 'existsSync'];
84
+ component.path.traverse({
85
+ CallExpression(path) {
86
+ const callee = path.node.callee;
87
+ if (t.isMemberExpression(callee) && t.isIdentifier(callee.property)) {
88
+ if (syncMethods.includes(callee.property.name)) {
89
+ const loc = path.node.loc;
90
+ smells.push({
91
+ type: 'nodejs-sync-io',
92
+ severity: 'warning',
93
+ message: `Synchronous file operation "${callee.property.name}" blocks event loop`,
94
+ file: filePath,
95
+ line: loc?.start.line || 0,
96
+ column: loc?.start.column || 0,
97
+ suggestion: `Use async version: ${callee.property.name.replace('Sync', '')} with await or promises`,
98
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
99
+ });
100
+ }
101
+ }
102
+ // Direct function call
103
+ if (t.isIdentifier(callee) && syncMethods.includes(callee.name)) {
104
+ const loc = path.node.loc;
105
+ smells.push({
106
+ type: 'nodejs-sync-io',
107
+ severity: 'warning',
108
+ message: `Synchronous file operation "${callee.name}" blocks event loop`,
109
+ file: filePath,
110
+ line: loc?.start.line || 0,
111
+ column: loc?.start.column || 0,
112
+ suggestion: `Use async version: ${callee.name.replace('Sync', '')} with await or promises`,
113
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
114
+ });
115
+ }
116
+ },
117
+ });
118
+ // Detect missing error handling in async functions
119
+ component.path.traverse({
120
+ AwaitExpression(path) {
121
+ // Check if inside try block
122
+ let insideTry = false;
123
+ let current = path;
124
+ while (current) {
125
+ if (t.isTryStatement(current.node)) {
126
+ insideTry = true;
127
+ break;
128
+ }
129
+ // Stop at function boundary
130
+ if (t.isFunction(current.node))
131
+ break;
132
+ current = current.parentPath;
133
+ }
134
+ if (!insideTry) {
135
+ // Check if the parent function has error handling at call site
136
+ // This is a simplified check - in practice you'd want more context
137
+ const loc = path.node.loc;
138
+ smells.push({
139
+ type: 'nodejs-missing-error-handling',
140
+ severity: 'info',
141
+ message: `await without try/catch may cause unhandled rejections`,
142
+ file: filePath,
143
+ line: loc?.start.line || 0,
144
+ column: loc?.start.column || 0,
145
+ suggestion: 'Wrap await in try/catch or handle errors at the call site',
146
+ codeSnippet: getCodeSnippet(sourceCode, loc?.start.line || 0),
147
+ });
148
+ }
149
+ },
150
+ });
151
+ return smells;
152
+ }
153
+ /**
154
+ * Calculate the depth of nested callbacks
155
+ */
156
+ function getCallbackDepth(path) {
157
+ let depth = 0;
158
+ let current = path;
159
+ while (current) {
160
+ const node = current.node;
161
+ // Count function expressions that are arguments to calls
162
+ if ((t.isFunctionExpression(node) || t.isArrowFunctionExpression(node)) &&
163
+ t.isCallExpression(current.parent)) {
164
+ depth++;
165
+ }
166
+ current = current.parentPath;
167
+ }
168
+ return depth;
169
+ }
@@ -0,0 +1,10 @@
1
+ import { ParsedComponent } from '../parser/index.js';
2
+ import { CodeSmell, DetectorConfig } from '../types/index.js';
3
+ /**
4
+ * Detects React Native-specific code smells:
5
+ * - Inline styles instead of StyleSheet
6
+ * - Missing accessibility props
7
+ * - Performance anti-patterns
8
+ */
9
+ export declare function detectReactNativeIssues(component: ParsedComponent, filePath: string, sourceCode: string, config?: DetectorConfig, imports?: string[]): CodeSmell[];
10
+ //# sourceMappingURL=reactNative.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reactNative.d.ts","sourceRoot":"","sources":["../../src/detectors/reactNative.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAkB,MAAM,oBAAoB,CAAC;AACrE,OAAO,EAAE,SAAS,EAAE,cAAc,EAAkB,MAAM,mBAAmB,CAAC;AAE9E;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,SAAS,EAAE,eAAe,EAC1B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,MAAM,GAAE,cAA+B,EACvC,OAAO,GAAE,MAAM,EAAO,GACrB,SAAS,EAAE,CAwIb"}