smart-review 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +713 -0
- package/bin/install.js +280 -0
- package/bin/review.js +256 -0
- package/index.js +5 -0
- package/lib/ai-client-pool.js +434 -0
- package/lib/ai-client.js +1413 -0
- package/lib/config-loader.js +223 -0
- package/lib/default-config.js +203 -0
- package/lib/reviewer.js +1340 -0
- package/lib/segmented-analyzer.js +490 -0
- package/lib/smart-batching.js +1671 -0
- package/lib/utils/concurrency-limiter.js +46 -0
- package/lib/utils/constants.js +117 -0
- package/lib/utils/git-diff-parser.js +624 -0
- package/lib/utils/logger.js +66 -0
- package/lib/utils/strip.js +221 -0
- package/package.json +44 -0
- package/templates/rules/best-practices.js +111 -0
- package/templates/rules/performance.js +123 -0
- package/templates/rules/security.js +311 -0
- package/templates/smart-review.json +80 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
|
|
3
|
+
// 复用 reviewer 中的注释范围计算逻辑的轻量版本
|
|
4
|
+
export async function stripCommentsForAI(content, filePath) {
|
|
5
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
6
|
+
const ranges = computeCommentRanges(content, ext);
|
|
7
|
+
if (ranges.length === 0) return content;
|
|
8
|
+
// 直接删除注释内容,并在末尾统一折叠多余空白行
|
|
9
|
+
let result = content;
|
|
10
|
+
ranges.sort((a,b) => b.start - a.start).forEach(r => {
|
|
11
|
+
result = result.slice(0, r.start) + result.slice(r.end);
|
|
12
|
+
});
|
|
13
|
+
// 删除所有空白行:移除行尾空白,滤掉空行
|
|
14
|
+
result = result.replace(/[ \t]+\r?\n/g, '\n');
|
|
15
|
+
result = result.split('\n').filter(line => line.trim().length > 0).join('\n');
|
|
16
|
+
return result;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function computeCommentRanges(content, ext) {
|
|
20
|
+
const ranges = [];
|
|
21
|
+
const pushRange = (start, end) => {
|
|
22
|
+
if (start >= 0 && end > start) ranges.push({ start, end });
|
|
23
|
+
};
|
|
24
|
+
const addByRegex = (regex) => {
|
|
25
|
+
let m;
|
|
26
|
+
while ((m = regex.exec(content)) !== null) {
|
|
27
|
+
pushRange(m.index, m.index + m[0].length);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
const jsLike = ['.js','.jsx','.ts','.tsx','.java','.go','.c','.cpp','.h','.rs','.php'];
|
|
31
|
+
if (jsLike.includes(ext)) {
|
|
32
|
+
addByRegex(/\/\/.*|\/\*[\s\S]*?\*\//g);
|
|
33
|
+
// 在 JSX/TSX 中移除注释包裹:{/* ... */},避免残留孤立的大括号
|
|
34
|
+
if (ext === '.jsx' || ext === '.tsx') {
|
|
35
|
+
// 仅匹配单行的注释包裹:限制为 { + 空格/Tab + /*...*/ + 空格/Tab + }
|
|
36
|
+
// 防止错误地从上一行的 {(如 enum {...})起始到后续任意注释块闭合的 } 形成巨大范围
|
|
37
|
+
addByRegex(/\{[ \t]*\/\*[\s\S]*?\*\/[ \t]*\}/g);
|
|
38
|
+
}
|
|
39
|
+
} else if (ext === '.py' || ext === '.rb') {
|
|
40
|
+
addByRegex(/(^|\s)#.*$/gm);
|
|
41
|
+
} else if (ext === '.html' || ext === '.svelte') {
|
|
42
|
+
addByRegex(/<!--[\s\S]*?-->/g);
|
|
43
|
+
} else if (ext === '.css' || ext === '.scss' || ext === '.less') {
|
|
44
|
+
addByRegex(/\/\*[\s\S]*?\*\//g);
|
|
45
|
+
} else {
|
|
46
|
+
addByRegex(/\/\/.*|\/\*[\s\S]*?\*\//g);
|
|
47
|
+
addByRegex(/(^|\s)#.*$/gm);
|
|
48
|
+
addByRegex(/<!--[\s\S]*?-->/g);
|
|
49
|
+
}
|
|
50
|
+
// 合并重叠区间,避免重复剥离导致索引错位
|
|
51
|
+
if (ranges.length > 1) {
|
|
52
|
+
ranges.sort((a, b) => a.start - b.start);
|
|
53
|
+
const merged = [];
|
|
54
|
+
let prev = ranges[0];
|
|
55
|
+
for (let i = 1; i < ranges.length; i++) {
|
|
56
|
+
const cur = ranges[i];
|
|
57
|
+
if (cur.start <= prev.end) {
|
|
58
|
+
prev.end = Math.max(prev.end, cur.end);
|
|
59
|
+
} else {
|
|
60
|
+
merged.push(prev);
|
|
61
|
+
prev = cur;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
merged.push(prev);
|
|
65
|
+
return merged;
|
|
66
|
+
}
|
|
67
|
+
return ranges;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// 剔除“代码内禁用”范围以避免AI分析受影响(保留换行,稳定行号)
|
|
71
|
+
export async function stripNoReviewForAI(content, filePath) {
|
|
72
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
73
|
+
const commentRanges = computeCommentRanges(content, ext);
|
|
74
|
+
// 固定令牌:与 reviewer 的 computeDisableRanges 保持一致
|
|
75
|
+
const nextToken = 'review-disable-next-line';
|
|
76
|
+
const startToken = 'review-disable-start';
|
|
77
|
+
const endToken = 'review-disable-end';
|
|
78
|
+
|
|
79
|
+
// 每行起始偏移
|
|
80
|
+
const lineOffsets = [];
|
|
81
|
+
const lines = content.split('\n');
|
|
82
|
+
let offset = 0;
|
|
83
|
+
for (const ln of lines) { lineOffsets.push(offset); offset += ln.length + 1; }
|
|
84
|
+
|
|
85
|
+
const suppressRanges = [];
|
|
86
|
+
let pendingBlockStart = null;
|
|
87
|
+
for (const r of commentRanges) {
|
|
88
|
+
const lower = content.slice(r.start, r.end).toLowerCase();
|
|
89
|
+
if (lower.includes(nextToken)) {
|
|
90
|
+
const lineIdx = content.substring(0, r.start).split('\n').length - 1;
|
|
91
|
+
const nextStart = lineOffsets[lineIdx + 1];
|
|
92
|
+
const nextEnd = lineOffsets[lineIdx + 2] ?? content.length;
|
|
93
|
+
if (Number.isFinite(nextStart)) suppressRanges.push({ start: nextStart, end: nextEnd });
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (lower.includes(startToken)) {
|
|
97
|
+
const lineIdx = content.substring(0, r.start).split('\n').length - 1;
|
|
98
|
+
const nextStart = lineOffsets[lineIdx + 1];
|
|
99
|
+
if (Number.isFinite(nextStart)) pendingBlockStart = nextStart;
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (lower.includes(endToken)) {
|
|
103
|
+
const lineIdx = content.substring(0, r.start).split('\n').length - 1;
|
|
104
|
+
const endStart = lineOffsets[lineIdx];
|
|
105
|
+
if (Number.isFinite(pendingBlockStart)) {
|
|
106
|
+
const startPos = pendingBlockStart;
|
|
107
|
+
const endPos = Number.isFinite(endStart) ? endStart : content.length;
|
|
108
|
+
if (startPos < endPos) suppressRanges.push({ start: startPos, end: endPos });
|
|
109
|
+
}
|
|
110
|
+
pendingBlockStart = null;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (Number.isFinite(pendingBlockStart)) {
|
|
115
|
+
suppressRanges.push({ start: pendingBlockStart, end: content.length });
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (suppressRanges.length === 0) return content;
|
|
119
|
+
// 直接删除禁用范围,并折叠空白行以减少无意义的空行
|
|
120
|
+
let result = content;
|
|
121
|
+
suppressRanges.sort((a,b) => b.start - a.start).forEach(r => {
|
|
122
|
+
result = result.slice(0, r.start) + result.slice(r.end);
|
|
123
|
+
});
|
|
124
|
+
// 删除所有空白行:去除行尾空白,并移除空行
|
|
125
|
+
result = result.replace(/[ \t]+\r?\n/g, '\n');
|
|
126
|
+
result = result.split('\n').filter(line => line.trim().length > 0).join('\n');
|
|
127
|
+
return result;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 组合剥离并返回行号映射:对AI输入进行“先映射后剥离”
|
|
131
|
+
// 返回的 lineMap 是按照清洗后每一行对应的源文件行号(1-based)。
|
|
132
|
+
export async function prepareForAIWithLineMap(content, filePath) {
|
|
133
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
134
|
+
const commentRanges = computeCommentRanges(content, ext);
|
|
135
|
+
// 不保留单行文档注释(/** ... */),统一作为注释剔除
|
|
136
|
+
const isPreservedDocBlock = () => false;
|
|
137
|
+
|
|
138
|
+
// 计算每行起始偏移,便于在行维度内裁剪注释片段
|
|
139
|
+
const lines = content.split('\n');
|
|
140
|
+
const lineOffsets = [];
|
|
141
|
+
let offset = 0;
|
|
142
|
+
for (const ln of lines) { lineOffsets.push(offset); offset += ln.length + 1; }
|
|
143
|
+
|
|
144
|
+
// 识别禁用标记所作用的行(保持与 stripNoReviewForAI 语义一致)
|
|
145
|
+
const nextToken = 'review-disable-next-line';
|
|
146
|
+
const startToken = 'review-disable-start';
|
|
147
|
+
const endToken = 'review-disable-end';
|
|
148
|
+
const disabled = new Set();
|
|
149
|
+
let pendingBlockStartLine = null;
|
|
150
|
+
for (const r of commentRanges) {
|
|
151
|
+
const lower = content.slice(r.start, r.end).toLowerCase();
|
|
152
|
+
const lineIdx = content.substring(0, r.start).split('\n').length - 1; // token 所在行索引
|
|
153
|
+
if (lower.includes(nextToken)) {
|
|
154
|
+
const nx = lineIdx + 1;
|
|
155
|
+
if (nx >= 0 && nx < lines.length) disabled.add(nx);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (lower.includes(startToken)) {
|
|
159
|
+
const nx = lineIdx + 1; // 从下一行开始禁用
|
|
160
|
+
if (nx >= 0 && nx < lines.length) pendingBlockStartLine = nx;
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
if (lower.includes(endToken)) {
|
|
164
|
+
const endLine = Math.max(0, lineIdx); // 到 end 标记所在行的上一行结束
|
|
165
|
+
if (pendingBlockStartLine !== null) {
|
|
166
|
+
for (let i = pendingBlockStartLine; i < endLine; i++) {
|
|
167
|
+
disabled.add(i);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
pendingBlockStartLine = null;
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (pendingBlockStartLine !== null) {
|
|
175
|
+
for (let i = pendingBlockStartLine; i < lines.length; i++) {
|
|
176
|
+
disabled.add(i);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 针对每一行,按注释范围做局部裁剪;若行仅剩空白则丢弃
|
|
181
|
+
const cleanLines = [];
|
|
182
|
+
const lineMap = [];
|
|
183
|
+
for (let i = 0; i < lines.length; i++) {
|
|
184
|
+
if (disabled.has(i)) continue; // 整行禁用
|
|
185
|
+
const raw = lines[i];
|
|
186
|
+
const start = lineOffsets[i];
|
|
187
|
+
const end = start + raw.length;
|
|
188
|
+
|
|
189
|
+
// 以当前行的可保留区间为基础,逐个剔除与之相交的注释区间
|
|
190
|
+
let segments = [{ start, end }];
|
|
191
|
+
for (const cr of commentRanges) {
|
|
192
|
+
// 跳过保留的文档注释块
|
|
193
|
+
if (isPreservedDocBlock(cr)) continue;
|
|
194
|
+
if (cr.start >= end || cr.end <= start) continue; // 与该行无交集
|
|
195
|
+
const rs = Math.max(cr.start, start);
|
|
196
|
+
const re = Math.min(cr.end, end);
|
|
197
|
+
const nextSegs = [];
|
|
198
|
+
for (const s of segments) {
|
|
199
|
+
if (re <= s.start || rs >= s.end) {
|
|
200
|
+
nextSegs.push(s);
|
|
201
|
+
} else {
|
|
202
|
+
if (rs > s.start) nextSegs.push({ start: s.start, end: rs });
|
|
203
|
+
if (re < s.end) nextSegs.push({ start: re, end: s.end });
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
segments = nextSegs;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
const rebuilt = segments.map(s => content.slice(s.start, s.end)).join('');
|
|
210
|
+
// 去除行尾空白(含 CR),保持前导缩进;用于决定是否是“空行”再丢弃
|
|
211
|
+
const trimmedRight = rebuilt.replace(/[ \t\r]+$/g, '');
|
|
212
|
+
if (trimmedRight.trim().length === 0) {
|
|
213
|
+
continue; // 清洗后为空行则跳过
|
|
214
|
+
}
|
|
215
|
+
cleanLines.push(trimmedRight);
|
|
216
|
+
lineMap.push(i + 1); // 使用源文件的行号(1-based)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const cleaned = cleanLines.join('\n');
|
|
220
|
+
return { cleaned, clean: cleaned, lineMap };
|
|
221
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "smart-review",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "AI智能代码审查工具,支持静态规则和AI分析",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"smart-review": "./bin/review.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"postinstall": "node ./bin/install.js",
|
|
12
|
+
"dev:install": "node ./bin/install.js",
|
|
13
|
+
"dev:review": "node ./bin/review.js",
|
|
14
|
+
"test:local": "cd test && npm run test-review",
|
|
15
|
+
"test:ai": "node ./bin/review.js --files test/src/index.tsx --ai",
|
|
16
|
+
"debug": "node --inspect ./bin/review.js --files test/src/test-file.js"
|
|
17
|
+
},
|
|
18
|
+
"keywords": ["code-review", "ai", "git-hook", "security"],
|
|
19
|
+
"author": "vlinr",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/vlinr/smart-review.git"
|
|
24
|
+
},
|
|
25
|
+
"homepage": "https://github.com/vlinr/smart-review#readme",
|
|
26
|
+
"bugs": {
|
|
27
|
+
"url": "https://github.com/vlinr/smart-review/issues"
|
|
28
|
+
},
|
|
29
|
+
"dependencies": {
|
|
30
|
+
"chalk": "^5.3.0",
|
|
31
|
+
"glob": "^10.3.10",
|
|
32
|
+
"openai": "^4.20.0",
|
|
33
|
+
"simple-git": "^3.19.1"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"bin/",
|
|
37
|
+
"lib/",
|
|
38
|
+
"templates/",
|
|
39
|
+
"index.js"
|
|
40
|
+
],
|
|
41
|
+
"engines": {
|
|
42
|
+
"node": ">=16.0.0"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// 最佳实践规则
|
|
2
|
+
export default [
|
|
3
|
+
{
|
|
4
|
+
id: 'BP001',
|
|
5
|
+
name: '调试代码',
|
|
6
|
+
pattern: 'console\\.log|print\\(|alert\\(',
|
|
7
|
+
risk: 'low',
|
|
8
|
+
message: '发现调试代码,建议在提交前移除',
|
|
9
|
+
suggestion: '使用日志系统替代console.log',
|
|
10
|
+
flags: 'gi'
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: 'BP002',
|
|
14
|
+
name: '魔法数字',
|
|
15
|
+
pattern: '\\b(?<!\\.)(?!(?:0|1|10|12|24|30|60|100|200|201|300|400|401|403|404|500|503|1000|3000|5000|8080|9000)\\b)\\d{3,}(?!\\.\\d)\\b',
|
|
16
|
+
risk: 'low',
|
|
17
|
+
message: '检测到魔法数字,建议使用常量定义',
|
|
18
|
+
suggestion: '将数字定义为有意义的常量',
|
|
19
|
+
flags: 'g'
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
id: 'BP003',
|
|
23
|
+
name: '空的异常捕获块',
|
|
24
|
+
pattern: 'catch\s*\([^)]*\)\s*\{\s*\}',
|
|
25
|
+
risk: 'medium',
|
|
26
|
+
message: '检测到空的catch块,可能隐藏错误并导致不可预期行为',
|
|
27
|
+
suggestion: '记录日志或采取补救措施,避免吞掉异常',
|
|
28
|
+
flags: 'gi'
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
id: 'BP004',
|
|
32
|
+
name: '忽略TypeScript类型检查',
|
|
33
|
+
pattern: '\\/\\/\\s*@ts-ignore',
|
|
34
|
+
risk: 'medium',
|
|
35
|
+
message: '检测到@ts-ignore,可能掩盖类型错误',
|
|
36
|
+
suggestion: '修复类型问题或使用更精确的类型定义',
|
|
37
|
+
flags: 'gi'
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'BP005',
|
|
41
|
+
name: '使用any类型',
|
|
42
|
+
pattern: ':\\s*any\\b',
|
|
43
|
+
risk: 'medium',
|
|
44
|
+
message: '检测到any类型,可能削弱类型系统保护',
|
|
45
|
+
suggestion: '使用具体类型或泛型替代any,提高类型安全',
|
|
46
|
+
flags: 'gi'
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: 'BP006',
|
|
50
|
+
name: '禁用ESLint规则',
|
|
51
|
+
pattern: '\\/\\/\\s*eslint-disable',
|
|
52
|
+
risk: 'medium',
|
|
53
|
+
message: '检测到禁用ESLint,可能隐藏代码质量问题',
|
|
54
|
+
suggestion: '只在必要范围局部禁用,并给出明确原因',
|
|
55
|
+
flags: 'gi'
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'BP007',
|
|
59
|
+
name: '调试断点未移除',
|
|
60
|
+
pattern: '\\bdebugger\\b',
|
|
61
|
+
risk: 'medium',
|
|
62
|
+
message: '检测到调试断点,可能影响线上行为',
|
|
63
|
+
suggestion: '在提交前移除debugger并使用日志或断言',
|
|
64
|
+
flags: 'gi'
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
id: 'BP008',
|
|
68
|
+
name: '过于宽泛的异常捕获',
|
|
69
|
+
pattern: 'catch\\s*\\(\\s*(Exception|Throwable|Error|BaseException)\\s+\\w+\\s*\\)\\s*\\{[^}]*(?!.*(?:log|throw|rethrow))[^}]*\\}',
|
|
70
|
+
risk: 'medium',
|
|
71
|
+
message: '捕获过于宽泛的异常类型且未进行适当处理',
|
|
72
|
+
suggestion: '捕获具体的异常类型,并确保进行适当的日志记录或重新抛出',
|
|
73
|
+
flags: 'gi'
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'BP009',
|
|
77
|
+
name: '打印堆栈而非日志记录',
|
|
78
|
+
pattern: '\\.printStackTrace\\s*\\(',
|
|
79
|
+
risk: 'medium',
|
|
80
|
+
message: '检测到直接打印堆栈跟踪,可能导致信息丢失与不可控输出',
|
|
81
|
+
suggestion: '使用结构化日志记录错误,并附带上下文信息',
|
|
82
|
+
flags: 'gi'
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 'BP010',
|
|
86
|
+
name: '进程级退出调用',
|
|
87
|
+
pattern: 'System\\.exit\\s*\\(',
|
|
88
|
+
risk: 'high',
|
|
89
|
+
message: '检测到System.exit,可能导致服务非预期中断',
|
|
90
|
+
suggestion: '使用受控的停止流程(优雅关闭)、信号处理与资源回收',
|
|
91
|
+
flags: 'gi'
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: 'BP011',
|
|
95
|
+
name: '使用root数据库用户',
|
|
96
|
+
pattern: '(user|username)\\s*=\\s*root\\b',
|
|
97
|
+
risk: 'medium',
|
|
98
|
+
message: '检测到使用root作为数据库用户,存在安全与审计风险',
|
|
99
|
+
suggestion: '使用最小权限的应用专用账户,分离权限与职责',
|
|
100
|
+
flags: 'gi'
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
id: 'BP012',
|
|
104
|
+
name: '禁用CSRF(Spring Security)',
|
|
105
|
+
pattern: 'csrf\\s*\\(\\)\\.disable\\s*\\(\\)',
|
|
106
|
+
risk: 'high',
|
|
107
|
+
message: '检测到全局禁用CSRF保护,可能导致跨站请求伪造风险',
|
|
108
|
+
suggestion: '在必要的API上采用令牌/同源策略,避免全局关闭',
|
|
109
|
+
flags: 'gi'
|
|
110
|
+
}
|
|
111
|
+
];
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// 性能规则
|
|
2
|
+
export default [
|
|
3
|
+
{
|
|
4
|
+
id: 'PERF001',
|
|
5
|
+
name: '循环内数据库查询',
|
|
6
|
+
pattern: '(for|while)\\s*\\([^)]*\\)\\s*\\{[^}]*\\b(find|query|select|findOne|findMany|findFirst|findUnique|create|update|delete|save)\\s*\\([^}]*\\}',
|
|
7
|
+
risk: 'medium',
|
|
8
|
+
message: '在循环内执行数据库查询,可能导致N+1查询问题',
|
|
9
|
+
suggestion: '使用批量查询或预加载数据',
|
|
10
|
+
flags: 'gi'
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: 'PERF002',
|
|
14
|
+
name: '内存泄漏风险(定时器使用)',
|
|
15
|
+
pattern: 'setInterval\\s*\\([^)]*\\)|setTimeout\\s*\\([^)]*\\)',
|
|
16
|
+
risk: 'medium',
|
|
17
|
+
message: '发现定时器使用,若未清理可能导致内存泄漏或残留任务',
|
|
18
|
+
suggestion: '确保在适当生命周期调用 clearInterval/clearTimeout 进行清理',
|
|
19
|
+
flags: 'gi',
|
|
20
|
+
// 为了覆盖内置 PERF002,外部规则增加清理检测,若文件中存在任一清理则跳过此规则
|
|
21
|
+
requiresAbsent: ['clearInterval\\s*\\(', 'clearTimeout\\s*\\(']
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
id: 'PERF003',
|
|
25
|
+
name: '同步文件IO阻塞',
|
|
26
|
+
pattern: 'fs\\.(readFileSync|writeFileSync|appendFileSync|existsSync|statSync|readdirSync)\\s*\\(',
|
|
27
|
+
risk: 'high',
|
|
28
|
+
message: '检测到同步文件IO,可能阻塞事件循环并影响吞吐',
|
|
29
|
+
suggestion: '优先使用异步IO或队列化处理,避免阻塞主线程',
|
|
30
|
+
flags: 'gi'
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'PERF004',
|
|
34
|
+
name: '循环内网络请求',
|
|
35
|
+
pattern: 'for\s*\([^)]*\)\s*\{[^}]*\b(fetch|axios\.(get|post|put|delete)|requests\.(get|post|put|delete)|http\.get)\b[^}]*\}',
|
|
36
|
+
risk: 'high',
|
|
37
|
+
message: '检测到循环内执行网络请求,可能导致级联延迟与拥塞',
|
|
38
|
+
suggestion: '合并请求、并发控制或批量处理,减少往返次数',
|
|
39
|
+
flags: 'gi'
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
id: 'PERF005',
|
|
43
|
+
name: '循环内JSON序列化',
|
|
44
|
+
pattern: 'for\s*\([^)]*\)\s*\{[^}]*JSON\.stringify[^}]*\}',
|
|
45
|
+
risk: 'medium',
|
|
46
|
+
message: '循环内频繁序列化可能导致CPU开销过大',
|
|
47
|
+
suggestion: '将序列化移到循环外或进行缓存/批量处理',
|
|
48
|
+
flags: 'gi'
|
|
49
|
+
},
|
|
50
|
+
{
|
|
51
|
+
id: 'PERF006',
|
|
52
|
+
name: '循环内正则编译',
|
|
53
|
+
pattern: 'for\\s*\\([^)]*\\)\\s*\\{[^}]*new\\s+RegExp\\s*\\([^}]*\\}',
|
|
54
|
+
risk: 'medium',
|
|
55
|
+
message: '循环内重复编译正则会增加不必要的开销',
|
|
56
|
+
suggestion: '将正则常量化或预编译,避免在循环中创建',
|
|
57
|
+
flags: 'gi'
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'PERF007',
|
|
61
|
+
name: '忙等待循环',
|
|
62
|
+
pattern: '(while\\s*\\(\\s*true\\s*\\)|for\\s*\\(\\s*;\\s*;\\s*\\))\\s*\\{[^}]*(?!.*(?:sleep|wait|await|setTimeout|setInterval|yield|break|return))[^}]*\\}',
|
|
63
|
+
risk: 'high',
|
|
64
|
+
message: '检测到可能的忙等待循环,可能导致CPU飙升与资源浪费',
|
|
65
|
+
suggestion: '使用事件驱动或阻塞等待机制,避免空循环',
|
|
66
|
+
flags: 'gi'
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
id: 'PERF008',
|
|
70
|
+
name: '循环内DOM布局抖动',
|
|
71
|
+
pattern: 'for\s*\([^)]*\)\s*\{[^}]*(offsetWidth|offsetHeight|getBoundingClientRect)[^}]*\}',
|
|
72
|
+
risk: 'high',
|
|
73
|
+
message: '循环内读取布局信息会触发频繁回流/重绘',
|
|
74
|
+
suggestion: '合并DOM读写、使用批处理、减少同步布局查询',
|
|
75
|
+
flags: 'gi'
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
id: 'PERF009',
|
|
79
|
+
name: '阻塞等待(sleep)',
|
|
80
|
+
pattern: '(Thread\\.sleep\\s*\\(|time\\.sleep\\s*\\()',
|
|
81
|
+
risk: 'medium',
|
|
82
|
+
message: '检测到阻塞等待调用,可能降低服务吞吐和响应',
|
|
83
|
+
suggestion: '改用异步等待或限流/队列机制,避免阻塞主线程',
|
|
84
|
+
flags: 'gi'
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
id: 'PERF010',
|
|
88
|
+
name: '无界线程池',
|
|
89
|
+
pattern: 'Executors\\.newCachedThreadPool\\s*\\(',
|
|
90
|
+
risk: 'high',
|
|
91
|
+
message: '检测到无界线程池,可能导致线程爆炸与资源枯竭',
|
|
92
|
+
suggestion: '使用有界线程池并设置合理最大值与队列长度',
|
|
93
|
+
flags: 'gi'
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
id: 'PERF011',
|
|
97
|
+
name: '循环内字符串拼接',
|
|
98
|
+
pattern: '(for|while)\\s*\\([^)]*\\)\\s*\\{[^}]*\\b\\w+\\s*\\+=\\s*[\'"`]',
|
|
99
|
+
risk: 'medium',
|
|
100
|
+
message: '循环内频繁字符串拼接会造成较大CPU与内存开销',
|
|
101
|
+
suggestion: '使用StringBuilder/列表收集再join,或其他批量化策略',
|
|
102
|
+
flags: 'gi'
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: 'PERF012',
|
|
106
|
+
name: '循环内创建数据库连接',
|
|
107
|
+
pattern: 'for\\s*\\([^)]*\\)\\s*\\{[^}]*\\b(getConnection|openConnection|new\\s+SqlConnection|mysql_connect|pg_connect|MongoClient\\s*\\()\\b',
|
|
108
|
+
risk: 'high',
|
|
109
|
+
message: '循环内反复创建数据库连接会导致严重性能问题',
|
|
110
|
+
suggestion: '使用连接池与复用策略,在循环外预先获取连接',
|
|
111
|
+
flags: 'gi'
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: 'PERF013',
|
|
115
|
+
name: 'HTTP请求缺少超时(Python)',
|
|
116
|
+
pattern: 'requests\\.(get|post|put|delete)\\s*\\(',
|
|
117
|
+
risk: 'medium',
|
|
118
|
+
message: '网络请求未设置超时会造成资源悬挂与吞吐下降',
|
|
119
|
+
suggestion: '设置合理的timeout参数,并对重试与熔断进行控制',
|
|
120
|
+
flags: 'gi',
|
|
121
|
+
requiresAbsent: ['timeout\\s*=']
|
|
122
|
+
}
|
|
123
|
+
];
|