mr-sliy 1.0.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.
Files changed (50) hide show
  1. package/.env.example +145 -0
  2. package/database/schema.sql +187 -0
  3. package/package.json +74 -0
  4. package/scripts/download-tree-sitter.js +171 -0
  5. package/scripts/postinstall.js +134 -0
  6. package/src/agent/agent.js +563 -0
  7. package/src/agent.js +87 -0
  8. package/src/cli/index.js +1643 -0
  9. package/src/config/index.js +232 -0
  10. package/src/engine/dualModeEngine.js +486 -0
  11. package/src/index.js +165 -0
  12. package/src/middlewares/errorHandler.js +166 -0
  13. package/src/middlewares/index.js +23 -0
  14. package/src/routes/aiRoutes.js +117 -0
  15. package/src/routes/configRoutes.js +31 -0
  16. package/src/routes/index.js +75 -0
  17. package/src/routes/issueRoutes.js +195 -0
  18. package/src/routes/projectRoutes.js +46 -0
  19. package/src/routes/reportRoutes.js +40 -0
  20. package/src/routes/scanRoutes.js +245 -0
  21. package/src/routes/userRoutes.js +47 -0
  22. package/src/services/ast/parser.js +503 -0
  23. package/src/services/detection/detector.js +934 -0
  24. package/src/services/llm/providers.js +1107 -0
  25. package/src/services/rag/agent.js +375 -0
  26. package/src/services/vector/knowledgeBase.js +863 -0
  27. package/src/skills/Skill.js +38 -0
  28. package/src/skills/code-analysis/index.js +272 -0
  29. package/src/skills/code-detection/index.js +166 -0
  30. package/src/skills/code-detection/rules/console-log.js +45 -0
  31. package/src/skills/code-detection/rules/deep-nesting.js +76 -0
  32. package/src/skills/code-detection/rules/duplicate-code.js +57 -0
  33. package/src/skills/code-detection/rules/high-complexity.js +109 -0
  34. package/src/skills/code-detection/rules/index.js +59 -0
  35. package/src/skills/code-detection/rules/long-functions.js +54 -0
  36. package/src/skills/code-detection/rules/magic-numbers.js +48 -0
  37. package/src/skills/code-detection/rules/missing-comment.js +64 -0
  38. package/src/skills/code-detection/rules/null-check.js +71 -0
  39. package/src/skills/code-detection/rules/unnecessary-else.js +46 -0
  40. package/src/skills/code-detection/rules/unused-functions.js +57 -0
  41. package/src/skills/code-detection/rules/unused-imports.js +57 -0
  42. package/src/skills/code-detection/rules/unused-variables.js +54 -0
  43. package/src/skills/code-optimization/index.js +319 -0
  44. package/src/skills/index.js +152 -0
  45. package/src/utils/crypto.js +212 -0
  46. package/src/utils/database.js +125 -0
  47. package/src/utils/helpers.js +226 -0
  48. package/src/utils/logger.js +202 -0
  49. package/src/utils/mysql.js +198 -0
  50. package/src/utils/response.js +124 -0
