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.
- package/.env.example +145 -0
- package/database/schema.sql +187 -0
- package/package.json +74 -0
- package/scripts/download-tree-sitter.js +171 -0
- package/scripts/postinstall.js +134 -0
- package/src/agent/agent.js +563 -0
- package/src/agent.js +87 -0
- package/src/cli/index.js +1643 -0
- package/src/config/index.js +232 -0
- package/src/engine/dualModeEngine.js +486 -0
- package/src/index.js +165 -0
- package/src/middlewares/errorHandler.js +166 -0
- package/src/middlewares/index.js +23 -0
- package/src/routes/aiRoutes.js +117 -0
- package/src/routes/configRoutes.js +31 -0
- package/src/routes/index.js +75 -0
- package/src/routes/issueRoutes.js +195 -0
- package/src/routes/projectRoutes.js +46 -0
- package/src/routes/reportRoutes.js +40 -0
- package/src/routes/scanRoutes.js +245 -0
- package/src/routes/userRoutes.js +47 -0
- package/src/services/ast/parser.js +503 -0
- package/src/services/detection/detector.js +934 -0
- package/src/services/llm/providers.js +1107 -0
- package/src/services/rag/agent.js +375 -0
- package/src/services/vector/knowledgeBase.js +863 -0
- package/src/skills/Skill.js +38 -0
- package/src/skills/code-analysis/index.js +272 -0
- package/src/skills/code-detection/index.js +166 -0
- package/src/skills/code-detection/rules/console-log.js +45 -0
- package/src/skills/code-detection/rules/deep-nesting.js +76 -0
- package/src/skills/code-detection/rules/duplicate-code.js +57 -0
- package/src/skills/code-detection/rules/high-complexity.js +109 -0
- package/src/skills/code-detection/rules/index.js +59 -0
- package/src/skills/code-detection/rules/long-functions.js +54 -0
- package/src/skills/code-detection/rules/magic-numbers.js +48 -0
- package/src/skills/code-detection/rules/missing-comment.js +64 -0
- package/src/skills/code-detection/rules/null-check.js +71 -0
- package/src/skills/code-detection/rules/unnecessary-else.js +46 -0
- package/src/skills/code-detection/rules/unused-functions.js +57 -0
- package/src/skills/code-detection/rules/unused-imports.js +57 -0
- package/src/skills/code-detection/rules/unused-variables.js +54 -0
- package/src/skills/code-optimization/index.js +319 -0
- package/src/skills/index.js +152 -0
- package/src/utils/crypto.js +212 -0
- package/src/utils/database.js +125 -0
- package/src/utils/helpers.js +226 -0
- package/src/utils/logger.js +202 -0
- package/src/utils/mysql.js +198 -0
- package/src/utils/response.js +124 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
const { generateUUID, getFileLanguage } = require('../../../utils/helpers');
|
|
2
|
+
|
|
3
|
+
const meta = {
|
|
4
|
+
id: 'high_complexity',
|
|
5
|
+
name: '圈复杂度检测',
|
|
6
|
+
description: '检测圈复杂度过高的函数',
|
|
7
|
+
category: 'code-smell',
|
|
8
|
+
defaultSeverity: 'medium'
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
const DEFAULT_THRESHOLD = 10;
|
|
12
|
+
|
|
13
|
+
function detect(tree, sourceCode, filePath, ruleConfig) {
|
|
14
|
+
const issues = [];
|
|
15
|
+
const threshold = ruleConfig.threshold || DEFAULT_THRESHOLD;
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
const lines = sourceCode.split('\n');
|
|
19
|
+
const funcDeclarations = [];
|
|
20
|
+
|
|
21
|
+
for (let i = 0; i < lines.length; i++) {
|
|
22
|
+
const match = lines[i].trim().match(/function\s+(\w+)\s*\(/);
|
|
23
|
+
if (match) {
|
|
24
|
+
let braceCount = 0;
|
|
25
|
+
const hasOpenBrace = lines[i].includes('{');
|
|
26
|
+
if (hasOpenBrace) braceCount++;
|
|
27
|
+
|
|
28
|
+
let endLine = i;
|
|
29
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
30
|
+
const lineBraces = (lines[j].match(/\{/g) || []).length;
|
|
31
|
+
const closeBraces = (lines[j].match(/\}/g) || []).length;
|
|
32
|
+
braceCount += lineBraces - closeBraces;
|
|
33
|
+
|
|
34
|
+
if (braceCount <= 0) {
|
|
35
|
+
endLine = j;
|
|
36
|
+
break;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
funcDeclarations.push({
|
|
41
|
+
name: match[1],
|
|
42
|
+
startLine: i,
|
|
43
|
+
endLine: endLine,
|
|
44
|
+
text: lines[i].trim()
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
funcDeclarations.forEach(funcDecl => {
|
|
50
|
+
let complexity = 1;
|
|
51
|
+
|
|
52
|
+
for (let i = funcDecl.startLine; i <= funcDecl.endLine; i++) {
|
|
53
|
+
const line = lines[i];
|
|
54
|
+
if (!line) continue;
|
|
55
|
+
|
|
56
|
+
const trimmed = line.trim();
|
|
57
|
+
|
|
58
|
+
if (trimmed.startsWith('if (') || trimmed.startsWith('if(')) {
|
|
59
|
+
complexity++;
|
|
60
|
+
}
|
|
61
|
+
if (trimmed.startsWith('for (')) {
|
|
62
|
+
complexity++;
|
|
63
|
+
}
|
|
64
|
+
if (trimmed.startsWith('while (')) {
|
|
65
|
+
complexity++;
|
|
66
|
+
}
|
|
67
|
+
if (trimmed.startsWith('case ')) {
|
|
68
|
+
complexity++;
|
|
69
|
+
}
|
|
70
|
+
if (trimmed.startsWith('catch (')) {
|
|
71
|
+
complexity++;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const andCount = (trimmed.match(/&&/g) || []).length;
|
|
75
|
+
const orCount = (trimmed.match(/\|\|/g) || []).length;
|
|
76
|
+
complexity += andCount + orCount;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (complexity > threshold) {
|
|
80
|
+
issues.push({
|
|
81
|
+
id: generateUUID(),
|
|
82
|
+
filePath,
|
|
83
|
+
fileName: filePath.split(/[/\\]/).pop(),
|
|
84
|
+
language: getFileLanguage(filePath),
|
|
85
|
+
issueType: 'high_complexity',
|
|
86
|
+
severity: ruleConfig.severity || meta.defaultSeverity,
|
|
87
|
+
message: `函数"${funcDecl.name}"圈复杂度过高(${complexity})`,
|
|
88
|
+
suggestion: '简化逻辑结构,减少嵌套层级',
|
|
89
|
+
lineStart: funcDecl.startLine + 1,
|
|
90
|
+
lineEnd: funcDecl.endLine + 1,
|
|
91
|
+
columnStart: 0,
|
|
92
|
+
columnEnd: funcDecl.text.length,
|
|
93
|
+
codeSnippet: funcDecl.text.substring(0, 100),
|
|
94
|
+
astNodeType: 'function_declaration',
|
|
95
|
+
metadata: {
|
|
96
|
+
complexity,
|
|
97
|
+
threshold
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
} catch (error) {
|
|
103
|
+
console.error('检测圈复杂度失败:', error);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return issues;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
module.exports = { meta, detect };
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 检测规则索引
|
|
3
|
+
* 所有检测规则的统一入口
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { config } = require('../../../config');
|
|
7
|
+
|
|
8
|
+
const rules = {
|
|
9
|
+
unused_variables: require('./unused-variables'),
|
|
10
|
+
unused_imports: require('./unused-imports'),
|
|
11
|
+
unused_functions: require('./unused-functions'),
|
|
12
|
+
magic_numbers: require('./magic-numbers'),
|
|
13
|
+
long_functions: require('./long-functions'),
|
|
14
|
+
high_complexity: require('./high-complexity'),
|
|
15
|
+
deep_nesting: require('./deep-nesting'),
|
|
16
|
+
null_check: require('./null-check'),
|
|
17
|
+
unnecessary_else: require('./unnecessary-else'),
|
|
18
|
+
console_log: require('./console-log'),
|
|
19
|
+
duplicate_code: require('./duplicate-code'),
|
|
20
|
+
missing_comment: require('./missing-comment')
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function getRuleConfig(ruleId) {
|
|
24
|
+
const ruleConfigMap = {
|
|
25
|
+
unused_variables: { enabled: config.detection.unusedVariables, severity: 'medium' },
|
|
26
|
+
unused_imports: { enabled: config.detection.unusedImports, severity: 'low' },
|
|
27
|
+
unused_functions: { enabled: config.detection.unusedFunctions, severity: 'high' },
|
|
28
|
+
magic_numbers: { enabled: config.detection.magicNumbers, severity: 'low' },
|
|
29
|
+
long_functions: { enabled: true, severity: 'medium', threshold: config.detection.maxFunctionLines },
|
|
30
|
+
high_complexity: { enabled: true, severity: 'medium', threshold: config.detection.maxCyclomaticComplexity },
|
|
31
|
+
deep_nesting: { enabled: config.detection.enableDeepNestingCheck, severity: 'medium', threshold: config.detection.maxNestingDepth },
|
|
32
|
+
null_check: { enabled: config.detection.enableNullCheck, severity: 'high' },
|
|
33
|
+
unnecessary_else: { enabled: true, severity: 'low' },
|
|
34
|
+
console_log: { enabled: config.detection.enableConsoleLogCheck, severity: 'low' },
|
|
35
|
+
duplicate_code: { enabled: config.detection.enableDuplicateCodeCheck, severity: 'medium' },
|
|
36
|
+
missing_comment: { enabled: config.detection.enableCommentCheck, severity: 'low' }
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
return ruleConfigMap[ruleId] || { enabled: false, severity: 'low' };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function getAllRules() {
|
|
43
|
+
return Object.entries(rules).map(([id, rule]) => ({
|
|
44
|
+
id,
|
|
45
|
+
...rule.meta,
|
|
46
|
+
config: getRuleConfig(id)
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getEnabledRules() {
|
|
51
|
+
return getAllRules().filter(rule => rule.config.enabled);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = {
|
|
55
|
+
rules,
|
|
56
|
+
getRuleConfig,
|
|
57
|
+
getAllRules,
|
|
58
|
+
getEnabledRules
|
|
59
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const { getNodesByType } = require('../../../services/ast/parser');
|
|
2
|
+
const { generateUUID, getFileLanguage } = require('../../../utils/helpers');
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
id: 'long_functions',
|
|
6
|
+
name: '过长函数检测',
|
|
7
|
+
description: '检测行数超过阈值的函数',
|
|
8
|
+
category: 'code-smell',
|
|
9
|
+
defaultSeverity: 'medium'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const DEFAULT_THRESHOLD = 50;
|
|
13
|
+
|
|
14
|
+
function detect(tree, sourceCode, filePath, ruleConfig) {
|
|
15
|
+
const issues = [];
|
|
16
|
+
const threshold = ruleConfig.threshold || DEFAULT_THRESHOLD;
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const functionDeclarations = getNodesByType(tree.rootNode, 'function_declaration');
|
|
20
|
+
|
|
21
|
+
functionDeclarations.forEach(funcDecl => {
|
|
22
|
+
const funcLines = funcDecl.endPosition.row - funcDecl.startPosition.row + 1;
|
|
23
|
+
|
|
24
|
+
if (funcLines > threshold) {
|
|
25
|
+
issues.push({
|
|
26
|
+
id: generateUUID(),
|
|
27
|
+
filePath,
|
|
28
|
+
fileName: filePath.split(/[/\\]/).pop(),
|
|
29
|
+
language: getFileLanguage(filePath),
|
|
30
|
+
issueType: 'long_function',
|
|
31
|
+
severity: ruleConfig.severity || meta.defaultSeverity,
|
|
32
|
+
message: `函数过长(${funcLines}行),建议拆分`,
|
|
33
|
+
suggestion: '将长函数拆分为多个小函数,每个函数负责单一职责',
|
|
34
|
+
lineStart: funcDecl.startPosition.row + 1,
|
|
35
|
+
lineEnd: funcDecl.endPosition.row + 1,
|
|
36
|
+
columnStart: funcDecl.startPosition.column,
|
|
37
|
+
columnEnd: funcDecl.endPosition.column,
|
|
38
|
+
codeSnippet: funcDecl.text.substring(0, 100),
|
|
39
|
+
astNodeType: 'function_declaration',
|
|
40
|
+
metadata: {
|
|
41
|
+
lineCount: funcLines,
|
|
42
|
+
threshold
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error('检测过长函数失败:', error);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return issues;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { meta, detect };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
const { getNodesByType } = require('../../../services/ast/parser');
|
|
2
|
+
const { generateUUID, getFileLanguage } = require('../../../utils/helpers');
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
id: 'magic_numbers',
|
|
6
|
+
name: '魔法数字检测',
|
|
7
|
+
description: '检测代码中出现的魔法数字',
|
|
8
|
+
category: 'code-smell',
|
|
9
|
+
defaultSeverity: 'low'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function detect(tree, sourceCode, filePath, ruleConfig) {
|
|
13
|
+
const issues = [];
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const numberNodes = getNodesByType(tree.rootNode, 'number');
|
|
17
|
+
const magicThresholds = [100, 1000, 60, 24, 7, 30, 365, 86400];
|
|
18
|
+
|
|
19
|
+
numberNodes.forEach(numNode => {
|
|
20
|
+
const numValue = parseFloat(numNode.text);
|
|
21
|
+
|
|
22
|
+
if (magicThresholds.includes(numValue) || numValue > 1000) {
|
|
23
|
+
issues.push({
|
|
24
|
+
id: generateUUID(),
|
|
25
|
+
filePath,
|
|
26
|
+
fileName: filePath.split(/[/\\]/).pop(),
|
|
27
|
+
language: getFileLanguage(filePath),
|
|
28
|
+
issueType: 'magic_number',
|
|
29
|
+
severity: ruleConfig.severity || meta.defaultSeverity,
|
|
30
|
+
message: `发现魔法数字: ${numValue}`,
|
|
31
|
+
suggestion: '将魔法数字提取为常量并添加说明性名称',
|
|
32
|
+
lineStart: numNode.startPosition.row + 1,
|
|
33
|
+
lineEnd: numNode.endPosition.row + 1,
|
|
34
|
+
columnStart: numNode.startPosition.column,
|
|
35
|
+
columnEnd: numNode.endPosition.column,
|
|
36
|
+
codeSnippet: numNode.text,
|
|
37
|
+
astNodeType: 'number'
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
} catch (error) {
|
|
42
|
+
console.error('检测魔法数字失败:', error);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return issues;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
module.exports = { meta, detect };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const { getNodesByType } = require('../../../services/ast/parser');
|
|
2
|
+
const { generateUUID, getFileLanguage } = require('../../../utils/helpers');
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
id: 'missing_comment',
|
|
6
|
+
name: '缺少注释检测',
|
|
7
|
+
description: '检测较长函数是否缺少注释说明',
|
|
8
|
+
category: 'code-smell',
|
|
9
|
+
defaultSeverity: 'low'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const DEFAULT_MIN_LINES = 10;
|
|
13
|
+
|
|
14
|
+
function detect(tree, sourceCode, filePath, ruleConfig) {
|
|
15
|
+
const issues = [];
|
|
16
|
+
const minLines = ruleConfig.threshold || DEFAULT_MIN_LINES;
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const functionDeclarations = getNodesByType(tree.rootNode, 'function_declaration');
|
|
20
|
+
|
|
21
|
+
functionDeclarations.forEach(funcDecl => {
|
|
22
|
+
const funcLines = funcDecl.endPosition.row - funcDecl.startPosition.row + 1;
|
|
23
|
+
if (funcLines > minLines) {
|
|
24
|
+
const lineNum = funcDecl.startPosition.row;
|
|
25
|
+
let hasComment = false;
|
|
26
|
+
|
|
27
|
+
if (lineNum > 0) {
|
|
28
|
+
const previousLine = sourceCode.split('\n')[lineNum - 1];
|
|
29
|
+
if (previousLine.trim().startsWith('//') || previousLine.trim().startsWith('/**') || previousLine.trim().startsWith('/*')) {
|
|
30
|
+
hasComment = true;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!hasComment) {
|
|
35
|
+
const funcNameMatch = funcDecl.text.match(/function\s+(\w+)/);
|
|
36
|
+
const funcName = funcNameMatch ? funcNameMatch[1] : 'anonymous';
|
|
37
|
+
|
|
38
|
+
issues.push({
|
|
39
|
+
id: generateUUID(),
|
|
40
|
+
filePath,
|
|
41
|
+
fileName: filePath.split(/[/\\]/).pop(),
|
|
42
|
+
language: getFileLanguage(filePath),
|
|
43
|
+
issueType: 'missing_comment',
|
|
44
|
+
severity: ruleConfig.severity || meta.defaultSeverity,
|
|
45
|
+
message: `函数"${funcName}"缺少注释说明`,
|
|
46
|
+
suggestion: '添加函数说明注释,提高代码可读性',
|
|
47
|
+
lineStart: funcDecl.startPosition.row + 1,
|
|
48
|
+
lineEnd: funcDecl.endPosition.row + 1,
|
|
49
|
+
columnStart: funcDecl.startPosition.column,
|
|
50
|
+
columnEnd: funcDecl.endPosition.column,
|
|
51
|
+
codeSnippet: funcDecl.text.substring(0, 50),
|
|
52
|
+
astNodeType: 'function_declaration'
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error('检测缺少注释失败:', error);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return issues;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = { meta, detect };
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
const { getNodesByType, traverseAST } = require('../../../services/ast/parser');
|
|
2
|
+
const { generateUUID, getFileLanguage } = require('../../../utils/helpers');
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
id: 'null_check',
|
|
6
|
+
name: '空值检查检测',
|
|
7
|
+
description: '检测函数参数缺少空值检查的情况',
|
|
8
|
+
category: 'bug-risk',
|
|
9
|
+
defaultSeverity: 'high'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function detect(tree, sourceCode, filePath, ruleConfig) {
|
|
13
|
+
const issues = [];
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const functionDeclarations = getNodesByType(tree.rootNode, 'function_declaration');
|
|
17
|
+
|
|
18
|
+
functionDeclarations.forEach(funcDecl => {
|
|
19
|
+
const identifiers = getNodesByType(funcDecl, 'identifier');
|
|
20
|
+
const params = funcDecl.text.match(/function\s+\w+\s*\(([^)]*)\)/);
|
|
21
|
+
|
|
22
|
+
if (params) {
|
|
23
|
+
const paramNames = params[1].split(',').map(p => p.trim()).filter(p => p.length > 0);
|
|
24
|
+
|
|
25
|
+
paramNames.forEach(param => {
|
|
26
|
+
const paramUsages = identifiers.filter(id => id.text === param);
|
|
27
|
+
if (paramUsages.length > 1) {
|
|
28
|
+
let hasNullCheck = false;
|
|
29
|
+
|
|
30
|
+
traverseAST(funcDecl, (node) => {
|
|
31
|
+
if (node.type === 'if_statement') {
|
|
32
|
+
if (node.text.includes(`!${param}`) || node.text.includes(`${param} !== null`) ||
|
|
33
|
+
node.text.includes(`${param} !== undefined`) || node.text.includes(`${param} != null`)) {
|
|
34
|
+
hasNullCheck = true;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!hasNullCheck && paramUsages.length > 2) {
|
|
40
|
+
issues.push({
|
|
41
|
+
id: generateUUID(),
|
|
42
|
+
filePath,
|
|
43
|
+
fileName: filePath.split(/[/\\]/).pop(),
|
|
44
|
+
language: getFileLanguage(filePath),
|
|
45
|
+
issueType: 'null_check',
|
|
46
|
+
severity: ruleConfig.severity || meta.defaultSeverity,
|
|
47
|
+
message: `参数"${param}"缺少空值检查`,
|
|
48
|
+
suggestion: '添加空值检查以防止运行时错误',
|
|
49
|
+
lineStart: funcDecl.startPosition.row + 1,
|
|
50
|
+
lineEnd: funcDecl.endPosition.row + 1,
|
|
51
|
+
columnStart: funcDecl.startPosition.column,
|
|
52
|
+
columnEnd: funcDecl.endPosition.column,
|
|
53
|
+
codeSnippet: funcDecl.text.substring(0, 100),
|
|
54
|
+
astNodeType: 'function_declaration',
|
|
55
|
+
metadata: {
|
|
56
|
+
paramName: param
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error('检测空值检查失败:', error);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return issues;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { meta, detect };
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const { traverseAST } = require('../../../services/ast/parser');
|
|
2
|
+
const { generateUUID, getFileLanguage } = require('../../../utils/helpers');
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
id: 'unnecessary_else',
|
|
6
|
+
name: '不必要else检测',
|
|
7
|
+
description: '检测return语句后不必要的else语句',
|
|
8
|
+
category: 'code-smell',
|
|
9
|
+
defaultSeverity: 'low'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function detect(tree, sourceCode, filePath, ruleConfig) {
|
|
13
|
+
const issues = [];
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
traverseAST(tree.rootNode, (node) => {
|
|
17
|
+
if (node.type === 'if_statement' && node.text.includes('else')) {
|
|
18
|
+
const ifBody = node.text.split('else')[0];
|
|
19
|
+
if (ifBody.includes('return ') || ifBody.includes('throw ')) {
|
|
20
|
+
issues.push({
|
|
21
|
+
id: generateUUID(),
|
|
22
|
+
filePath,
|
|
23
|
+
fileName: filePath.split(/[/\\]/).pop(),
|
|
24
|
+
language: getFileLanguage(filePath),
|
|
25
|
+
issueType: 'unnecessary_else',
|
|
26
|
+
severity: ruleConfig.severity || meta.defaultSeverity,
|
|
27
|
+
message: 'return后使用不必要的else',
|
|
28
|
+
suggestion: '删除return后的else语句以简化代码',
|
|
29
|
+
lineStart: node.startPosition.row + 1,
|
|
30
|
+
lineEnd: node.endPosition.row + 1,
|
|
31
|
+
columnStart: node.startPosition.column,
|
|
32
|
+
columnEnd: node.endPosition.column,
|
|
33
|
+
codeSnippet: node.text.substring(0, 80),
|
|
34
|
+
astNodeType: 'if_statement'
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error('检测不必要else失败:', error);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return issues;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = { meta, detect };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const { getNodesByType } = require('../../../services/ast/parser');
|
|
2
|
+
const { generateUUID, getFileLanguage } = require('../../../utils/helpers');
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
id: 'unused_functions',
|
|
6
|
+
name: '未使用函数检测',
|
|
7
|
+
description: '检测已定义但从未被调用的函数',
|
|
8
|
+
category: 'code-smell',
|
|
9
|
+
defaultSeverity: 'high'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function detect(tree, sourceCode, filePath, ruleConfig) {
|
|
13
|
+
const issues = [];
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const functionDeclarations = getNodesByType(tree.rootNode, 'function_declaration');
|
|
17
|
+
const identifiers = getNodesByType(tree.rootNode, 'identifier');
|
|
18
|
+
|
|
19
|
+
functionDeclarations.forEach(funcDecl => {
|
|
20
|
+
const funcNameMatch = funcDecl.text.match(/function\s+(\w+)\s*\(/);
|
|
21
|
+
|
|
22
|
+
if (funcNameMatch) {
|
|
23
|
+
const funcName = funcNameMatch[1];
|
|
24
|
+
|
|
25
|
+
const isCalled = identifiers.some(id =>
|
|
26
|
+
id.text === funcName &&
|
|
27
|
+
id.startPosition.row !== funcDecl.startPosition.row
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
if (!isCalled) {
|
|
31
|
+
issues.push({
|
|
32
|
+
id: generateUUID(),
|
|
33
|
+
filePath,
|
|
34
|
+
fileName: filePath.split(/[/\\]/).pop(),
|
|
35
|
+
language: getFileLanguage(filePath),
|
|
36
|
+
issueType: 'unused_function',
|
|
37
|
+
severity: ruleConfig.severity || meta.defaultSeverity,
|
|
38
|
+
message: `函数"${funcName}"已定义但从未被调用`,
|
|
39
|
+
suggestion: '删除未使用的函数或确认是否应该在代码中使用它',
|
|
40
|
+
lineStart: funcDecl.startPosition.row + 1,
|
|
41
|
+
lineEnd: funcDecl.endPosition.row + 1,
|
|
42
|
+
columnStart: funcDecl.startPosition.column,
|
|
43
|
+
columnEnd: funcDecl.endPosition.column,
|
|
44
|
+
codeSnippet: funcDecl.text.substring(0, 100),
|
|
45
|
+
astNodeType: 'function_declaration'
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error('检测未使用函数失败:', error);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return issues;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { meta, detect };
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const { getNodesByType } = require('../../../services/ast/parser');
|
|
2
|
+
const { generateUUID, getFileLanguage } = require('../../../utils/helpers');
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
id: 'unused_imports',
|
|
6
|
+
name: '未使用导入检测',
|
|
7
|
+
description: '检测已声明但从未使用的导入',
|
|
8
|
+
category: 'code-smell',
|
|
9
|
+
defaultSeverity: 'low'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function detect(tree, sourceCode, filePath, ruleConfig) {
|
|
13
|
+
const issues = [];
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const importStatements = getNodesByType(tree.rootNode, 'import_statement');
|
|
17
|
+
const identifiers = getNodesByType(tree.rootNode, 'identifier');
|
|
18
|
+
|
|
19
|
+
importStatements.forEach(importStmt => {
|
|
20
|
+
const importMatch = importStmt.text.match(/import\s+(\w+)\s+from/);
|
|
21
|
+
|
|
22
|
+
if (importMatch) {
|
|
23
|
+
const importedName = importMatch[1];
|
|
24
|
+
|
|
25
|
+
const isUsed = identifiers.some(id =>
|
|
26
|
+
id.text === importedName &&
|
|
27
|
+
id.startPosition.row !== importStmt.startPosition.row
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
if (!isUsed) {
|
|
31
|
+
issues.push({
|
|
32
|
+
id: generateUUID(),
|
|
33
|
+
filePath,
|
|
34
|
+
fileName: filePath.split(/[/\\]/).pop(),
|
|
35
|
+
language: getFileLanguage(filePath),
|
|
36
|
+
issueType: 'unused_import',
|
|
37
|
+
severity: ruleConfig.severity || meta.defaultSeverity,
|
|
38
|
+
message: `导入"${importedName}"已声明但从未使用`,
|
|
39
|
+
suggestion: '删除未使用的导入以减少打包体积',
|
|
40
|
+
lineStart: importStmt.startPosition.row + 1,
|
|
41
|
+
lineEnd: importStmt.endPosition.row + 1,
|
|
42
|
+
columnStart: importStmt.startPosition.column,
|
|
43
|
+
columnEnd: importStmt.endPosition.column,
|
|
44
|
+
codeSnippet: importStmt.text,
|
|
45
|
+
astNodeType: 'import_statement'
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
});
|
|
50
|
+
} catch (error) {
|
|
51
|
+
console.error('检测未使用导入失败:', error);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return issues;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { meta, detect };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const { getNodesByType } = require('../../../services/ast/parser');
|
|
2
|
+
const { generateUUID, getFileLanguage } = require('../../../utils/helpers');
|
|
3
|
+
|
|
4
|
+
const meta = {
|
|
5
|
+
id: 'unused_variables',
|
|
6
|
+
name: '未使用变量检测',
|
|
7
|
+
description: '检测已声明但从未使用的变量',
|
|
8
|
+
category: 'code-smell',
|
|
9
|
+
defaultSeverity: 'medium'
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
function detect(tree, sourceCode, filePath, ruleConfig) {
|
|
13
|
+
const issues = [];
|
|
14
|
+
|
|
15
|
+
try {
|
|
16
|
+
const variableDeclarations = getNodesByType(tree.rootNode, 'variable_declarator');
|
|
17
|
+
const identifiers = getNodesByType(tree.rootNode, 'identifier');
|
|
18
|
+
|
|
19
|
+
variableDeclarations.forEach(varDecl => {
|
|
20
|
+
const varName = varDecl.text.split('=')[0].trim();
|
|
21
|
+
|
|
22
|
+
const isUsed = identifiers.some(id => {
|
|
23
|
+
return id.text === varName &&
|
|
24
|
+
(id.startPosition.row !== varDecl.startPosition.row ||
|
|
25
|
+
id.startPosition.column !== varDecl.startPosition.column);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!isUsed) {
|
|
29
|
+
issues.push({
|
|
30
|
+
id: generateUUID(),
|
|
31
|
+
filePath,
|
|
32
|
+
fileName: filePath.split(/[/\\]/).pop(),
|
|
33
|
+
language: getFileLanguage(filePath),
|
|
34
|
+
issueType: 'unused_variable',
|
|
35
|
+
severity: ruleConfig.severity || meta.defaultSeverity,
|
|
36
|
+
message: `变量"${varName}"已声明但从未使用`,
|
|
37
|
+
suggestion: '删除未使用的变量或根据需要使用它',
|
|
38
|
+
lineStart: varDecl.startPosition.row + 1,
|
|
39
|
+
lineEnd: varDecl.endPosition.row + 1,
|
|
40
|
+
columnStart: varDecl.startPosition.column,
|
|
41
|
+
columnEnd: varDecl.endPosition.column,
|
|
42
|
+
codeSnippet: varDecl.text,
|
|
43
|
+
astNodeType: 'variable_declarator'
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
} catch (error) {
|
|
48
|
+
console.error('检测未使用变量失败:', error);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return issues;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = { meta, detect };
|