code-simplifier 1.0.0 → 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 +56 -1
- package/bin/code-simplifier.js +73 -1
- package/cypress.config.js +45 -0
- package/jest.config.js +46 -0
- package/lib/ai-auto-fix.js +829 -0
- package/lib/ai-code-review.js +705 -0
- package/lib/ai-knowledge-base.js +757 -0
- package/lib/ai-quality-analyzer.js +1095 -0
- package/lib/ai-refactor-advisor.js +853 -0
- package/lib/ai-trend-analyzer.js +674 -0
- package/lib/auto-fix.js +382 -0
- package/lib/eslint-integration.js +328 -0
- package/lib/git-hooks.js +353 -0
- package/lib/master.js +117 -0
- package/lib/multi-language-analyzer.js +397 -0
- package/lib/ralph-integration.js +541 -0
- package/package.json +87 -61
|
@@ -0,0 +1,1095 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AI-Powered Code Quality Analyzer
|
|
5
|
+
* Supports analyzing code quality across 11+ languages using AI
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs-extra')
|
|
9
|
+
const path = require('path')
|
|
10
|
+
const chalk = require('chalk')
|
|
11
|
+
const ora = require('ora')
|
|
12
|
+
const { OpenAI } = require('openai')
|
|
13
|
+
const Anthropic = require('@anthropic-ai/sdk')
|
|
14
|
+
|
|
15
|
+
// Code smell patterns for different languages
|
|
16
|
+
const CODE_SMELLS = {
|
|
17
|
+
// Code Structure Issues
|
|
18
|
+
longFunction: {
|
|
19
|
+
name: 'Long Function',
|
|
20
|
+
description: 'Function exceeds recommended length',
|
|
21
|
+
severity: 'warning',
|
|
22
|
+
category: 'structure'
|
|
23
|
+
},
|
|
24
|
+
largeClass: {
|
|
25
|
+
name: 'Large Class',
|
|
26
|
+
description: 'Class has too many responsibilities',
|
|
27
|
+
severity: 'warning',
|
|
28
|
+
category: 'structure'
|
|
29
|
+
},
|
|
30
|
+
tooManyParameters: {
|
|
31
|
+
name: 'Too Many Parameters',
|
|
32
|
+
description: 'Function has excessive parameters',
|
|
33
|
+
severity: 'warning',
|
|
34
|
+
category: 'structure'
|
|
35
|
+
},
|
|
36
|
+
duplicateCode: {
|
|
37
|
+
name: 'Duplicate Code',
|
|
38
|
+
description: 'Code is duplicated across multiple locations',
|
|
39
|
+
severity: 'error',
|
|
40
|
+
category: 'maintainability'
|
|
41
|
+
},
|
|
42
|
+
|
|
43
|
+
// Naming Issues
|
|
44
|
+
badVariableNames: {
|
|
45
|
+
name: 'Poor Variable Names',
|
|
46
|
+
description: 'Variable names are not descriptive',
|
|
47
|
+
severity: 'warning',
|
|
48
|
+
category: 'naming'
|
|
49
|
+
},
|
|
50
|
+
badFunctionNames: {
|
|
51
|
+
name: 'Poor Function Names',
|
|
52
|
+
description: 'Function names are not descriptive',
|
|
53
|
+
severity: 'warning',
|
|
54
|
+
category: 'naming'
|
|
55
|
+
},
|
|
56
|
+
magicNumbers: {
|
|
57
|
+
name: 'Magic Numbers',
|
|
58
|
+
description: 'Unnamed numerical constants in code',
|
|
59
|
+
severity: 'warning',
|
|
60
|
+
category: 'maintainability'
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// Complexity Issues
|
|
64
|
+
highComplexity: {
|
|
65
|
+
name: 'High Cyclomatic Complexity',
|
|
66
|
+
description: 'Function has too many decision points',
|
|
67
|
+
severity: 'error',
|
|
68
|
+
category: 'complexity'
|
|
69
|
+
},
|
|
70
|
+
nestedConditionals: {
|
|
71
|
+
name: 'Deeply Nested Conditionals',
|
|
72
|
+
description: 'Excessive nesting reduces readability',
|
|
73
|
+
severity: 'warning',
|
|
74
|
+
category: 'complexity'
|
|
75
|
+
},
|
|
76
|
+
arrowAntiPattern: {
|
|
77
|
+
name: 'Arrow Anti-Pattern',
|
|
78
|
+
description: 'Excessive callback nesting',
|
|
79
|
+
severity: 'warning',
|
|
80
|
+
category: 'complexity'
|
|
81
|
+
},
|
|
82
|
+
|
|
83
|
+
// Code Smells
|
|
84
|
+
godObject: {
|
|
85
|
+
name: 'God Object',
|
|
86
|
+
description: 'Class knows too much or does too much',
|
|
87
|
+
severity: 'error',
|
|
88
|
+
category: 'design'
|
|
89
|
+
},
|
|
90
|
+
featureEnvy: {
|
|
91
|
+
name: 'Feature Envy',
|
|
92
|
+
description: 'Class uses methods of another class excessively',
|
|
93
|
+
severity: 'warning',
|
|
94
|
+
category: 'design'
|
|
95
|
+
},
|
|
96
|
+
dataClass: {
|
|
97
|
+
name: 'Data Class',
|
|
98
|
+
description: 'Class with only data and no behavior',
|
|
99
|
+
severity: 'warning',
|
|
100
|
+
category: 'design'
|
|
101
|
+
},
|
|
102
|
+
deadCode: {
|
|
103
|
+
name: 'Dead Code',
|
|
104
|
+
description: 'Code that is never executed',
|
|
105
|
+
severity: 'warning',
|
|
106
|
+
category: 'maintainability'
|
|
107
|
+
},
|
|
108
|
+
commentedCode: {
|
|
109
|
+
name: 'Commented Code',
|
|
110
|
+
description: 'Old commented-out code',
|
|
111
|
+
severity: 'info',
|
|
112
|
+
category: 'maintainability'
|
|
113
|
+
},
|
|
114
|
+
|
|
115
|
+
// Performance Issues
|
|
116
|
+
inefficientLoop: {
|
|
117
|
+
name: 'Inefficient Loop',
|
|
118
|
+
description: 'Suboptimal loop implementation',
|
|
119
|
+
severity: 'warning',
|
|
120
|
+
category: 'performance'
|
|
121
|
+
},
|
|
122
|
+
stringConcatenation: {
|
|
123
|
+
name: 'String Concatenation in Loop',
|
|
124
|
+
description: 'Inefficient string concatenation',
|
|
125
|
+
severity: 'warning',
|
|
126
|
+
category: 'performance'
|
|
127
|
+
},
|
|
128
|
+
memoryLeak: {
|
|
129
|
+
name: 'Potential Memory Leak',
|
|
130
|
+
description: 'May cause memory leaks',
|
|
131
|
+
severity: 'error',
|
|
132
|
+
category: 'performance'
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
// Security Issues
|
|
136
|
+
sqlInjection: {
|
|
137
|
+
name: 'SQL Injection Risk',
|
|
138
|
+
description: 'Potential SQL injection vulnerability',
|
|
139
|
+
severity: 'error',
|
|
140
|
+
category: 'security'
|
|
141
|
+
},
|
|
142
|
+
xssRisk: {
|
|
143
|
+
name: 'XSS Risk',
|
|
144
|
+
description: 'Potential cross-site scripting vulnerability',
|
|
145
|
+
severity: 'error',
|
|
146
|
+
category: 'security'
|
|
147
|
+
},
|
|
148
|
+
hardcodedSecrets: {
|
|
149
|
+
name: 'Hardcoded Secrets',
|
|
150
|
+
description: 'Credentials or keys in source code',
|
|
151
|
+
severity: 'error',
|
|
152
|
+
category: 'security'
|
|
153
|
+
},
|
|
154
|
+
|
|
155
|
+
// Best Practices
|
|
156
|
+
missingErrorHandling: {
|
|
157
|
+
name: 'Missing Error Handling',
|
|
158
|
+
description: 'No error handling for risky operations',
|
|
159
|
+
severity: 'warning',
|
|
160
|
+
category: 'best-practice'
|
|
161
|
+
},
|
|
162
|
+
missingValidation: {
|
|
163
|
+
name: 'Missing Input Validation',
|
|
164
|
+
description: 'User input not validated',
|
|
165
|
+
severity: 'warning',
|
|
166
|
+
category: 'best-practice'
|
|
167
|
+
},
|
|
168
|
+
missingDocumentation: {
|
|
169
|
+
name: 'Missing Documentation',
|
|
170
|
+
description: 'Function/class lacks documentation',
|
|
171
|
+
severity: 'info',
|
|
172
|
+
category: 'documentation'
|
|
173
|
+
},
|
|
174
|
+
inconsistentStyle: {
|
|
175
|
+
name: 'Inconsistent Code Style',
|
|
176
|
+
description: 'Code style inconsistent with project',
|
|
177
|
+
severity: 'info',
|
|
178
|
+
category: 'style'
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const SUPPORTED_LANGUAGES = [
|
|
183
|
+
'javascript',
|
|
184
|
+
'typescript',
|
|
185
|
+
'python',
|
|
186
|
+
'java',
|
|
187
|
+
'csharp',
|
|
188
|
+
'cpp',
|
|
189
|
+
'php',
|
|
190
|
+
'ruby',
|
|
191
|
+
'go',
|
|
192
|
+
'rust',
|
|
193
|
+
'swift',
|
|
194
|
+
'kotlin'
|
|
195
|
+
]
|
|
196
|
+
|
|
197
|
+
class AIQualityAnalyzer {
|
|
198
|
+
constructor() {
|
|
199
|
+
this.openai = null
|
|
200
|
+
this.anthropic = null
|
|
201
|
+
this.apiProvider = process.env.AI_PROVIDER || 'openai'
|
|
202
|
+
this.apiKey = null
|
|
203
|
+
|
|
204
|
+
// Initialize API clients
|
|
205
|
+
this.initializeAPIs()
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Initialize API clients
|
|
210
|
+
*/
|
|
211
|
+
initializeAPIs() {
|
|
212
|
+
try {
|
|
213
|
+
const openaiKey = process.env.OPENAI_API_KEY
|
|
214
|
+
const anthropicKey = process.env.ANTHROPIC_API_KEY
|
|
215
|
+
|
|
216
|
+
if (openaiKey && (this.apiProvider === 'openai' || !anthropicKey)) {
|
|
217
|
+
this.openai = new OpenAI({ apiKey: openaiKey })
|
|
218
|
+
this.apiKey = openaiKey
|
|
219
|
+
this.currentProvider = 'openai'
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (anthropicKey && (this.apiProvider === 'anthropic' || !openaiKey)) {
|
|
223
|
+
this.anthropic = new Anthropic({ apiKey: anthropicKey })
|
|
224
|
+
this.apiKey = anthropicKey
|
|
225
|
+
this.currentProvider = 'anthropic'
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!this.openai && !this.anthropic) {
|
|
229
|
+
console.warn(chalk.yellow('⚠️ No AI API keys found. Using mock mode for testing.'))
|
|
230
|
+
this.mockMode = true
|
|
231
|
+
}
|
|
232
|
+
} catch (error) {
|
|
233
|
+
console.warn(chalk.yellow('⚠️ Failed to initialize AI APIs. Using mock mode.'))
|
|
234
|
+
this.mockMode = true
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Analyze a single file
|
|
240
|
+
*/
|
|
241
|
+
async analyzeFile(filePath, options = {}) {
|
|
242
|
+
try {
|
|
243
|
+
const content = await fs.readFile(filePath, 'utf8')
|
|
244
|
+
const language = this.detectLanguage(filePath)
|
|
245
|
+
|
|
246
|
+
if (!language || !SUPPORTED_LANGUAGES.includes(language)) {
|
|
247
|
+
throw new Error(`Unsupported or unknown language for file: ${filePath}`)
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const issues = this.detectCodeSmells(content, language, filePath)
|
|
251
|
+
|
|
252
|
+
return {
|
|
253
|
+
file: filePath,
|
|
254
|
+
language,
|
|
255
|
+
issues,
|
|
256
|
+
summary: {
|
|
257
|
+
total: issues.length,
|
|
258
|
+
errors: issues.filter(i => i.severity === 'error').length,
|
|
259
|
+
warnings: issues.filter(i => i.severity === 'warning').length,
|
|
260
|
+
info: issues.filter(i => i.severity === 'info').length
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
} catch (error) {
|
|
264
|
+
throw new Error(`Failed to analyze file ${filePath}: ${error.message}`)
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Analyze multiple files
|
|
270
|
+
*/
|
|
271
|
+
async analyzeFiles(filePaths, options = {}) {
|
|
272
|
+
const spinner = ora(chalk.blue('Analyzing files with AI...')).start()
|
|
273
|
+
const results = []
|
|
274
|
+
const errors = []
|
|
275
|
+
|
|
276
|
+
try {
|
|
277
|
+
for (const filePath of filePaths) {
|
|
278
|
+
try {
|
|
279
|
+
const result = await this.analyzeFile(filePath, options)
|
|
280
|
+
results.push(result)
|
|
281
|
+
spinner.text = `Analyzed ${results.length}/${filePaths.length} files`
|
|
282
|
+
} catch (error) {
|
|
283
|
+
errors.push({ file: filePath, error: error.message })
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
spinner.succeed(chalk.green(`Analysis complete: ${results.length} files analyzed`))
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
results,
|
|
291
|
+
errors,
|
|
292
|
+
summary: this.generateSummary(results)
|
|
293
|
+
}
|
|
294
|
+
} catch (error) {
|
|
295
|
+
spinner.fail(chalk.red('Analysis failed'))
|
|
296
|
+
throw error
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* Detect programming language from file extension
|
|
302
|
+
*/
|
|
303
|
+
detectLanguage(filePath) {
|
|
304
|
+
const ext = path.extname(filePath).toLowerCase()
|
|
305
|
+
const languageMap = {
|
|
306
|
+
'.js': 'javascript',
|
|
307
|
+
'.jsx': 'javascript',
|
|
308
|
+
'.ts': 'typescript',
|
|
309
|
+
'.tsx': 'typescript',
|
|
310
|
+
'.py': 'python',
|
|
311
|
+
'.java': 'java',
|
|
312
|
+
'.cs': 'csharp',
|
|
313
|
+
'.cpp': 'cpp',
|
|
314
|
+
'.cc': 'cpp',
|
|
315
|
+
'.cxx': 'cpp',
|
|
316
|
+
'.h': 'cpp',
|
|
317
|
+
'.hpp': 'cpp',
|
|
318
|
+
'.php': 'php',
|
|
319
|
+
'.rb': 'ruby',
|
|
320
|
+
'.go': 'go',
|
|
321
|
+
'.rs': 'rust',
|
|
322
|
+
'.swift': 'swift',
|
|
323
|
+
'.kt': 'kotlin',
|
|
324
|
+
'.kts': 'kotlin'
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
return languageMap[ext]
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Detect code smells in code
|
|
332
|
+
*/
|
|
333
|
+
detectCodeSmells(code, language, filePath) {
|
|
334
|
+
const issues = []
|
|
335
|
+
const lines = code.split('\n')
|
|
336
|
+
|
|
337
|
+
// Check for various code smells
|
|
338
|
+
issues.push(...this.checkLongFunctions(code, language, filePath))
|
|
339
|
+
issues.push(...this.checkLargeClasses(code, language, filePath))
|
|
340
|
+
issues.push(...this.checkTooManyParameters(code, language, filePath))
|
|
341
|
+
issues.push(...this.checkMagicNumbers(code, language, filePath))
|
|
342
|
+
issues.push(...this.checkHighComplexity(code, language, filePath))
|
|
343
|
+
issues.push(...this.checkNestedConditionals(code, language, filePath))
|
|
344
|
+
issues.push(...this.checkDeadCode(code, language, filePath))
|
|
345
|
+
issues.push(...this.checkCommentedCode(code, language, filePath))
|
|
346
|
+
issues.push(...this.checkSecurityIssues(code, language, filePath))
|
|
347
|
+
issues.push(...this.checkPerformanceIssues(code, language, filePath))
|
|
348
|
+
issues.push(...this.checkNamingIssues(code, language, filePath))
|
|
349
|
+
issues.push(...this.checkBestPractices(code, language, filePath))
|
|
350
|
+
|
|
351
|
+
return issues
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Check for long functions
|
|
356
|
+
*/
|
|
357
|
+
checkLongFunctions(code, language, filePath) {
|
|
358
|
+
const issues = []
|
|
359
|
+
const lines = code.split('\n')
|
|
360
|
+
|
|
361
|
+
// Detect function definitions and their lengths
|
|
362
|
+
const functionRegex = this.getFunctionRegex(language)
|
|
363
|
+
const functionRegexNoGlobal = new RegExp(functionRegex.source, functionRegex.flags.replace('g', ''))
|
|
364
|
+
let match
|
|
365
|
+
|
|
366
|
+
while ((match = functionRegex.exec(code)) !== null) {
|
|
367
|
+
const functionStart = code.substring(0, match.index).split('\n').length
|
|
368
|
+
const functionBody = this.extractFunctionBody(code, match.index, language)
|
|
369
|
+
const functionLines = functionBody.split('\n').length
|
|
370
|
+
|
|
371
|
+
if (functionLines > 50) {
|
|
372
|
+
issues.push({
|
|
373
|
+
type: 'longFunction',
|
|
374
|
+
name: CODE_SMELLS.longFunction.name,
|
|
375
|
+
severity: CODE_SMELLS.longFunction.severity,
|
|
376
|
+
category: CODE_SMELLS.longFunction.category,
|
|
377
|
+
line: functionStart,
|
|
378
|
+
message: `Function is ${functionLines} lines (recommended: < 50)`,
|
|
379
|
+
fix: 'Break down into smaller, more focused functions'
|
|
380
|
+
})
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
return issues
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Check for large classes
|
|
389
|
+
*/
|
|
390
|
+
checkLargeClasses(code, language, filePath) {
|
|
391
|
+
const issues = []
|
|
392
|
+
const lines = code.split('\n')
|
|
393
|
+
|
|
394
|
+
if (language === 'java' || language === 'csharp') {
|
|
395
|
+
const classRegex = /(?:class|interface)\s+(\w+)/g
|
|
396
|
+
let match
|
|
397
|
+
|
|
398
|
+
while ((match = classRegex.exec(code)) !== null) {
|
|
399
|
+
const classStart = code.substring(0, match.index).split('\n').length
|
|
400
|
+
// Count lines until next class or end of file
|
|
401
|
+
const afterClass = code.substring(match.index)
|
|
402
|
+
const classLines = afterClass.split('\n').findIndex((line, idx) =>
|
|
403
|
+
idx > 0 && (line.match(/^\s*(class|interface)\s+\w+/) || line.match(/^\s*}/))
|
|
404
|
+
)
|
|
405
|
+
|
|
406
|
+
if (classLines > 200) {
|
|
407
|
+
issues.push({
|
|
408
|
+
type: 'largeClass',
|
|
409
|
+
name: CODE_SMELLS.largeClass.name,
|
|
410
|
+
severity: CODE_SMELLS.largeClass.severity,
|
|
411
|
+
category: CODE_SMELLS.largeClass.category,
|
|
412
|
+
line: classStart,
|
|
413
|
+
message: `Class has ~${classLines} lines (recommended: < 200)`,
|
|
414
|
+
fix: 'Split into smaller, cohesive classes'
|
|
415
|
+
})
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
return issues
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Check for too many parameters
|
|
425
|
+
*/
|
|
426
|
+
checkTooManyParameters(code, language, filePath) {
|
|
427
|
+
const issues = []
|
|
428
|
+
const functionRegex = this.getFunctionRegex(language)
|
|
429
|
+
const functionRegexNoGlobal = new RegExp(functionRegex.source, functionRegex.flags.replace('g', ''))
|
|
430
|
+
let match
|
|
431
|
+
|
|
432
|
+
while ((match = functionRegex.exec(code)) !== null) {
|
|
433
|
+
const functionStartLine = code.substring(0, match.index).split('\n').length
|
|
434
|
+
const codeFromFunction = code.substring(match.index)
|
|
435
|
+
const firstLine = codeFromFunction.split('\n')[0]
|
|
436
|
+
|
|
437
|
+
// Extract parameter list from the first line
|
|
438
|
+
const paramMatch = firstLine.match(/\(([^)]*)\)/)
|
|
439
|
+
|
|
440
|
+
if (paramMatch) {
|
|
441
|
+
const params = paramMatch[1].trim()
|
|
442
|
+
// Handle destructuring parameters and default values
|
|
443
|
+
const paramList = params.split(',')
|
|
444
|
+
.map(p => p.trim())
|
|
445
|
+
.filter(p => p.length > 0)
|
|
446
|
+
|
|
447
|
+
if (paramList.length > 5) {
|
|
448
|
+
issues.push({
|
|
449
|
+
type: 'tooManyParameters',
|
|
450
|
+
name: CODE_SMELLS.tooManyParameters.name,
|
|
451
|
+
severity: CODE_SMELLS.tooManyParameters.severity,
|
|
452
|
+
category: CODE_SMELLS.tooManyParameters.category,
|
|
453
|
+
line: functionStartLine,
|
|
454
|
+
message: `Function has ${paramList.length} parameters (recommended: < 5)`,
|
|
455
|
+
fix: 'Use parameter object or break down function'
|
|
456
|
+
})
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
return issues
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Check for magic numbers
|
|
466
|
+
*/
|
|
467
|
+
checkMagicNumbers(code, language, filePath) {
|
|
468
|
+
const issues = []
|
|
469
|
+
const lines = code.split('\n')
|
|
470
|
+
|
|
471
|
+
lines.forEach((line, index) => {
|
|
472
|
+
// Skip if line already has issue
|
|
473
|
+
if (line.includes('//') || line.includes('#')) return
|
|
474
|
+
|
|
475
|
+
// Find standalone numbers (not in strings or comments)
|
|
476
|
+
const numberRegex = /\b(\d{4,})\b/g
|
|
477
|
+
let match
|
|
478
|
+
|
|
479
|
+
while ((match = numberRegex.exec(line)) !== null) {
|
|
480
|
+
const number = match[1]
|
|
481
|
+
|
|
482
|
+
// Skip version numbers, dates, etc.
|
|
483
|
+
if (!['2020', '2021', '2022', '2023', '2024'].includes(number)) {
|
|
484
|
+
issues.push({
|
|
485
|
+
type: 'magicNumbers',
|
|
486
|
+
name: CODE_SMELLS.magicNumbers.name,
|
|
487
|
+
severity: CODE_SMELLS.magicNumbers.severity,
|
|
488
|
+
category: CODE_SMELLS.magicNumbers.category,
|
|
489
|
+
line: index + 1,
|
|
490
|
+
message: `Magic number: ${number}`,
|
|
491
|
+
fix: 'Extract to a named constant'
|
|
492
|
+
})
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
})
|
|
496
|
+
|
|
497
|
+
return issues
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Check for high cyclomatic complexity
|
|
502
|
+
*/
|
|
503
|
+
checkHighComplexity(code, language, filePath) {
|
|
504
|
+
const issues = []
|
|
505
|
+
const lines = code.split('\n')
|
|
506
|
+
const functionRegex = this.getFunctionRegex(language)
|
|
507
|
+
const functionRegexNoGlobal = new RegExp(functionRegex.source, functionRegex.flags.replace('g', ''))
|
|
508
|
+
|
|
509
|
+
let inFunction = false
|
|
510
|
+
let functionStartLine = 0
|
|
511
|
+
let braceDepth = 0
|
|
512
|
+
let decisionPoints = 0
|
|
513
|
+
|
|
514
|
+
lines.forEach((line, index) => {
|
|
515
|
+
const trimmed = line.trim()
|
|
516
|
+
|
|
517
|
+
// Check for function start
|
|
518
|
+
if (functionRegexNoGlobal.test(line)) {
|
|
519
|
+
if (inFunction && decisionPoints > 10) {
|
|
520
|
+
issues.push({
|
|
521
|
+
type: 'highComplexity',
|
|
522
|
+
name: CODE_SMELLS.highComplexity.name,
|
|
523
|
+
severity: CODE_SMELLS.highComplexity.severity,
|
|
524
|
+
category: CODE_SMELLS.highComplexity.category,
|
|
525
|
+
line: functionStartLine,
|
|
526
|
+
message: `Cyclomatic complexity: ${decisionPoints} (recommended: < 10)`,
|
|
527
|
+
fix: 'Simplify logic by extracting methods or using early returns'
|
|
528
|
+
})
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
inFunction = true
|
|
532
|
+
functionStartLine = index + 1
|
|
533
|
+
braceDepth = 0
|
|
534
|
+
decisionPoints = 1 // Start with 1 for the function itself
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
if (inFunction) {
|
|
538
|
+
// Count braces to track function scope
|
|
539
|
+
for (const char of line) {
|
|
540
|
+
if (char === '{') braceDepth++
|
|
541
|
+
else if (char === '}') braceDepth--
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Count decision points
|
|
545
|
+
if (trimmed.includes('if ') || trimmed.includes('else if')) {
|
|
546
|
+
decisionPoints++
|
|
547
|
+
} else if (trimmed.includes('for ') || trimmed.includes('while ')) {
|
|
548
|
+
decisionPoints++
|
|
549
|
+
} else if (trimmed.includes('case ') || trimmed.includes('default:')) {
|
|
550
|
+
decisionPoints++
|
|
551
|
+
} else if (trimmed.includes('catch ')) {
|
|
552
|
+
decisionPoints++
|
|
553
|
+
} else if (trimmed.includes('&&')) {
|
|
554
|
+
decisionPoints++
|
|
555
|
+
} else if (trimmed.includes('||')) {
|
|
556
|
+
decisionPoints++
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
// Check for function end (when we exit the function's brace scope)
|
|
560
|
+
if (trimmed === '}' && braceDepth === 0) {
|
|
561
|
+
inFunction = false
|
|
562
|
+
if (decisionPoints > 10) {
|
|
563
|
+
issues.push({
|
|
564
|
+
type: 'highComplexity',
|
|
565
|
+
name: CODE_SMELLS.highComplexity.name,
|
|
566
|
+
severity: CODE_SMELLS.highComplexity.severity,
|
|
567
|
+
category: CODE_SMELLS.highComplexity.category,
|
|
568
|
+
line: functionStartLine,
|
|
569
|
+
message: `Cyclomatic complexity: ${decisionPoints} (recommended: < 10)`,
|
|
570
|
+
fix: 'Simplify logic by extracting methods or using early returns'
|
|
571
|
+
})
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
})
|
|
576
|
+
|
|
577
|
+
return issues
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Check for deeply nested conditionals
|
|
582
|
+
*/
|
|
583
|
+
checkNestedConditionals(code, language, filePath) {
|
|
584
|
+
const issues = []
|
|
585
|
+
const lines = code.split('\n')
|
|
586
|
+
|
|
587
|
+
lines.forEach((line, index) => {
|
|
588
|
+
const trimmed = line.trim()
|
|
589
|
+
const indentation = line.match(/^(\s*)/)[1].length
|
|
590
|
+
|
|
591
|
+
if ((trimmed.includes('if ') || trimmed.includes('for ') || trimmed.includes('while ')) &&
|
|
592
|
+
indentation > 16) { // More than 4 levels of nesting
|
|
593
|
+
issues.push({
|
|
594
|
+
type: 'nestedConditionals',
|
|
595
|
+
name: CODE_SMELLS.nestedConditionals.name,
|
|
596
|
+
severity: CODE_SMELLS.nestedConditionals.severity,
|
|
597
|
+
category: CODE_SMELLS.nestedConditionals.category,
|
|
598
|
+
line: index + 1,
|
|
599
|
+
message: `Deeply nested conditional (indentation: ${indentation} spaces)`,
|
|
600
|
+
fix: 'Use early returns or extract nested logic to separate functions'
|
|
601
|
+
})
|
|
602
|
+
}
|
|
603
|
+
})
|
|
604
|
+
|
|
605
|
+
return issues
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Check for dead code
|
|
610
|
+
*/
|
|
611
|
+
checkDeadCode(code, language, filePath) {
|
|
612
|
+
const issues = []
|
|
613
|
+
const lines = code.split('\n')
|
|
614
|
+
|
|
615
|
+
// Check for unreachable code after return/throw
|
|
616
|
+
let foundReturn = false
|
|
617
|
+
let braceDepth = 0
|
|
618
|
+
let inBlock = false
|
|
619
|
+
|
|
620
|
+
lines.forEach((line, index) => {
|
|
621
|
+
const trimmed = line.trim()
|
|
622
|
+
|
|
623
|
+
// Track brace depth
|
|
624
|
+
for (const char of line) {
|
|
625
|
+
if (char === '{') {
|
|
626
|
+
braceDepth++
|
|
627
|
+
inBlock = true
|
|
628
|
+
} else if (char === '}') {
|
|
629
|
+
braceDepth--
|
|
630
|
+
if (braceDepth === 0) {
|
|
631
|
+
inBlock = false
|
|
632
|
+
foundReturn = false
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
if (foundReturn && trimmed && !trimmed.startsWith('//') && !trimmed.startsWith('*') && !trimmed.match(/^[{}]$/)) {
|
|
638
|
+
issues.push({
|
|
639
|
+
type: 'deadCode',
|
|
640
|
+
name: CODE_SMELLS.deadCode.name,
|
|
641
|
+
severity: CODE_SMELLS.deadCode.severity,
|
|
642
|
+
category: CODE_SMELLS.deadCode.category,
|
|
643
|
+
line: index + 1,
|
|
644
|
+
message: 'Code appears to be unreachable',
|
|
645
|
+
fix: 'Remove unreachable code'
|
|
646
|
+
})
|
|
647
|
+
}
|
|
648
|
+
|
|
649
|
+
if (!inBlock && (trimmed.startsWith('return') || trimmed.startsWith('throw') ||
|
|
650
|
+
trimmed.startsWith('exit') || trimmed.startsWith('sys.exit'))) {
|
|
651
|
+
foundReturn = true
|
|
652
|
+
}
|
|
653
|
+
})
|
|
654
|
+
|
|
655
|
+
return issues
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* Check for commented code
|
|
660
|
+
*/
|
|
661
|
+
checkCommentedCode(code, language, filePath) {
|
|
662
|
+
const issues = []
|
|
663
|
+
const lines = code.split('\n')
|
|
664
|
+
|
|
665
|
+
lines.forEach((line, index) => {
|
|
666
|
+
const trimmed = line.trim()
|
|
667
|
+
|
|
668
|
+
// Check for commented code blocks
|
|
669
|
+
if ((trimmed.startsWith('//') || trimmed.startsWith('#')) &&
|
|
670
|
+
(trimmed.includes('function') || trimmed.includes('class') ||
|
|
671
|
+
trimmed.includes('const ') || trimmed.includes('let ') ||
|
|
672
|
+
trimmed.includes('var '))) {
|
|
673
|
+
issues.push({
|
|
674
|
+
type: 'commentedCode',
|
|
675
|
+
name: CODE_SMELLS.commentedCode.name,
|
|
676
|
+
severity: CODE_SMELLS.commentedCode.severity,
|
|
677
|
+
category: CODE_SMELLS.commentedCode.category,
|
|
678
|
+
line: index + 1,
|
|
679
|
+
message: 'Commented-out code found',
|
|
680
|
+
fix: 'Remove commented code or use version control to track changes'
|
|
681
|
+
})
|
|
682
|
+
}
|
|
683
|
+
})
|
|
684
|
+
|
|
685
|
+
return issues
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Check for security issues
|
|
690
|
+
*/
|
|
691
|
+
checkSecurityIssues(code, language, filePath) {
|
|
692
|
+
const issues = []
|
|
693
|
+
const lines = code.split('\n')
|
|
694
|
+
|
|
695
|
+
lines.forEach((line, index) => {
|
|
696
|
+
const trimmed = line.toLowerCase()
|
|
697
|
+
|
|
698
|
+
// SQL Injection
|
|
699
|
+
if ((trimmed.includes('query') || trimmed.includes('execute')) &&
|
|
700
|
+
trimmed.includes('+') && (trimmed.includes('select') || trimmed.includes('from'))) {
|
|
701
|
+
issues.push({
|
|
702
|
+
type: 'sqlInjection',
|
|
703
|
+
name: CODE_SMELLS.sqlInjection.name,
|
|
704
|
+
severity: CODE_SMELLS.sqlInjection.severity,
|
|
705
|
+
category: CODE_SMELLS.sqlInjection.category,
|
|
706
|
+
line: index + 1,
|
|
707
|
+
message: 'Potential SQL injection vulnerability',
|
|
708
|
+
fix: 'Use parameterized queries or prepared statements'
|
|
709
|
+
})
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
// XSS
|
|
713
|
+
if (trimmed.includes('innerhtml') || trimmed.includes('document.write')) {
|
|
714
|
+
issues.push({
|
|
715
|
+
type: 'xssRisk',
|
|
716
|
+
name: CODE_SMELLS.xssRisk.name,
|
|
717
|
+
severity: CODE_SMELLS.xssRisk.severity,
|
|
718
|
+
category: CODE_SMELLS.xssRisk.category,
|
|
719
|
+
line: index + 1,
|
|
720
|
+
message: 'Potential XSS vulnerability',
|
|
721
|
+
fix: 'Sanitize user input or use safe DOM APIs'
|
|
722
|
+
})
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
// Hardcoded secrets
|
|
726
|
+
if (trimmed.includes('api_key') || trimmed.includes('password') ||
|
|
727
|
+
trimmed.includes('secret') || trimmed.includes('token')) {
|
|
728
|
+
// Check for assignment without process.env
|
|
729
|
+
if (trimmed.includes('=') && !trimmed.includes('process.env') &&
|
|
730
|
+
!trimmed.includes('env.') && !trimmed.includes('env[')) {
|
|
731
|
+
issues.push({
|
|
732
|
+
type: 'hardcodedSecrets',
|
|
733
|
+
name: CODE_SMELLS.hardcodedSecrets.name,
|
|
734
|
+
severity: CODE_SMELLS.hardcodedSecrets.severity,
|
|
735
|
+
category: CODE_SMELLS.hardcodedSecrets.category,
|
|
736
|
+
line: index + 1,
|
|
737
|
+
message: 'Potential hardcoded secret',
|
|
738
|
+
fix: 'Use environment variables or secure configuration'
|
|
739
|
+
})
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
})
|
|
743
|
+
|
|
744
|
+
return issues
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Check for performance issues
|
|
749
|
+
*/
|
|
750
|
+
checkPerformanceIssues(code, language, filePath) {
|
|
751
|
+
const issues = []
|
|
752
|
+
const lines = code.split('\n')
|
|
753
|
+
|
|
754
|
+
lines.forEach((line, index) => {
|
|
755
|
+
const trimmed = line.trim()
|
|
756
|
+
|
|
757
|
+
// String concatenation in loop
|
|
758
|
+
if (line.includes('for ') || line.includes('while ')) {
|
|
759
|
+
const nextLine = lines[index + 1] || ''
|
|
760
|
+
if (nextLine.includes('+=') && (nextLine.includes('str') || nextLine.includes('string'))) {
|
|
761
|
+
issues.push({
|
|
762
|
+
type: 'stringConcatenation',
|
|
763
|
+
name: CODE_SMELLS.stringConcatenation.name,
|
|
764
|
+
severity: CODE_SMELLS.stringConcatenation.severity,
|
|
765
|
+
category: CODE_SMELLS.stringConcatenation.category,
|
|
766
|
+
line: index + 1,
|
|
767
|
+
message: 'String concatenation in loop',
|
|
768
|
+
fix: 'Use StringBuilder or array join for better performance'
|
|
769
|
+
})
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
// Inefficient loop
|
|
774
|
+
if (trimmed.includes('for (') && trimmed.includes('length')) {
|
|
775
|
+
issues.push({
|
|
776
|
+
type: 'inefficientLoop',
|
|
777
|
+
name: CODE_SMELLS.inefficientLoop.name,
|
|
778
|
+
severity: CODE_SMELLS.inefficientLoop.severity,
|
|
779
|
+
category: CODE_SMELLS.inefficientLoop.category,
|
|
780
|
+
line: index + 1,
|
|
781
|
+
message: 'Potentially inefficient loop (length accessed each iteration)',
|
|
782
|
+
fix: 'Cache length in a variable before loop'
|
|
783
|
+
})
|
|
784
|
+
}
|
|
785
|
+
})
|
|
786
|
+
|
|
787
|
+
return issues
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
/**
|
|
791
|
+
* Check for naming issues
|
|
792
|
+
*/
|
|
793
|
+
checkNamingIssues(code, language, filePath) {
|
|
794
|
+
const issues = []
|
|
795
|
+
const lines = code.split('\n')
|
|
796
|
+
|
|
797
|
+
lines.forEach((line, index) => {
|
|
798
|
+
// Single letter variables (except loop counters)
|
|
799
|
+
const singleLetterVar = line.match(/\b([a-z])\b(?!\s*[=+\-*/]|\s*[;,\)\]])/g)
|
|
800
|
+
if (singleLetterVar && singleLetterVar.length > 0) {
|
|
801
|
+
// Check if it's not a common loop counter
|
|
802
|
+
const hasLoopCounter = /\bfor\s*\(\s*[a-z]\s+/.test(line)
|
|
803
|
+
if (!hasLoopCounter) {
|
|
804
|
+
issues.push({
|
|
805
|
+
type: 'badVariableNames',
|
|
806
|
+
name: CODE_SMELLS.badVariableNames.name,
|
|
807
|
+
severity: CODE_SMELLS.badVariableNames.severity,
|
|
808
|
+
category: CODE_SMELLS.badVariableNames.category,
|
|
809
|
+
line: index + 1,
|
|
810
|
+
message: 'Non-descriptive variable name',
|
|
811
|
+
fix: 'Use more descriptive variable names'
|
|
812
|
+
})
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
// Bad function names (too short or unclear)
|
|
817
|
+
const functionRegex = this.getFunctionRegex(language)
|
|
818
|
+
// Create a non-global version for matching
|
|
819
|
+
const functionRegexNoGlobal = new RegExp(functionRegex.source, functionRegex.flags.replace('g', ''))
|
|
820
|
+
const functionMatch = line.match(functionRegexNoGlobal)
|
|
821
|
+
if (functionMatch) {
|
|
822
|
+
// Extract function name from match groups
|
|
823
|
+
let funcName = null
|
|
824
|
+
for (let i = 1; i < functionMatch.length; i++) {
|
|
825
|
+
if (functionMatch[i]) {
|
|
826
|
+
funcName = functionMatch[i]
|
|
827
|
+
break
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (funcName && funcName.length < 3 && !['main', 'init', 'run'].includes(funcName)) {
|
|
832
|
+
issues.push({
|
|
833
|
+
type: 'badFunctionNames',
|
|
834
|
+
name: CODE_SMELLS.badFunctionNames.name,
|
|
835
|
+
severity: CODE_SMELLS.badFunctionNames.severity,
|
|
836
|
+
category: CODE_SMELLS.badFunctionNames.category,
|
|
837
|
+
line: index + 1,
|
|
838
|
+
message: 'Function name is too short or unclear',
|
|
839
|
+
fix: 'Use descriptive function names that indicate purpose'
|
|
840
|
+
})
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
})
|
|
844
|
+
|
|
845
|
+
return issues
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
/**
|
|
849
|
+
* Check for best practice violations
|
|
850
|
+
*/
|
|
851
|
+
checkBestPractices(code, language, filePath) {
|
|
852
|
+
const issues = []
|
|
853
|
+
const lines = code.split('\n')
|
|
854
|
+
|
|
855
|
+
lines.forEach((line, index) => {
|
|
856
|
+
const trimmed = line.trim()
|
|
857
|
+
|
|
858
|
+
// Missing error handling
|
|
859
|
+
if ((trimmed.includes('fetch') || trimmed.includes('axios') ||
|
|
860
|
+
trimmed.includes('http') || trimmed.includes('request')) &&
|
|
861
|
+
!lines.slice(Math.max(0, index - 5), index).some(l => l.includes('try') || l.includes('catch'))) {
|
|
862
|
+
issues.push({
|
|
863
|
+
type: 'missingErrorHandling',
|
|
864
|
+
name: CODE_SMELLS.missingErrorHandling.name,
|
|
865
|
+
severity: CODE_SMELLS.missingErrorHandling.severity,
|
|
866
|
+
category: CODE_SMELLS.missingErrorHandling.category,
|
|
867
|
+
line: index + 1,
|
|
868
|
+
message: 'Network operation without error handling',
|
|
869
|
+
fix: 'Wrap in try-catch or use .catch() for promise-based operations'
|
|
870
|
+
})
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
// Missing documentation
|
|
874
|
+
const functionRegex = this.getFunctionRegex(language)
|
|
875
|
+
const functionRegexNoGlobal = new RegExp(functionRegex.source, functionRegex.flags.replace('g', ''))
|
|
876
|
+
const functionMatch = line.match(functionRegexNoGlobal)
|
|
877
|
+
if (functionMatch && index > 0) {
|
|
878
|
+
const prevLine = lines[index - 1].trim()
|
|
879
|
+
// Check if previous line is a documentation comment
|
|
880
|
+
const isDocumented = prevLine.startsWith('/**') ||
|
|
881
|
+
prevLine.startsWith('///') ||
|
|
882
|
+
prevLine.startsWith('#') ||
|
|
883
|
+
prevLine.startsWith('"""') ||
|
|
884
|
+
prevLine.startsWith('/*')
|
|
885
|
+
|
|
886
|
+
if (!isDocumented) {
|
|
887
|
+
issues.push({
|
|
888
|
+
type: 'missingDocumentation',
|
|
889
|
+
name: CODE_SMELLS.missingDocumentation.name,
|
|
890
|
+
severity: CODE_SMELLS.missingDocumentation.severity,
|
|
891
|
+
category: CODE_SMELLS.missingDocumentation.category,
|
|
892
|
+
line: index + 1,
|
|
893
|
+
message: 'Function lacks documentation',
|
|
894
|
+
fix: 'Add JSDoc comments or documentation comments'
|
|
895
|
+
})
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
})
|
|
899
|
+
|
|
900
|
+
return issues
|
|
901
|
+
}
|
|
902
|
+
|
|
903
|
+
/**
|
|
904
|
+
* Get function regex based on language
|
|
905
|
+
*/
|
|
906
|
+
getFunctionRegex(language) {
|
|
907
|
+
const regexMap = {
|
|
908
|
+
javascript: /(?:function\s+(\w+)|(?:const|let|var)\s+(\w+)\s*=\s*(?:function|\([^)]*\)\s*=>))/g,
|
|
909
|
+
typescript: /(?:function\s+(\w+)|(\w+)\s*:\s*(?:.*?)=>\s*|(\w+)\s*\(.*?\)\s*{)/g,
|
|
910
|
+
python: /def\s+(\w+)\s*\(/g,
|
|
911
|
+
java: /(?:public|private|protected)?\s*(?:static)?\s*\w+\s+(\w+)\s*\(/g,
|
|
912
|
+
csharp: /(?:public|private|protected)?\s*(?:static)?\s*\w+\s+(\w+)\s*\(/g,
|
|
913
|
+
cpp: /\w+\s+(\w+)\s*\([^)]*\)\s*{/g,
|
|
914
|
+
php: /function\s+(\w+)\s*\(/g,
|
|
915
|
+
ruby: /def\s+(\w+)/g,
|
|
916
|
+
go: /func\s+(?:\([^)]+\)\s+)?(\w+)\s*\(/g,
|
|
917
|
+
rust: /fn\s+(\w+)\s*\(/g,
|
|
918
|
+
swift: /func\s+(\w+)\s*\(/g,
|
|
919
|
+
kotlin: /(?:fun|private fun|public fun)\s+(\w+)\s*\(/g
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
return regexMap[language] || regexMap.javascript
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/**
|
|
926
|
+
* Extract function body from code
|
|
927
|
+
*/
|
|
928
|
+
extractFunctionBody(code, functionIndex, language) {
|
|
929
|
+
const codeFromFunction = code.substring(functionIndex)
|
|
930
|
+
const lines = codeFromFunction.split('\n')
|
|
931
|
+
|
|
932
|
+
if (language === 'python') {
|
|
933
|
+
// Python: find next line at same indentation
|
|
934
|
+
const functionLine = lines[0]
|
|
935
|
+
const indentMatch = functionLine.match(/^(\s*)/)
|
|
936
|
+
const baseIndent = indentMatch ? indentMatch[1].length : 0
|
|
937
|
+
let bodyLines = []
|
|
938
|
+
|
|
939
|
+
for (let i = 1; i < lines.length; i++) {
|
|
940
|
+
const line = lines[i]
|
|
941
|
+
const indent = line.match(/^(\s*)/)?.[1]?.length || 0
|
|
942
|
+
|
|
943
|
+
if (indent > baseIndent || line.trim() === '') {
|
|
944
|
+
bodyLines.push(line)
|
|
945
|
+
} else {
|
|
946
|
+
break
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
return bodyLines.join('\n')
|
|
950
|
+
} else {
|
|
951
|
+
// JavaScript-like languages
|
|
952
|
+
let braceCount = 0
|
|
953
|
+
let bodyLines = []
|
|
954
|
+
let foundOpenBrace = false
|
|
955
|
+
|
|
956
|
+
for (let i = 0; i < lines.length; i++) {
|
|
957
|
+
const line = lines[i]
|
|
958
|
+
|
|
959
|
+
for (const char of line) {
|
|
960
|
+
if (char === '{') {
|
|
961
|
+
braceCount++
|
|
962
|
+
foundOpenBrace = true
|
|
963
|
+
} else if (char === '}') {
|
|
964
|
+
braceCount--
|
|
965
|
+
if (foundOpenBrace && braceCount === 0) {
|
|
966
|
+
return bodyLines.join('\n')
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
if (foundOpenBrace) {
|
|
972
|
+
bodyLines.push(line)
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
return lines.slice(1).join('\n')
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
/**
|
|
980
|
+
* Generate summary statistics
|
|
981
|
+
*/
|
|
982
|
+
generateSummary(results) {
|
|
983
|
+
const summary = {
|
|
984
|
+
totalFiles: results.length,
|
|
985
|
+
totalIssues: 0,
|
|
986
|
+
errors: 0,
|
|
987
|
+
warnings: 0,
|
|
988
|
+
info: 0,
|
|
989
|
+
byCategory: {},
|
|
990
|
+
byLanguage: {}
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
results.forEach(result => {
|
|
994
|
+
result.issues.forEach(issue => {
|
|
995
|
+
summary.totalIssues++
|
|
996
|
+
|
|
997
|
+
if (issue.severity === 'error') summary.errors++
|
|
998
|
+
else if (issue.severity === 'warning') summary.warnings++
|
|
999
|
+
else summary.info++
|
|
1000
|
+
|
|
1001
|
+
// By category
|
|
1002
|
+
if (!summary.byCategory[issue.category]) {
|
|
1003
|
+
summary.byCategory[issue.category] = 0
|
|
1004
|
+
}
|
|
1005
|
+
summary.byCategory[issue.category]++
|
|
1006
|
+
|
|
1007
|
+
// By language
|
|
1008
|
+
if (!summary.byLanguage[result.language]) {
|
|
1009
|
+
summary.byLanguage[result.language] = 0
|
|
1010
|
+
}
|
|
1011
|
+
summary.byLanguage[result.language]++
|
|
1012
|
+
})
|
|
1013
|
+
})
|
|
1014
|
+
|
|
1015
|
+
return summary
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/**
|
|
1019
|
+
* Export results to JSON
|
|
1020
|
+
*/
|
|
1021
|
+
async exportToJSON(results, outputPath) {
|
|
1022
|
+
const jsonContent = JSON.stringify(results, null, 2)
|
|
1023
|
+
await fs.writeFile(outputPath, jsonContent)
|
|
1024
|
+
return outputPath
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Export results to Markdown
|
|
1029
|
+
*/
|
|
1030
|
+
async exportToMarkdown(results, outputPath) {
|
|
1031
|
+
const markdown = this.generateMarkdownReport(results)
|
|
1032
|
+
await fs.writeFile(outputPath, markdown)
|
|
1033
|
+
return outputPath
|
|
1034
|
+
}
|
|
1035
|
+
|
|
1036
|
+
/**
|
|
1037
|
+
* Generate Markdown report
|
|
1038
|
+
*/
|
|
1039
|
+
generateMarkdownReport(results) {
|
|
1040
|
+
const timestamp = new Date().toLocaleString()
|
|
1041
|
+
const summary = this.generateSummary(results.results || [results])
|
|
1042
|
+
|
|
1043
|
+
let report = `# AI Code Quality Analysis Report\n\n`
|
|
1044
|
+
report += `**Generated**: ${timestamp}\n`
|
|
1045
|
+
report += `**Provider**: ${this.currentProvider || 'Mock'}\n`
|
|
1046
|
+
report += `**Files Analyzed**: ${summary.totalFiles}\n\n`
|
|
1047
|
+
|
|
1048
|
+
report += `## Summary\n\n`
|
|
1049
|
+
report += `| Metric | Count |\n`
|
|
1050
|
+
report += `|--------|-------|\n`
|
|
1051
|
+
report += `| Total Issues | ${summary.totalIssues} |\n`
|
|
1052
|
+
report += `| Errors | ${summary.errors} |\n`
|
|
1053
|
+
report += `| Warnings | ${summary.warnings} |\n`
|
|
1054
|
+
report += `| Info | ${summary.info} |\n\n`
|
|
1055
|
+
|
|
1056
|
+
report += `### Issues by Category\n\n`
|
|
1057
|
+
Object.entries(summary.byCategory).forEach(([category, count]) => {
|
|
1058
|
+
report += `- **${category}**: ${count}\n`
|
|
1059
|
+
})
|
|
1060
|
+
report += `\n`
|
|
1061
|
+
|
|
1062
|
+
report += `### Issues by Language\n\n`
|
|
1063
|
+
Object.entries(summary.byLanguage).forEach(([language, count]) => {
|
|
1064
|
+
report += `- **${language}**: ${count}\n`
|
|
1065
|
+
})
|
|
1066
|
+
report += `\n`
|
|
1067
|
+
|
|
1068
|
+
report += `## Detailed Issues\n\n`
|
|
1069
|
+
|
|
1070
|
+
const analysisResults = results.results || [results]
|
|
1071
|
+
analysisResults.forEach(result => {
|
|
1072
|
+
if (result.issues && result.issues.length > 0) {
|
|
1073
|
+
report += `### ${result.file}\n\n`
|
|
1074
|
+
report += `**Language**: ${result.language}\n`
|
|
1075
|
+
report += `**Total Issues**: ${result.issues.length}\n\n`
|
|
1076
|
+
|
|
1077
|
+
result.issues.forEach(issue => {
|
|
1078
|
+
const severityIcon = issue.severity === 'error' ? '❌' :
|
|
1079
|
+
issue.severity === 'warning' ? '⚠️' : 'ℹ️'
|
|
1080
|
+
report += `${severityIcon} **${issue.name}** (Line ${issue.line})\n`
|
|
1081
|
+
report += `- Severity: ${issue.severity}\n`
|
|
1082
|
+
report += `- Category: ${issue.category}\n`
|
|
1083
|
+
report += `- Message: ${issue.message}\n`
|
|
1084
|
+
report += `- Fix: ${issue.fix}\n\n`
|
|
1085
|
+
})
|
|
1086
|
+
}
|
|
1087
|
+
})
|
|
1088
|
+
|
|
1089
|
+
report += `---\n*Generated by Code-Simplifier AI Quality Analyzer*\n`
|
|
1090
|
+
|
|
1091
|
+
return report
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
module.exports = new AIQualityAnalyzer()
|