@@ -0,0 +1,934 @@
1
+ /**
2
+ * 代码缺陷检测引擎
3
+ * 基于AST分析识别代码冗余和不规范问题
4
+ */
5
+
6
+ const { config } = require('../../config');
7
+ const { logger } = require('../../utils/logger');
8
+ const { countLines, getFileLanguage } = require('../../utils/helpers');
9
+ const { parseCode, traverseAST, getNodesByType, extractFunctions, extractVariables, extractImports } = require('../ast/parser');
10
+ const { getDatabase } = require('../../utils/database');
11
+ const { generateUUID } = require('../../utils/helpers');
12
+
13
+ /**
14
+ * 检测规则配置
15
+ */
16
+ const detectionRules = {
17
+ unusedVariables: {
18
+ enabled: config.detection.unusedVariables,
19
+ severity: 'medium',
20
+ message: '变量已声明但从未使用',
21
+ suggestion: '删除未使用的变量或根据需要使用它'
22
+ },
23
+ unusedImports: {
24
+ enabled: config.detection.unusedImports,
25
+ severity: 'low',
26
+ message: '导入已声明但从未使用',
27
+ suggestion: '删除未使用的导入以减少打包体积'
28
+ },
29
+ unusedFunctions: {
30
+ enabled: config.detection.unusedFunctions,
31
+ severity: 'high',
32
+ message: '函数已定义但从未被调用',
33
+ suggestion: '删除未使用的函数或确认是否应该在代码中使用它'
34
+ },
35
+ magicNumbers: {
36
+ enabled: config.detection.magicNumbers,
37
+ severity: 'low',
38
+ message: '发现魔法数字',
39
+ suggestion: '将魔法数字提取为常量并添加说明性名称'
40
+ },
41
+ longFunctions: {
42
+ enabled: true,
43
+ severity: 'medium',
44
+ threshold: config.detection.maxFunctionLines,
45
+ message: '函数过长',
46
+ suggestion: '将长函数拆分为多个小函数,每个函数负责单一职责'
47
+ },
48
+ highComplexity: {
49
+ enabled: true,
50
+ severity: 'medium',
51
+ threshold: config.detection.maxCyclomaticComplexity,
52
+ message: '圈复杂度过高',
53
+ suggestion: '简化逻辑结构,减少嵌套层级'
54
+ },
55
+ missingComments: {
56
+ enabled: true,
57
+ severity: 'low',
58
+ message: '函数缺少注释',
59
+ suggestion: '添加函数说明注释,提高代码可读性'
60
+ },
61
+ duplicateCode: {
62
+ enabled: true,
63
+ severity: 'medium',
64
+ message: '发现重复代码片段',
65
+ suggestion: '提取重复代码为独立函数或模块'
66
+ },
67
+ deepNesting: {
68
+ enabled: true,
69
+ severity: 'medium',
70
+ threshold: 4,
71
+ message: '嵌套层级过深',
72
+ suggestion: '减少嵌套层级,提取内层逻辑为独立函数'
73
+ },
74
+ nullCheck: {
75
+ enabled: true,
76
+ severity: 'high',
77
+ message: '缺少空值检查',
78
+ suggestion: '添加空值检查以防止运行时错误'
79
+ },
80
+ unnecessaryElse: {
81
+ enabled: true,
82
+ severity: 'low',
83
+ message: 'return后使用不必要的else',
84
+ suggestion: '删除return后的else语句以简化代码'
85
+ },
86
+ consoleLog: {
87
+ enabled: true,
88
+ severity: 'low',
89
+ message: '存在调试用的console.log',
90
+ suggestion: '删除或替换为正式的日志记录'
91
+ }
92
+ };
93
+
94
+ /**
95
+ * 执行代码缺陷检测
96
+ */
97
+ async function detectIssues(sourceCode, filePath, options = {}) {
98
+ const startTime = Date.now();
99
+ const language = getFileLanguage(filePath);
100
+
101
+ try {
102
+ logger.info(`开始检测: ${filePath}`);
103
+
104
+ // 1. 解析代码为AST
105
+ const parseResult = await parseCode(sourceCode, language);
106
+
107
+ if (!parseResult.success) {
108
+ return {
109
+ success: false,
110
+ message: parseResult.error,
111
+ filePath
112
+ };
113
+ }
114
+
115
+ const tree = parseResult.tree;
116
+ const issues = [];
117
+
118
+ // 2. 执行各项检测
119
+ if (detectionRules.unusedVariables.enabled) {
120
+ const unusedVarIssues = detectUnusedVariables(tree, sourceCode, filePath);
121
+ issues.push(...unusedVarIssues);
122
+ }
123
+
124
+ if (detectionRules.unusedImports.enabled) {
125
+ const unusedImportIssues = detectUnusedImports(tree, sourceCode, filePath);
126
+ issues.push(...unusedImportIssues);
127
+ }
128
+
129
+ if (detectionRules.unusedFunctions.enabled) {
130
+ const unusedFuncIssues = detectUnusedFunctions(tree, sourceCode, filePath);
131
+ issues.push(...unusedFuncIssues);
132
+ }
133
+
134
+ if (detectionRules.magicNumbers.enabled) {
135
+ const magicNumberIssues = detectMagicNumbers(tree, sourceCode, filePath);
136
+ issues.push(...magicNumberIssues);
137
+ }
138
+
139
+ if (detectionRules.longFunctions.enabled) {
140
+ const longFuncIssues = detectLongFunctions(tree, sourceCode, filePath);
141
+ issues.push(...longFuncIssues);
142
+ }
143
+
144
+ if (detectionRules.highComplexity.enabled) {
145
+ const complexityIssues = detectCyclomaticComplexity(tree, sourceCode, filePath);
146
+ issues.push(...complexityIssues);
147
+ }
148
+
149
+ if (detectionRules.deepNesting.enabled) {
150
+ const nestingIssues = detectDeepNesting(tree, sourceCode, filePath);
151
+ issues.push(...nestingIssues);
152
+ }
153
+
154
+ if (detectionRules.nullCheck.enabled) {
155
+ const nullCheckIssues = detectMissingNullCheck(tree, sourceCode, filePath);
156
+ issues.push(...nullCheckIssues);
157
+ }
158
+
159
+ if (detectionRules.unnecessaryElse.enabled) {
160
+ const elseIssues = detectUnnecessaryElse(tree, sourceCode, filePath);
161
+ issues.push(...elseIssues);
162
+ }
163
+
164
+ if (detectionRules.consoleLog.enabled) {
165
+ const consoleIssues = detectConsoleLog(tree, sourceCode, filePath);
166
+ issues.push(...consoleIssues);
167
+ }
168
+
169
+ if (detectionRules.duplicateCode.enabled) {
170
+ const duplicateIssues = detectDuplicateCode(tree, sourceCode, filePath);
171
+ issues.push(...duplicateIssues);
172
+ }
173
+
174
+ if (detectionRules.missingComments.enabled) {
175
+ const commentIssues = detectMissingComments(tree, sourceCode, filePath);
176
+ issues.push(...commentIssues);
177
+ }
178
+
179
+ // 3. 统计检测结果
180
+ const result = {
181
+ success: true,
182
+ filePath,
183
+ language,
184
+ totalIssues: issues.length,
185
+ issueCounts: {
186
+ critical: issues.filter(i => i.severity === 'critical').length,
187
+ high: issues.filter(i => i.severity === 'high').length,
188
+ medium: issues.filter(i => i.severity === 'medium').length,
189
+ low: issues.filter(i => i.severity === 'low').length
190
+ },
191
+ issues,
192
+ durationMs: Date.now() - startTime
193
+ };
194
+
195
+ logger.info(`检测完成: ${filePath}, 发现 ${issues.length} 个问题`);
196
+
197
+ return result;
198
+ } catch (error) {
199
+ logger.error(`检测失败: ${filePath}`, error);
200
+ return {
201
+ success: false,
202
+ message: error.message,
203
+ filePath,
204
+ durationMs: Date.now() - startTime
205
+ };
206
+ }
207
+ }
208
+
209
+ /**
210
+ * 检测未使用的变量
211
+ */
212
+ function detectUnusedVariables(tree, sourceCode, filePath) {
213
+ const issues = [];
214
+
215
+ try {
216
+ // 提取所有变量声明
217
+ const variableDeclarations = getNodesByType(tree.rootNode, 'variable_declarator');
218
+
219
+ // 提取所有标识符引用
220
+ const identifiers = getNodesByType(tree.rootNode, 'identifier');
221
+
222
+ // 检查每个变量是否被使用
223
+ variableDeclarations.forEach(varDecl => {
224
+ const varName = varDecl.text.split('=')[0].trim();
225
+
226
+ // 检查是否在其他地方被引用(排除声明位置)
227
+ const isUsed = identifiers.some(id => {
228
+ return id.text === varName &&
229
+ (id.startPosition.row !== varDecl.startPosition.row ||
230
+ id.startPosition.column !== varDecl.startPosition.column);
231
+ });
232
+
233
+ if (!isUsed) {
234
+ issues.push({
235
+ id: generateUUID(),
236
+ filePath,
237
+ fileName: filePath.split(/[/\\]/).pop(),
238
+ language: getFileLanguage(filePath),
239
+ issueType: 'unused_variable',
240
+ severity: detectionRules.unusedVariables.severity,
241
+ message: `变量"${varName}"已声明但从未使用`,
242
+ suggestion: detectionRules.unusedVariables.suggestion,
243
+ lineStart: varDecl.startPosition.row + 1,
244
+ lineEnd: varDecl.endPosition.row + 1,
245
+ columnStart: varDecl.startPosition.column,
246
+ columnEnd: varDecl.endPosition.column,
247
+ codeSnippet: varDecl.text,
248
+ astNodeType: 'variable_declarator'
249
+ });
250
+ }
251
+ });
252
+ } catch (error) {
253
+ logger.error('检测未使用变量失败:', error);
254
+ }
255
+
256
+ return issues;
257
+ }
258
+
259
+ /**
260
+ * 检测未使用的导入
261
+ */
262
+ function detectUnusedImports(tree, sourceCode, filePath) {
263
+ const issues = [];
264
+
265
+ try {
266
+ // 提取导入语句
267
+ const importStatements = getNodesByType(tree.rootNode, 'import_statement');
268
+
269
+ // 提取所有标识符
270
+ const identifiers = getNodesByType(tree.rootNode, 'identifier');
271
+
272
+ importStatements.forEach(importStmt => {
273
+ // 提取导入的模块名
274
+ const importMatch = importStmt.text.match(/import\s+(\w+)\s+from/);
275
+
276
+ if (importMatch) {
277
+ const importedName = importMatch[1];
278
+
279
+ // 检查是否被使用
280
+ const isUsed = identifiers.some(id =>
281
+ id.text === importedName &&
282
+ id.startPosition.row !== importStmt.startPosition.row
283
+ );
284
+
285
+ if (!isUsed) {
286
+ issues.push({
287
+ id: generateUUID(),
288
+ filePath,
289
+ fileName: filePath.split(/[/\\]/).pop(),
290
+ language: getFileLanguage(filePath),
291
+ issueType: 'unused_import',
292
+ severity: detectionRules.unusedImports.severity,
293
+ message: `导入"${importedName}"已声明但从未使用`,
294
+ suggestion: detectionRules.unusedImports.suggestion,
295
+ lineStart: importStmt.startPosition.row + 1,
296
+ lineEnd: importStmt.endPosition.row + 1,
297
+ columnStart: importStmt.startPosition.column,
298
+ columnEnd: importStmt.endPosition.column,
299
+ codeSnippet: importStmt.text,
300
+ astNodeType: 'import_statement'
301
+ });
302
+ }
303
+ }
304
+ });
305
+ } catch (error) {
306
+ logger.error('检测未使用导入失败:', error);
307
+ }
308
+
309
+ return issues;
310
+ }
311
+
312
+ /**
313
+ * 检测未使用的函数
314
+ */
315
+ function detectUnusedFunctions(tree, sourceCode, filePath) {
316
+ const issues = [];
317
+
318
+ try {
319
+ // 提取函数声明
320
+ const functionDeclarations = getNodesByType(tree.rootNode, 'function_declaration');
321
+
322
+ // 提取所有标识符
323
+ const identifiers = getNodesByType(tree.rootNode, 'identifier');
324
+
325
+ functionDeclarations.forEach(funcDecl => {
326
+ // 提取函数名
327
+ const funcNameMatch = funcDecl.text.match(/function\s+(\w+)\s*\(/);
328
+
329
+ if (funcNameMatch) {
330
+ const funcName = funcNameMatch[1];
331
+
332
+ // 检查是否被调用(排除函数定义位置)
333
+ const isCalled = identifiers.some(id =>
334
+ id.text === funcName &&
335
+ id.startPosition.row !== funcDecl.startPosition.row
336
+ );
337
+
338
+ if (!isCalled) {
339
+ issues.push({
340
+ id: generateUUID(),
341
+ filePath,
342
+ fileName: filePath.split(/[/\\]/).pop(),
343
+ language: getFileLanguage(filePath),
344
+ issueType: 'unused_function',
345
+ severity: detectionRules.unusedFunctions.severity,
346
+ message: `函数"${funcName}"已定义但从未被调用`,
347
+ suggestion: detectionRules.unusedFunctions.suggestion,
348
+ lineStart: funcDecl.startPosition.row + 1,
349
+ lineEnd: funcDecl.endPosition.row + 1,
350
+ columnStart: funcDecl.startPosition.column,
351
+ columnEnd: funcDecl.endPosition.column,
352
+ codeSnippet: funcDecl.text.substring(0, 100),
353
+ astNodeType: 'function_declaration'
354
+ });
355
+ }
356
+ }
357
+ });
358
+ } catch (error) {
359
+ logger.error('检测未使用函数失败:', error);
360
+ }
361
+
362
+ return issues;
363
+ }
364
+
365
+ /**
366
+ * 检测魔法数字
367
+ */
368
+ function detectMagicNumbers(tree, sourceCode, filePath) {
369
+ const issues = [];
370
+
371
+ try {
372
+ // 提取数字节点
373
+ const numberNodes = getNodesByType(tree.rootNode, 'number');
374
+
375
+ // 常见的魔法数字阈值
376
+ const magicThresholds = [100, 1000, 60, 24, 7, 30, 365, 86400];
377
+
378
+ numberNodes.forEach(numNode => {
379
+ const numValue = parseFloat(numNode.text);
380
+
381
+ // 检查是否为魔法数字
382
+ if (magicThresholds.includes(numValue) || numValue > 1000) {
383
+ issues.push({
384
+ id: generateUUID(),
385
+ filePath,
386
+ fileName: filePath.split(/[/\\]/).pop(),
387
+ language: getFileLanguage(filePath),
388
+ issueType: 'magic_number',
389
+ severity: detectionRules.magicNumbers.severity,
390
+ message: `发现魔法数字: ${numValue}`,
391
+ suggestion: detectionRules.magicNumbers.suggestion,
392
+ lineStart: numNode.startPosition.row + 1,
393
+ lineEnd: numNode.endPosition.row + 1,
394
+ columnStart: numNode.startPosition.column,
395
+ columnEnd: numNode.endPosition.column,
396
+ codeSnippet: numNode.text,
397
+ astNodeType: 'number'
398
+ });
399
+ }
400
+ });
401
+ } catch (error) {
402
+ logger.error('检测魔法数字失败:', error);
403
+ }
404
+
405
+ return issues;
406
+ }
407
+
408
+ /**
409
+ * 检测过长函数
410
+ */
411
+ function detectLongFunctions(tree, sourceCode, filePath) {
412
+ const issues = [];
413
+
414
+ try {
415
+ // 提取函数声明
416
+ const functionDeclarations = getNodesByType(tree.rootNode, 'function_declaration');
417
+
418
+ functionDeclarations.forEach(funcDecl => {
419
+ const funcLines = funcDecl.endPosition.row - funcDecl.startPosition.row + 1;
420
+
421
+ if (funcLines > detectionRules.longFunctions.threshold) {
422
+ issues.push({
423
+ id: generateUUID(),
424
+ filePath,
425
+ fileName: filePath.split(/[/\\]/).pop(),
426
+ language: getFileLanguage(filePath),
427
+ issueType: 'long_function',
428
+ severity: detectionRules.longFunctions.severity,
429
+ message: `函数过长(${funcLines}行),建议拆分`,
430
+ suggestion: detectionRules.longFunctions.suggestion,
431
+ lineStart: funcDecl.startPosition.row + 1,
432
+ lineEnd: funcDecl.endPosition.row + 1,
433
+ columnStart: funcDecl.startPosition.column,
434
+ columnEnd: funcDecl.endPosition.column,
435
+ codeSnippet: funcDecl.text.substring(0, 100),
436
+ astNodeType: 'function_declaration',
437
+ metadata: {
438
+ lineCount: funcLines,
439
+ threshold: detectionRules.longFunctions.threshold
440
+ }
441
+ });
442
+ }
443
+ });
444
+ } catch (error) {
445
+ logger.error('检测过长函数失败:', error);
446
+ }
447
+
448
+ return issues;
449
+ }
450
+
451
+ /**
452
+ * 检测圈复杂度
453
+ */
454
+ function detectCyclomaticComplexity(tree, sourceCode, filePath) {
455
+ const issues = [];
456
+
457
+ try {
458
+ const lines = sourceCode.split('\n');
459
+ const funcDeclarations = [];
460
+
461
+ for (let i = 0; i < lines.length; i++) {
462
+ const match = lines[i].trim().match(/function\s+(\w+)\s*\(/);
463
+ if (match) {
464
+ let braceCount = 0;
465
+ const hasOpenBrace = lines[i].includes('{');
466
+ if (hasOpenBrace) braceCount++;
467
+
468
+ let endLine = i;
469
+ for (let j = i + 1; j < lines.length; j++) {
470
+ const lineBraces = (lines[j].match(/\{/g) || []).length;
471
+ const closeBraces = (lines[j].match(/\}/g) || []).length;
472
+ braceCount += lineBraces - closeBraces;
473
+
474
+ if (braceCount <= 0) {
475
+ endLine = j;
476
+ break;
477
+ }
478
+ }
479
+
480
+ funcDeclarations.push({
481
+ name: match[1],
482
+ startLine: i,
483
+ endLine: endLine,
484
+ text: lines[i].trim()
485
+ });
486
+ }
487
+ }
488
+
489
+ funcDeclarations.forEach(funcDecl => {
490
+ let complexity = 1;
491
+
492
+ for (let i = funcDecl.startLine; i <= funcDecl.endLine; i++) {
493
+ const line = lines[i];
494
+ if (!line) continue;
495
+
496
+ const trimmed = line.trim();
497
+
498
+ if (trimmed.startsWith('if (') || trimmed.startsWith('if(')) {
499
+ complexity++;
500
+ }
501
+ if (trimmed.startsWith('for (')) {
502
+ complexity++;
503
+ }
504
+ if (trimmed.startsWith('while (')) {
505
+ complexity++;
506
+ }
507
+ if (trimmed.startsWith('case ')) {
508
+ complexity++;
509
+ }
510
+ if (trimmed.startsWith('catch (')) {
511
+ complexity++;
512
+ }
513
+
514
+ const andCount = (trimmed.match(/&&/g) || []).length;
515
+ const orCount = (trimmed.match(/\|\|/g) || []).length;
516
+ complexity += andCount + orCount;
517
+ }
518
+
519
+ if (complexity > detectionRules.highComplexity.threshold) {
520
+ issues.push({
521
+ id: generateUUID(),
522
+ filePath,
523
+ fileName: filePath.split(/[/\\]/).pop(),
524
+ language: getFileLanguage(filePath),
525
+ issueType: 'high_complexity',
526
+ severity: detectionRules.highComplexity.severity,
527
+ message: `函数"${funcDecl.name}"圈复杂度过高(${complexity})`,
528
+ suggestion: detectionRules.highComplexity.suggestion,
529
+ lineStart: funcDecl.startLine + 1,
530
+ lineEnd: funcDecl.endLine + 1,
531
+ columnStart: 0,
532
+ columnEnd: funcDecl.text.length,
533
+ codeSnippet: funcDecl.text.substring(0, 100),
534
+ astNodeType: 'function_declaration',
535
+ metadata: {
536
+ complexity,
537
+ threshold: detectionRules.highComplexity.threshold
538
+ }
539
+ });
540
+ }
541
+ });
542
+ } catch (error) {
543
+ logger.error('检测圈复杂度失败:', error);
544
+ }
545
+
546
+ return issues;
547
+ }
548
+
549
+ /**
550
+ * 检测嵌套过深
551
+ */
552
+ function detectDeepNesting(tree, sourceCode, filePath) {
553
+ const issues = [];
554
+
555
+ try {
556
+ const lines = sourceCode.split('\n');
557
+ let currentDepth = 0;
558
+ let maxDepth = 0;
559
+ let deepLines = [];
560
+
561
+ lines.forEach((line, index) => {
562
+ const trimmed = line.trim();
563
+ if (!trimmed) return;
564
+
565
+ const indentMatch = line.match(/^(\s+)/);
566
+ const indentSpaces = indentMatch ? indentMatch[1].length : 0;
567
+ const indentLevel = Math.floor(indentSpaces / 2);
568
+
569
+ currentDepth = indentLevel;
570
+
571
+ if (currentDepth > maxDepth) {
572
+ maxDepth = currentDepth;
573
+ }
574
+
575
+ if (currentDepth > detectionRules.deepNesting.threshold) {
576
+ deepLines.push({
577
+ line: index + 1,
578
+ depth: currentDepth,
579
+ text: trimmed.substring(0, 50)
580
+ });
581
+ }
582
+ });
583
+
584
+ if (maxDepth > detectionRules.deepNesting.threshold && deepLines.length > 0) {
585
+ const firstDeepLine = deepLines[0];
586
+ issues.push({
587
+ id: generateUUID(),
588
+ filePath,
589
+ fileName: filePath.split(/[/\\]/).pop(),
590
+ language: getFileLanguage(filePath),
591
+ issueType: 'deep_nesting',
592
+ severity: detectionRules.deepNesting.severity,
593
+ message: `嵌套层级过深(最大${maxDepth}层,阈值${detectionRules.deepNesting.threshold}层)`,
594
+ suggestion: detectionRules.deepNesting.suggestion,
595
+ lineStart: firstDeepLine.line,
596
+ lineEnd: firstDeepLine.line,
597
+ columnStart: 0,
598
+ columnEnd: firstDeepLine.text.length,
599
+ codeSnippet: firstDeepLine.text,
600
+ astNodeType: 'deep_nesting',
601
+ metadata: {
602
+ maxDepth,
603
+ threshold: detectionRules.deepNesting.threshold,
604
+ deepLineCount: deepLines.length
605
+ }
606
+ });
607
+ }
608
+ } catch (error) {
609
+ logger.error('检测嵌套过深失败:', error);
610
+ }
611
+
612
+ return issues;
613
+ }
614
+
615
+ /**
616
+ * 检测缺少空值检查
617
+ */
618
+ function detectMissingNullCheck(tree, sourceCode, filePath) {
619
+ const issues = [];
620
+
621
+ try {
622
+ const functionDeclarations = getNodesByType(tree.rootNode, 'function_declaration');
623
+
624
+ functionDeclarations.forEach(funcDecl => {
625
+ const identifiers = getNodesByType(funcDecl, 'identifier');
626
+ const params = funcDecl.text.match(/function\s+\w+\s*\(([^)]*)\)/);
627
+
628
+ if (params) {
629
+ const paramNames = params[1].split(',').map(p => p.trim()).filter(p => p.length > 0);
630
+
631
+ paramNames.forEach(param => {
632
+ const paramUsages = identifiers.filter(id => id.text === param);
633
+ if (paramUsages.length > 1) {
634
+ let hasNullCheck = false;
635
+
636
+ traverseAST(funcDecl, (node) => {
637
+ if (node.type === 'if_statement') {
638
+ if (node.text.includes(`!${param}`) || node.text.includes(`${param} !== null`) ||
639
+ node.text.includes(`${param} !== undefined`) || node.text.includes(`${param} != null`)) {
640
+ hasNullCheck = true;
641
+ }
642
+ }
643
+ });
644
+
645
+ if (!hasNullCheck && paramUsages.length > 2) {
646
+ issues.push({
647
+ id: generateUUID(),
648
+ filePath,
649
+ fileName: filePath.split(/[/\\]/).pop(),
650
+ language: getFileLanguage(filePath),
651
+ issueType: 'null_check',
652
+ severity: detectionRules.nullCheck.severity,
653
+ message: `参数"${param}"缺少空值检查`,
654
+ suggestion: detectionRules.nullCheck.suggestion,
655
+ lineStart: funcDecl.startPosition.row + 1,
656
+ lineEnd: funcDecl.endPosition.row + 1,
657
+ columnStart: funcDecl.startPosition.column,
658
+ columnEnd: funcDecl.endPosition.column,
659
+ codeSnippet: funcDecl.text.substring(0, 100),
660
+ astNodeType: 'function_declaration',
661
+ metadata: {
662
+ paramName: param
663
+ }
664
+ });
665
+ }
666
+ }
667
+ });
668
+ }
669
+ });
670
+ } catch (error) {
671
+ logger.error('检测空值检查失败:', error);
672
+ }
673
+
674
+ return issues;
675
+ }
676
+
677
+ /**
678
+ * 检测不必要的else语句
679
+ */
680
+ function detectUnnecessaryElse(tree, sourceCode, filePath) {
681
+ const issues = [];
682
+
683
+ try {
684
+ traverseAST(tree.rootNode, (node) => {
685
+ if (node.type === 'if_statement' && node.text.includes('else')) {
686
+ const ifBody = node.text.split('else')[0];
687
+ if (ifBody.includes('return ') || ifBody.includes('throw ')) {
688
+ issues.push({
689
+ id: generateUUID(),
690
+ filePath,
691
+ fileName: filePath.split(/[/\\]/).pop(),
692
+ language: getFileLanguage(filePath),
693
+ issueType: 'unnecessary_else',
694
+ severity: detectionRules.unnecessaryElse.severity,
695
+ message: 'return后使用不必要的else',
696
+ suggestion: detectionRules.unnecessaryElse.suggestion,
697
+ lineStart: node.startPosition.row + 1,
698
+ lineEnd: node.endPosition.row + 1,
699
+ columnStart: node.startPosition.column,
700
+ columnEnd: node.endPosition.column,
701
+ codeSnippet: node.text.substring(0, 80),
702
+ astNodeType: 'if_statement'
703
+ });
704
+ }
705
+ }
706
+ });
707
+ } catch (error) {
708
+ logger.error('检测不必要else失败:', error);
709
+ }
710
+
711
+ return issues;
712
+ }
713
+
714
+ /**
715
+ * 检测调试用console.log
716
+ */
717
+ function detectConsoleLog(tree, sourceCode, filePath) {
718
+ const issues = [];
719
+
720
+ try {
721
+ const callExpressions = getNodesByType(tree.rootNode, 'call_expression');
722
+
723
+ callExpressions.forEach(callExpr => {
724
+ if (callExpr.text.startsWith('console.log(')) {
725
+ issues.push({
726
+ id: generateUUID(),
727
+ filePath,
728
+ fileName: filePath.split(/[/\\]/).pop(),
729
+ language: getFileLanguage(filePath),
730
+ issueType: 'console_log',
731
+ severity: detectionRules.consoleLog.severity,
732
+ message: '存在调试用的console.log',
733
+ suggestion: detectionRules.consoleLog.suggestion,
734
+ lineStart: callExpr.startPosition.row + 1,
735
+ lineEnd: callExpr.endPosition.row + 1,
736
+ columnStart: callExpr.startPosition.column,
737
+ columnEnd: callExpr.endPosition.column,
738
+ codeSnippet: callExpr.text.substring(0, 80),
739
+ astNodeType: 'call_expression'
740
+ });
741
+ }
742
+ });
743
+ } catch (error) {
744
+ logger.error('检测console.log失败:', error);
745
+ }
746
+
747
+ return issues;
748
+ }
749
+
750
+ /**
751
+ * 检测重复代码
752
+ */
753
+ function detectDuplicateCode(tree, sourceCode, filePath) {
754
+ const issues = [];
755
+
756
+ try {
757
+ const lines = sourceCode.split('\n');
758
+ const minDuplicateLines = 3;
759
+ const seenBlocks = new Map();
760
+
761
+ for (let i = 0; i < lines.length - minDuplicateLines; i++) {
762
+ const block = lines.slice(i, i + minDuplicateLines).join('\n').trim();
763
+ if (block.length < 20) continue;
764
+
765
+ const trimmed = block.replace(/\s+/g, '');
766
+ if (seenBlocks.has(trimmed)) {
767
+ const previousLine = seenBlocks.get(trimmed);
768
+ if (i > previousLine + minDuplicateLines) {
769
+ issues.push({
770
+ id: generateUUID(),
771
+ filePath,
772
+ fileName: filePath.split(/[/\\]/).pop(),
773
+ language: getFileLanguage(filePath),
774
+ issueType: 'duplicate_code',
775
+ severity: detectionRules.duplicateCode.severity,
776
+ message: `发现重复代码片段(第${previousLine + 1}行和第${i + 1}行)`,
777
+ suggestion: detectionRules.duplicateCode.suggestion,
778
+ lineStart: i + 1,
779
+ lineEnd: i + minDuplicateLines,
780
+ columnStart: 0,
781
+ columnEnd: block.length,
782
+ codeSnippet: block.substring(0, 100),
783
+ astNodeType: 'duplicate_block'
784
+ });
785
+ }
786
+ } else {
787
+ seenBlocks.set(trimmed, i);
788
+ }
789
+ }
790
+ } catch (error) {
791
+ logger.error('检测重复代码失败:', error);
792
+ }
793
+
794
+ return issues;
795
+ }
796
+
797
+ /**
798
+ * 检测缺少注释
799
+ */
800
+ function detectMissingComments(tree, sourceCode, filePath) {
801
+ const issues = [];
802
+
803
+ try {
804
+ const functionDeclarations = getNodesByType(tree.rootNode, 'function_declaration');
805
+
806
+ functionDeclarations.forEach(funcDecl => {
807
+ const funcLines = funcDecl.endPosition.row - funcDecl.startPosition.row + 1;
808
+ if (funcLines > 10) {
809
+ const lineNum = funcDecl.startPosition.row;
810
+ let hasComment = false;
811
+
812
+ if (lineNum > 0) {
813
+ const previousLine = sourceCode.split('\n')[lineNum - 1];
814
+ if (previousLine.trim().startsWith('//') || previousLine.trim().startsWith('/**') || previousLine.trim().startsWith('/*')) {
815
+ hasComment = true;
816
+ }
817
+ }
818
+
819
+ if (!hasComment) {
820
+ const funcNameMatch = funcDecl.text.match(/function\s+(\w+)/);
821
+ const funcName = funcNameMatch ? funcNameMatch[1] : 'anonymous';
822
+
823
+ issues.push({
824
+ id: generateUUID(),
825
+ filePath,
826
+ fileName: filePath.split(/[/\\]/).pop(),
827
+ language: getFileLanguage(filePath),
828
+ issueType: 'missing_comment',
829
+ severity: detectionRules.missingComments.severity,
830
+ message: `函数"${funcName}"缺少注释说明`,
831
+ suggestion: detectionRules.missingComments.suggestion,
832
+ lineStart: funcDecl.startPosition.row + 1,
833
+ lineEnd: funcDecl.endPosition.row + 1,
834
+ columnStart: funcDecl.startPosition.column,
835
+ columnEnd: funcDecl.endPosition.column,
836
+ codeSnippet: funcDecl.text.substring(0, 50),
837
+ astNodeType: 'function_declaration'
838
+ });
839
+ }
840
+ }
841
+ });
842
+ } catch (error) {
843
+ logger.error('检测缺少注释失败:', error);
844
+ }
845
+
846
+ return issues;
847
+ }
848
+
849
+ /**
850
+ * 批量检测多个文件
851
+ */
852
+ async function batchDetect(filePaths, options = {}) {
853
+ const results = [];
854
+ const startTime = Date.now();
855
+
856
+ for (const filePath of filePaths) {
857
+ try {
858
+ const sourceCode = require('fs').readFileSync(filePath, 'utf-8');
859
+ const result = await detectIssues(sourceCode, filePath, options);
860
+ results.push(result);
861
+ } catch (error) {
862
+ logger.error(`批量检测文件失败: ${filePath}`, error);
863
+ results.push({
864
+ success: false,
865
+ message: error.message,
866
+ filePath
867
+ });
868
+ }
869
+ }
870
+
871
+ return {
872
+ success: true,
873
+ totalFiles: filePaths.length,
874
+ scannedFiles: results.filter(r => r.success).length,
875
+ failedFiles: results.filter(r => !r.success).length,
876
+ totalIssues: results.reduce((sum, r) => sum + (r.totalIssues || 0), 0),
877
+ results,
878
+ durationMs: Date.now() - startTime
879
+ };
880
+ }
881
+
882
+ /**
883
+ * 存储检测结果到数据库
884
+ */
885
+ async function saveDetectionResults(taskId, projectId, results) {
886
+ try {
887
+ const db = getDatabase();
888
+
889
+ const stmt = db.prepare(`
890
+ INSERT INTO code_issue
891
+ (id, task_id, project_id, file_path, file_name, language, issue_type,
892
+ severity, message, suggestion, line_start, line_end, column_start,
893
+ column_end, code_snippet, ast_node_type)
894
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
895
+ `);
896
+
897
+ const insertMany = db.transaction((issues) => {
898
+ for (const issue of issues) {
899
+ stmt.run(
900
+ issue.id,
901
+ taskId,
902
+ projectId,
903
+ issue.filePath,
904
+ issue.fileName,
905
+ issue.language,
906
+ issue.issueType,
907
+ issue.severity,
908
+ issue.message,
909
+ issue.suggestion,
910
+ issue.lineStart,
911
+ issue.lineEnd,
912
+ issue.columnStart,
913
+ issue.columnEnd,
914
+ issue.codeSnippet,
915
+ issue.astNodeType
916
+ );
917
+ }
918
+ });
919
+
920
+ const allIssues = results.flatMap(r => r.issues || []);
921
+ insertMany(allIssues);
922
+
923
+ logger.info(`存储检测结果: ${allIssues.length} 个问题`);
924
+ } catch (error) {
925
+ logger.error('存储检测结果失败:', error);
926
+ }
927
+ }
928
+
929
+ module.exports = {
930
+ detectIssues,
931
+ batchDetect,
932
+ saveDetectionResults,
933
+ detectionRules
934
+ };