getdoorman 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/LICENSE +21 -0
- package/README.md +181 -0
- package/bin/doorman.js +444 -0
- package/package.json +74 -0
- package/src/ai-fixer.js +559 -0
- package/src/ast-scanner.js +434 -0
- package/src/auth.js +149 -0
- package/src/baseline.js +48 -0
- package/src/compliance.js +539 -0
- package/src/config.js +466 -0
- package/src/custom-rules.js +32 -0
- package/src/dashboard.js +202 -0
- package/src/detector.js +142 -0
- package/src/fix-engine.js +48 -0
- package/src/fix-registry-extra.js +95 -0
- package/src/fix-registry-go-rust.js +77 -0
- package/src/fix-registry-java-csharp.js +77 -0
- package/src/fix-registry-js.js +99 -0
- package/src/fix-registry-mcp-ai.js +57 -0
- package/src/fix-registry-python.js +87 -0
- package/src/fixer-ruby-php.js +608 -0
- package/src/fixer.js +2113 -0
- package/src/hooks.js +115 -0
- package/src/ignore.js +176 -0
- package/src/index.js +384 -0
- package/src/metrics.js +126 -0
- package/src/monorepo.js +65 -0
- package/src/presets.js +54 -0
- package/src/reporter.js +975 -0
- package/src/rule-worker.js +36 -0
- package/src/rules/ast-rules.js +756 -0
- package/src/rules/bugs/accessibility.js +235 -0
- package/src/rules/bugs/ai-codegen-fixable.js +172 -0
- package/src/rules/bugs/ai-codegen.js +365 -0
- package/src/rules/bugs/code-smell-bugs.js +247 -0
- package/src/rules/bugs/crypto-bugs.js +195 -0
- package/src/rules/bugs/docker-bugs.js +158 -0
- package/src/rules/bugs/general.js +361 -0
- package/src/rules/bugs/go-bugs.js +279 -0
- package/src/rules/bugs/index.js +73 -0
- package/src/rules/bugs/js-api.js +257 -0
- package/src/rules/bugs/js-array-object.js +210 -0
- package/src/rules/bugs/js-async-fixable.js +223 -0
- package/src/rules/bugs/js-async.js +211 -0
- package/src/rules/bugs/js-closure-scope.js +182 -0
- package/src/rules/bugs/js-database.js +203 -0
- package/src/rules/bugs/js-error-handling.js +148 -0
- package/src/rules/bugs/js-logic.js +261 -0
- package/src/rules/bugs/js-memory.js +214 -0
- package/src/rules/bugs/js-node.js +361 -0
- package/src/rules/bugs/js-react.js +373 -0
- package/src/rules/bugs/js-regex.js +200 -0
- package/src/rules/bugs/js-state.js +272 -0
- package/src/rules/bugs/js-type-coercion.js +318 -0
- package/src/rules/bugs/nextjs-bugs.js +242 -0
- package/src/rules/bugs/nextjs-fixable.js +120 -0
- package/src/rules/bugs/node-fixable.js +178 -0
- package/src/rules/bugs/python-advanced.js +245 -0
- package/src/rules/bugs/python-fixable.js +98 -0
- package/src/rules/bugs/python.js +284 -0
- package/src/rules/bugs/react-fixable.js +207 -0
- package/src/rules/bugs/ruby-bugs.js +182 -0
- package/src/rules/bugs/shell-bugs.js +181 -0
- package/src/rules/bugs/silent-failures.js +261 -0
- package/src/rules/bugs/ts-bugs.js +235 -0
- package/src/rules/bugs/unused-vars.js +65 -0
- package/src/rules/compliance/accessibility-ext.js +468 -0
- package/src/rules/compliance/education.js +322 -0
- package/src/rules/compliance/financial.js +421 -0
- package/src/rules/compliance/frameworks.js +507 -0
- package/src/rules/compliance/healthcare.js +520 -0
- package/src/rules/compliance/index.js +2714 -0
- package/src/rules/compliance/regional-eu.js +480 -0
- package/src/rules/compliance/regional-international.js +903 -0
- package/src/rules/cost/index.js +1993 -0
- package/src/rules/data/index.js +2503 -0
- package/src/rules/dependencies/index.js +1684 -0
- package/src/rules/deployment/index.js +2050 -0
- package/src/rules/index.js +71 -0
- package/src/rules/infrastructure/index.js +3048 -0
- package/src/rules/performance/index.js +3455 -0
- package/src/rules/quality/index.js +3175 -0
- package/src/rules/reliability/index.js +3040 -0
- package/src/rules/scope-rules.js +815 -0
- package/src/rules/security/ai-api.js +1177 -0
- package/src/rules/security/auth.js +1328 -0
- package/src/rules/security/cors.js +127 -0
- package/src/rules/security/crypto.js +527 -0
- package/src/rules/security/csharp.js +862 -0
- package/src/rules/security/csrf.js +193 -0
- package/src/rules/security/dart.js +835 -0
- package/src/rules/security/deserialization.js +291 -0
- package/src/rules/security/file-upload.js +187 -0
- package/src/rules/security/go.js +850 -0
- package/src/rules/security/headers.js +235 -0
- package/src/rules/security/index.js +65 -0
- package/src/rules/security/injection.js +1639 -0
- package/src/rules/security/mcp-server.js +71 -0
- package/src/rules/security/misconfiguration.js +660 -0
- package/src/rules/security/oauth-jwt.js +329 -0
- package/src/rules/security/path-traversal.js +295 -0
- package/src/rules/security/php.js +1054 -0
- package/src/rules/security/prototype-pollution.js +283 -0
- package/src/rules/security/rate-limiting.js +208 -0
- package/src/rules/security/ruby.js +1061 -0
- package/src/rules/security/rust.js +693 -0
- package/src/rules/security/secrets.js +747 -0
- package/src/rules/security/shell.js +647 -0
- package/src/rules/security/ssrf.js +298 -0
- package/src/rules/security/supply-chain-advanced.js +393 -0
- package/src/rules/security/supply-chain.js +734 -0
- package/src/rules/security/swift.js +835 -0
- package/src/rules/security/taint.js +27 -0
- package/src/rules/security/xss.js +520 -0
- package/src/scan-cache.js +71 -0
- package/src/scanner.js +710 -0
- package/src/scope-analyzer.js +685 -0
- package/src/share.js +88 -0
- package/src/taint.js +300 -0
- package/src/telemetry.js +183 -0
- package/src/tracer.js +190 -0
- package/src/upload.js +35 -0
- package/src/worker.js +31 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
// Auto-fixable versions of async bug rules
|
|
2
|
+
const JS_EXT = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
|
|
3
|
+
function isJS(f) { return JS_EXT.some(e => f.endsWith(e)); }
|
|
4
|
+
|
|
5
|
+
const rules = [
|
|
6
|
+
{
|
|
7
|
+
id: 'BUG-FIX-001',
|
|
8
|
+
category: 'bugs',
|
|
9
|
+
severity: 'high',
|
|
10
|
+
confidence: 'likely',
|
|
11
|
+
title: 'parseInt without radix — auto-fixable',
|
|
12
|
+
check({ files }) {
|
|
13
|
+
const findings = [];
|
|
14
|
+
for (const [fp, content] of files) {
|
|
15
|
+
if (!isJS(fp)) continue;
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
for (let i = 0; i < lines.length; i++) {
|
|
18
|
+
const match = lines[i].match(/parseInt\s*\(\s*(\w+)\s*\)/);
|
|
19
|
+
if (match && !/parseInt\s*\([^,]+,/.test(lines[i])) {
|
|
20
|
+
findings.push({
|
|
21
|
+
ruleId: 'BUG-FIX-001', category: 'bugs', severity: 'high',
|
|
22
|
+
title: 'parseInt() without radix — auto-fixable',
|
|
23
|
+
file: fp, line: i + 1,
|
|
24
|
+
fix: { type: 'replace', old: match[0], new: `parseInt(${match[1]}, 10)` },
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return findings;
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'BUG-FIX-002',
|
|
34
|
+
category: 'bugs',
|
|
35
|
+
severity: 'high',
|
|
36
|
+
confidence: 'likely',
|
|
37
|
+
title: 'isNaN() → Number.isNaN() — auto-fixable',
|
|
38
|
+
check({ files }) {
|
|
39
|
+
const findings = [];
|
|
40
|
+
for (const [fp, content] of files) {
|
|
41
|
+
if (!isJS(fp)) continue;
|
|
42
|
+
const lines = content.split('\n');
|
|
43
|
+
for (let i = 0; i < lines.length; i++) {
|
|
44
|
+
const match = lines[i].match(/(?<![.\w])isNaN\s*\(([^)]+)\)/);
|
|
45
|
+
if (match && !lines[i].includes('Number.isNaN')) {
|
|
46
|
+
findings.push({
|
|
47
|
+
ruleId: 'BUG-FIX-002', category: 'bugs', severity: 'high',
|
|
48
|
+
title: 'isNaN() → Number.isNaN() — auto-fixable',
|
|
49
|
+
file: fp, line: i + 1,
|
|
50
|
+
fix: { type: 'replace', old: match[0], new: `Number.isNaN(${match[1]})` },
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return findings;
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
id: 'BUG-FIX-003',
|
|
60
|
+
category: 'bugs',
|
|
61
|
+
severity: 'high',
|
|
62
|
+
confidence: 'likely',
|
|
63
|
+
title: 'var in for-loop → let — auto-fixable',
|
|
64
|
+
check({ files }) {
|
|
65
|
+
const findings = [];
|
|
66
|
+
for (const [fp, content] of files) {
|
|
67
|
+
if (!isJS(fp)) continue;
|
|
68
|
+
const lines = content.split('\n');
|
|
69
|
+
for (let i = 0; i < lines.length; i++) {
|
|
70
|
+
const match = lines[i].match(/for\s*\(\s*var\s+(\w+)/);
|
|
71
|
+
if (match) {
|
|
72
|
+
findings.push({
|
|
73
|
+
ruleId: 'BUG-FIX-003', category: 'bugs', severity: 'high',
|
|
74
|
+
title: 'var in for-loop → let — auto-fixable',
|
|
75
|
+
file: fp, line: i + 1,
|
|
76
|
+
fix: { type: 'replace', old: `for (var ${match[1]}`, new: `for (let ${match[1]}` },
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return findings;
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 'BUG-FIX-004',
|
|
86
|
+
category: 'bugs',
|
|
87
|
+
severity: 'high',
|
|
88
|
+
confidence: 'likely',
|
|
89
|
+
title: 'throw "string" → throw new Error("string") — auto-fixable',
|
|
90
|
+
check({ files }) {
|
|
91
|
+
const findings = [];
|
|
92
|
+
for (const [fp, content] of files) {
|
|
93
|
+
if (!isJS(fp)) continue;
|
|
94
|
+
const lines = content.split('\n');
|
|
95
|
+
for (let i = 0; i < lines.length; i++) {
|
|
96
|
+
const match = lines[i].match(/throw\s+(['"`])([^'"`]*)\1/);
|
|
97
|
+
if (match) {
|
|
98
|
+
findings.push({
|
|
99
|
+
ruleId: 'BUG-FIX-004', category: 'bugs', severity: 'high',
|
|
100
|
+
title: 'throw string → throw new Error() — auto-fixable',
|
|
101
|
+
file: fp, line: i + 1,
|
|
102
|
+
fix: { type: 'replace', old: match[0], new: `throw new Error(${match[1]}${match[2]}${match[1]})` },
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return findings;
|
|
108
|
+
},
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
id: 'BUG-FIX-005',
|
|
112
|
+
category: 'bugs',
|
|
113
|
+
severity: 'high',
|
|
114
|
+
confidence: 'likely',
|
|
115
|
+
title: 'Off-by-one: <= length → < length — auto-fixable',
|
|
116
|
+
check({ files }) {
|
|
117
|
+
const findings = [];
|
|
118
|
+
for (const [fp, content] of files) {
|
|
119
|
+
if (!isJS(fp)) continue;
|
|
120
|
+
const lines = content.split('\n');
|
|
121
|
+
for (let i = 0; i < lines.length; i++) {
|
|
122
|
+
const match = lines[i].match(/(\w+)\s*<=\s*(\w+)\.length\b/);
|
|
123
|
+
if (match && /for\s*\(/.test(lines[i])) {
|
|
124
|
+
findings.push({
|
|
125
|
+
ruleId: 'BUG-FIX-005', category: 'bugs', severity: 'high',
|
|
126
|
+
title: 'Off-by-one: <= length → < length — auto-fixable',
|
|
127
|
+
file: fp, line: i + 1,
|
|
128
|
+
fix: { type: 'replace', old: `${match[1]} <= ${match[2]}.length`, new: `${match[1]} < ${match[2]}.length` },
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return findings;
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
id: 'BUG-FIX-006',
|
|
138
|
+
category: 'bugs',
|
|
139
|
+
severity: 'high',
|
|
140
|
+
confidence: 'likely',
|
|
141
|
+
title: 'Empty catch block → log error — auto-fixable',
|
|
142
|
+
check({ files }) {
|
|
143
|
+
const findings = [];
|
|
144
|
+
for (const [fp, content] of files) {
|
|
145
|
+
if (!isJS(fp)) continue;
|
|
146
|
+
// Match catch(e) {} or catch (e) { }
|
|
147
|
+
const regex = /catch\s*\(\s*(\w+)\s*\)\s*\{\s*\}/g;
|
|
148
|
+
let match;
|
|
149
|
+
while ((match = regex.exec(content)) !== null) {
|
|
150
|
+
const lineNum = content.substring(0, match.index).split('\n').length;
|
|
151
|
+
findings.push({
|
|
152
|
+
ruleId: 'BUG-FIX-006', category: 'bugs', severity: 'high',
|
|
153
|
+
title: 'Empty catch block → log error — auto-fixable',
|
|
154
|
+
file: fp, line: lineNum,
|
|
155
|
+
fix: { type: 'replace', old: match[0], new: `catch (${match[1]}) { console.error(${match[1]}); }` },
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return findings;
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
id: 'BUG-FIX-007',
|
|
164
|
+
category: 'bugs',
|
|
165
|
+
severity: 'high',
|
|
166
|
+
confidence: 'likely',
|
|
167
|
+
title: 'new Promise(async ...) → just use async function — auto-fixable',
|
|
168
|
+
check({ files }) {
|
|
169
|
+
const findings = [];
|
|
170
|
+
for (const [fp, content] of files) {
|
|
171
|
+
if (!isJS(fp)) continue;
|
|
172
|
+
const lines = content.split('\n');
|
|
173
|
+
for (let i = 0; i < lines.length; i++) {
|
|
174
|
+
if (/new\s+Promise\s*\(\s*async\b/.test(lines[i])) {
|
|
175
|
+
findings.push({
|
|
176
|
+
ruleId: 'BUG-FIX-007', category: 'bugs', severity: 'high',
|
|
177
|
+
title: 'new Promise(async ...) anti-pattern',
|
|
178
|
+
description: 'Async errors inside Promise executor are silently swallowed. Remove the Promise wrapper and use async/await directly.',
|
|
179
|
+
file: fp, line: i + 1,
|
|
180
|
+
fix: { suggestion: 'Remove the new Promise() wrapper. The async function already returns a Promise.' },
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return findings;
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
{
|
|
189
|
+
id: 'BUG-FIX-008',
|
|
190
|
+
category: 'bugs',
|
|
191
|
+
severity: 'high',
|
|
192
|
+
confidence: 'likely',
|
|
193
|
+
title: 'console.log(err) in catch → console.error — auto-fixable',
|
|
194
|
+
check({ files }) {
|
|
195
|
+
const findings = [];
|
|
196
|
+
for (const [fp, content] of files) {
|
|
197
|
+
if (!isJS(fp)) continue;
|
|
198
|
+
const lines = content.split('\n');
|
|
199
|
+
let inCatch = false;
|
|
200
|
+
let catchVar = null;
|
|
201
|
+
for (let i = 0; i < lines.length; i++) {
|
|
202
|
+
const catchMatch = lines[i].match(/\bcatch\s*\(\s*(\w+)\s*\)/);
|
|
203
|
+
if (catchMatch) { inCatch = true; catchVar = catchMatch[1]; }
|
|
204
|
+
if (inCatch && catchVar) {
|
|
205
|
+
const logMatch = lines[i].match(new RegExp(`(console\\.log\\(\\s*${catchVar}\\s*\\))`));
|
|
206
|
+
if (logMatch) {
|
|
207
|
+
findings.push({
|
|
208
|
+
ruleId: 'BUG-FIX-008', category: 'bugs', severity: 'high',
|
|
209
|
+
title: 'console.log(err) → console.error(err) — auto-fixable',
|
|
210
|
+
file: fp, line: i + 1,
|
|
211
|
+
fix: { type: 'replace', old: logMatch[1], new: logMatch[1].replace('console.log', 'console.error') },
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (/^\s*\}/.test(lines[i]) && inCatch) inCatch = false;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return findings;
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
];
|
|
222
|
+
|
|
223
|
+
export default rules;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
// Bug detection: JavaScript async/await and Promise bugs
|
|
2
|
+
const JS_EXT = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
|
|
3
|
+
function isJS(f) { return JS_EXT.some(e => f.endsWith(e)); }
|
|
4
|
+
|
|
5
|
+
const rules = [
|
|
6
|
+
{
|
|
7
|
+
id: 'BUG-ASYNC-001',
|
|
8
|
+
category: 'bugs',
|
|
9
|
+
severity: 'high',
|
|
10
|
+
confidence: 'likely',
|
|
11
|
+
title: 'async function inside forEach — callbacks not awaited',
|
|
12
|
+
check({ files }) {
|
|
13
|
+
const findings = [];
|
|
14
|
+
for (const [fp, content] of files) {
|
|
15
|
+
if (!isJS(fp)) continue;
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
for (let i = 0; i < lines.length; i++) {
|
|
18
|
+
if (/\.forEach\(\s*async\b/.test(lines[i])) {
|
|
19
|
+
findings.push({
|
|
20
|
+
ruleId: 'BUG-ASYNC-001', category: 'bugs', severity: 'high',
|
|
21
|
+
title: 'async function inside forEach — callbacks not awaited',
|
|
22
|
+
description: 'forEach does not await async callbacks. Use for...of or Promise.all(arr.map(async ...)) instead.',
|
|
23
|
+
file: fp, line: i + 1, fix: null,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return findings;
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'BUG-ASYNC-002',
|
|
33
|
+
category: 'bugs',
|
|
34
|
+
severity: 'high',
|
|
35
|
+
confidence: 'likely',
|
|
36
|
+
title: 'Promise created but not awaited or returned',
|
|
37
|
+
check({ files }) {
|
|
38
|
+
const findings = [];
|
|
39
|
+
for (const [fp, content] of files) {
|
|
40
|
+
if (!isJS(fp)) continue;
|
|
41
|
+
const lines = content.split('\n');
|
|
42
|
+
for (let i = 0; i < lines.length; i++) {
|
|
43
|
+
const line = lines[i];
|
|
44
|
+
// Detect: functionCall() on its own line where function is likely async
|
|
45
|
+
if (/^\s+\w+\.\w+\([^)]*\)\s*;?\s*$/.test(line) && !line.includes('await') && !line.includes('return')) {
|
|
46
|
+
// Check if function name suggests async (save, fetch, send, create, update, delete, write, read)
|
|
47
|
+
if (/\.(save|fetch|send|create|update|delete|remove|write|read|insert|find|query|execute|upload|download)\s*\(/.test(line)) {
|
|
48
|
+
// Check if it's inside an async function
|
|
49
|
+
const contextBefore = lines.slice(Math.max(0, i - 15), i).join('\n');
|
|
50
|
+
if (/async\s+(?:function|\w+\s*=\s*async|\([^)]*\)\s*=>)/.test(contextBefore)) {
|
|
51
|
+
findings.push({
|
|
52
|
+
ruleId: 'BUG-ASYNC-002', category: 'bugs', severity: 'high',
|
|
53
|
+
title: 'Async operation may not be awaited',
|
|
54
|
+
description: 'This looks like an async operation (save/fetch/send/etc.) without await. The operation may not complete before the function continues.',
|
|
55
|
+
file: fp, line: i + 1, fix: null,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return findings;
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'BUG-ASYNC-003',
|
|
67
|
+
category: 'bugs',
|
|
68
|
+
severity: 'high',
|
|
69
|
+
confidence: 'likely',
|
|
70
|
+
title: '.then() chain without .catch() — unhandled rejection',
|
|
71
|
+
check({ files }) {
|
|
72
|
+
const findings = [];
|
|
73
|
+
for (const [fp, content] of files) {
|
|
74
|
+
if (!isJS(fp)) continue;
|
|
75
|
+
const lines = content.split('\n');
|
|
76
|
+
for (let i = 0; i < lines.length; i++) {
|
|
77
|
+
if (/\.then\s*\(/.test(lines[i]) && !lines[i].includes('.catch')) {
|
|
78
|
+
// Check next few lines for .catch
|
|
79
|
+
const next5 = lines.slice(i, i + 5).join('\n');
|
|
80
|
+
if (!next5.includes('.catch') && !next5.includes('try') && !lines[i].includes('return')) {
|
|
81
|
+
findings.push({
|
|
82
|
+
ruleId: 'BUG-ASYNC-003', category: 'bugs', severity: 'high',
|
|
83
|
+
title: 'Promise .then() without .catch() — unhandled rejection',
|
|
84
|
+
description: 'A rejected promise without .catch() causes an unhandled rejection, which crashes Node.js by default.',
|
|
85
|
+
file: fp, line: i + 1, fix: null,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return findings;
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
id: 'BUG-ASYNC-004',
|
|
96
|
+
category: 'bugs',
|
|
97
|
+
severity: 'high',
|
|
98
|
+
confidence: 'likely',
|
|
99
|
+
title: 'await inside a loop — sequential execution, should be parallel',
|
|
100
|
+
check({ files }) {
|
|
101
|
+
const findings = [];
|
|
102
|
+
for (const [fp, content] of files) {
|
|
103
|
+
if (!isJS(fp)) continue;
|
|
104
|
+
const lines = content.split('\n');
|
|
105
|
+
let inLoop = 0;
|
|
106
|
+
for (let i = 0; i < lines.length; i++) {
|
|
107
|
+
const line = lines[i];
|
|
108
|
+
if (/\b(for|while)\s*\(/.test(line)) inLoop++;
|
|
109
|
+
if (inLoop > 0 && /\bawait\b/.test(line)) {
|
|
110
|
+
// Check if it's a data-dependent await (skip those)
|
|
111
|
+
if (!/await.*(?:previous|result|last|acc)/i.test(line)) {
|
|
112
|
+
findings.push({
|
|
113
|
+
ruleId: 'BUG-ASYNC-004', category: 'bugs', severity: 'high',
|
|
114
|
+
title: 'await inside loop — runs sequentially instead of in parallel',
|
|
115
|
+
description: 'Each iteration waits for the previous one to complete. Use Promise.all() with .map() for parallel execution.',
|
|
116
|
+
file: fp, line: i + 1, fix: null,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (/\}\s*$/.test(line) && inLoop > 0) inLoop--;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return findings;
|
|
124
|
+
},
|
|
125
|
+
},
|
|
126
|
+
{
|
|
127
|
+
id: 'BUG-ASYNC-005',
|
|
128
|
+
category: 'bugs',
|
|
129
|
+
severity: 'high',
|
|
130
|
+
confidence: 'likely',
|
|
131
|
+
title: 'Promise.all without error handling — one rejection loses all results',
|
|
132
|
+
check({ files }) {
|
|
133
|
+
const findings = [];
|
|
134
|
+
for (const [fp, content] of files) {
|
|
135
|
+
if (!isJS(fp)) continue;
|
|
136
|
+
const lines = content.split('\n');
|
|
137
|
+
for (let i = 0; i < lines.length; i++) {
|
|
138
|
+
if (/Promise\.all\s*\(/.test(lines[i])) {
|
|
139
|
+
const context = lines.slice(Math.max(0, i - 2), Math.min(lines.length, i + 5)).join('\n');
|
|
140
|
+
if (!context.includes('catch') && !context.includes('try') && !context.includes('allSettled')) {
|
|
141
|
+
findings.push({
|
|
142
|
+
ruleId: 'BUG-ASYNC-005', category: 'bugs', severity: 'high',
|
|
143
|
+
title: 'Promise.all without error handling — one rejection loses all results',
|
|
144
|
+
description: 'If any promise rejects, Promise.all rejects immediately and other results are lost. Use Promise.allSettled() or wrap with try/catch.',
|
|
145
|
+
file: fp, line: i + 1, fix: null,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return findings;
|
|
152
|
+
},
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
id: 'BUG-ASYNC-006',
|
|
156
|
+
category: 'bugs',
|
|
157
|
+
severity: 'medium',
|
|
158
|
+
confidence: 'likely',
|
|
159
|
+
title: 'new Promise with async executor — errors may be swallowed',
|
|
160
|
+
check({ files }) {
|
|
161
|
+
const findings = [];
|
|
162
|
+
for (const [fp, content] of files) {
|
|
163
|
+
if (!isJS(fp)) continue;
|
|
164
|
+
const lines = content.split('\n');
|
|
165
|
+
for (let i = 0; i < lines.length; i++) {
|
|
166
|
+
if (/new\s+Promise\s*\(\s*async\b/.test(lines[i])) {
|
|
167
|
+
findings.push({
|
|
168
|
+
ruleId: 'BUG-ASYNC-006', category: 'bugs', severity: 'medium',
|
|
169
|
+
title: 'new Promise with async executor — thrown errors are silently swallowed',
|
|
170
|
+
description: 'An async function as Promise executor can throw errors that are not caught by the Promise. Just use an async function directly.',
|
|
171
|
+
file: fp, line: i + 1, fix: null,
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
return findings;
|
|
177
|
+
},
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
id: 'BUG-ASYNC-007',
|
|
181
|
+
category: 'bugs',
|
|
182
|
+
severity: 'high',
|
|
183
|
+
confidence: 'likely',
|
|
184
|
+
title: 'Floating promise in event handler — errors silently ignored',
|
|
185
|
+
check({ files }) {
|
|
186
|
+
const findings = [];
|
|
187
|
+
for (const [fp, content] of files) {
|
|
188
|
+
if (!isJS(fp)) continue;
|
|
189
|
+
const lines = content.split('\n');
|
|
190
|
+
for (let i = 0; i < lines.length; i++) {
|
|
191
|
+
const line = lines[i];
|
|
192
|
+
if (/\.on\(\s*['"]/.test(line) || /addEventListener\s*\(/.test(line)) {
|
|
193
|
+
// Check if handler is async but event doesn't await
|
|
194
|
+
const next10 = lines.slice(i, i + 10).join('\n');
|
|
195
|
+
if (/async\s*(?:\(|function)/.test(next10) && !next10.includes('.catch') && !next10.includes('try')) {
|
|
196
|
+
findings.push({
|
|
197
|
+
ruleId: 'BUG-ASYNC-007', category: 'bugs', severity: 'high',
|
|
198
|
+
title: 'Async event handler without error handling',
|
|
199
|
+
description: 'Event emitters do not catch async errors. Wrap the handler body in try/catch or the error will be silently lost.',
|
|
200
|
+
file: fp, line: i + 1, fix: null,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
return findings;
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
];
|
|
210
|
+
|
|
211
|
+
export default rules;
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
// Bug detection: JavaScript closure and scope bugs
|
|
2
|
+
const JS_EXT = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
|
|
3
|
+
function isJS(f) { return JS_EXT.some(e => f.endsWith(e)); }
|
|
4
|
+
|
|
5
|
+
const rules = [
|
|
6
|
+
{
|
|
7
|
+
id: 'BUG-SCOPE-001',
|
|
8
|
+
category: 'bugs',
|
|
9
|
+
severity: 'high',
|
|
10
|
+
confidence: 'likely',
|
|
11
|
+
title: 'var in loop with closure — all closures share same variable',
|
|
12
|
+
check({ files }) {
|
|
13
|
+
const findings = [];
|
|
14
|
+
for (const [fp, content] of files) {
|
|
15
|
+
if (!isJS(fp)) continue;
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
for (let i = 0; i < lines.length; i++) {
|
|
18
|
+
if (/for\s*\(\s*var\s+/.test(lines[i])) {
|
|
19
|
+
// Check if there's a closure in the loop body
|
|
20
|
+
const body = lines.slice(i, Math.min(lines.length, i + 15)).join('\n');
|
|
21
|
+
if (/(?:setTimeout|setInterval|addEventListener|\.on\(|function\s*\(|=>\s*\{)/.test(body)) {
|
|
22
|
+
findings.push({
|
|
23
|
+
ruleId: 'BUG-SCOPE-001', category: 'bugs', severity: 'high',
|
|
24
|
+
title: 'var in for-loop with closure — all callbacks share same variable',
|
|
25
|
+
description: 'var is function-scoped, so all closures in this loop capture the same variable. By the time callbacks run, the loop variable has its final value. Use let instead.',
|
|
26
|
+
file: fp, line: i + 1, fix: null,
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return findings;
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
id: 'BUG-SCOPE-002',
|
|
37
|
+
category: 'bugs',
|
|
38
|
+
severity: 'high',
|
|
39
|
+
confidence: 'likely',
|
|
40
|
+
title: 'this context lost in callback — use arrow function or .bind()',
|
|
41
|
+
check({ files }) {
|
|
42
|
+
const findings = [];
|
|
43
|
+
for (const [fp, content] of files) {
|
|
44
|
+
if (!isJS(fp)) continue;
|
|
45
|
+
const lines = content.split('\n');
|
|
46
|
+
for (let i = 0; i < lines.length; i++) {
|
|
47
|
+
const line = lines[i];
|
|
48
|
+
// Detect: setTimeout(function() { this.x }) or .on('event', function() { this.x })
|
|
49
|
+
if (/(?:setTimeout|setInterval|\.on|addEventListener)\s*\(\s*function\b/.test(line)) {
|
|
50
|
+
const body = lines.slice(i, Math.min(lines.length, i + 10)).join('\n');
|
|
51
|
+
if (/\bthis\./.test(body) && !body.includes('.bind(this)')) {
|
|
52
|
+
findings.push({
|
|
53
|
+
ruleId: 'BUG-SCOPE-002', category: 'bugs', severity: 'high',
|
|
54
|
+
title: 'this context lost in callback — function() rebinds this',
|
|
55
|
+
description: 'Regular function() expressions get their own "this". Use an arrow function () => {} to preserve the outer this context.',
|
|
56
|
+
file: fp, line: i + 1, fix: null,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return findings;
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'BUG-SCOPE-003',
|
|
67
|
+
category: 'bugs',
|
|
68
|
+
severity: 'high',
|
|
69
|
+
confidence: 'likely',
|
|
70
|
+
title: 'Variable declared but never used',
|
|
71
|
+
check({ files }) {
|
|
72
|
+
const findings = [];
|
|
73
|
+
for (const [fp, content] of files) {
|
|
74
|
+
if (!isJS(fp)) continue;
|
|
75
|
+
const lines = content.split('\n');
|
|
76
|
+
for (let i = 0; i < lines.length; i++) {
|
|
77
|
+
const match = lines[i].match(/^\s*(?:const|let|var)\s+(\w+)\s*=\s*.+;?\s*$/);
|
|
78
|
+
if (match) {
|
|
79
|
+
const varName = match[1];
|
|
80
|
+
if (varName === '_' || varName.startsWith('_')) continue;
|
|
81
|
+
if (varName.length < 2) continue;
|
|
82
|
+
// Count occurrences in the rest of the file
|
|
83
|
+
const rest = lines.slice(i + 1).join('\n');
|
|
84
|
+
const regex = new RegExp('\\b' + varName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\b');
|
|
85
|
+
if (!regex.test(rest)) {
|
|
86
|
+
findings.push({
|
|
87
|
+
ruleId: 'BUG-SCOPE-003', category: 'bugs', severity: 'high',
|
|
88
|
+
title: `Variable "${varName}" is declared but never used`,
|
|
89
|
+
description: 'Unused variables indicate dead code or a missing reference. Remove it or use it.',
|
|
90
|
+
file: fp, line: i + 1, fix: null,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return findings;
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
id: 'BUG-SCOPE-004',
|
|
101
|
+
category: 'bugs',
|
|
102
|
+
severity: 'high',
|
|
103
|
+
confidence: 'likely',
|
|
104
|
+
title: 'Implicit global — assignment to undeclared variable',
|
|
105
|
+
check({ files }) {
|
|
106
|
+
const findings = [];
|
|
107
|
+
for (const [fp, content] of files) {
|
|
108
|
+
if (!isJS(fp)) continue;
|
|
109
|
+
if (content.includes('"use strict"') || content.includes("'use strict'") || fp.endsWith('.mjs') || fp.endsWith('.ts') || fp.endsWith('.tsx')) continue;
|
|
110
|
+
const lines = content.split('\n');
|
|
111
|
+
for (let i = 0; i < lines.length; i++) {
|
|
112
|
+
const line = lines[i].trim();
|
|
113
|
+
// Simple heuristic: assignment at start of line without var/let/const/this/module/exports
|
|
114
|
+
const m = line.match(/^(\w+)\s*=\s*(?!==)/);
|
|
115
|
+
if (m && !/^(var|let|const|if|else|for|while|return|throw|this|module|exports|self|window|global|process|require|import|export|class|function|async|switch|case|default|break|continue|new|delete|typeof|void|yield|true|false|null|undefined)\b/.test(m[1])) {
|
|
116
|
+
// Check if it's declared elsewhere in file
|
|
117
|
+
const name = m[1];
|
|
118
|
+
const declPattern = new RegExp('(?:var|let|const|function)\\s+' + name + '\\b');
|
|
119
|
+
const paramPattern = new RegExp('[({,]\\s*' + name + '\\s*[,})]');
|
|
120
|
+
if (!declPattern.test(content) && !paramPattern.test(content)) {
|
|
121
|
+
findings.push({
|
|
122
|
+
ruleId: 'BUG-SCOPE-004', category: 'bugs', severity: 'high',
|
|
123
|
+
title: `Implicit global variable "${name}"`,
|
|
124
|
+
description: 'Assigning to an undeclared variable creates a global. This causes hard-to-track bugs and breaks in strict mode. Add let/const.',
|
|
125
|
+
file: fp, line: i + 1, fix: null,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return findings;
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
id: 'BUG-SCOPE-005',
|
|
136
|
+
category: 'bugs',
|
|
137
|
+
severity: 'medium',
|
|
138
|
+
confidence: 'likely',
|
|
139
|
+
title: 'Variable shadows outer scope — same name in nested scope',
|
|
140
|
+
check({ files }) {
|
|
141
|
+
const findings = [];
|
|
142
|
+
for (const [fp, content] of files) {
|
|
143
|
+
if (!isJS(fp)) continue;
|
|
144
|
+
const lines = content.split('\n');
|
|
145
|
+
// Track scope depth and declarations per depth
|
|
146
|
+
const scopeStack = [new Set()]; // stack of sets, each set = vars declared at that depth
|
|
147
|
+
const topDecl = new Map(); // name -> line of top-level declaration
|
|
148
|
+
let depth = 0;
|
|
149
|
+
for (let i = 0; i < lines.length; i++) {
|
|
150
|
+
const line = lines[i];
|
|
151
|
+
// Track brace depth for scope
|
|
152
|
+
const opens = (line.match(/\{/g) || []).length;
|
|
153
|
+
const closes = (line.match(/\}/g) || []).length;
|
|
154
|
+
|
|
155
|
+
const match = line.match(/^\s*(?:const|let|var)\s+(\w+)\b/);
|
|
156
|
+
if (match) {
|
|
157
|
+
const name = match[1];
|
|
158
|
+
// Skip very common short names and common loop/callback vars
|
|
159
|
+
if (name.length < 4 || /^(err|key|val|res|req|ctx|idx|str|obj|arr|tmp|buf|msg|ret|acc|cur|pre|ref|row|col|src|dst|len|ptr|sum|max|min|map|set|log|doc|dom|div|img|btn|url|api|arg|cmd|cfg|env|app|pkg|lib|mod|opt|out|ext|fmt|num|pos|end|top)$/.test(name)) continue;
|
|
160
|
+
// Check if same name exists in an outer scope
|
|
161
|
+
if (depth === 0) {
|
|
162
|
+
topDecl.set(name, i + 1);
|
|
163
|
+
} else if (topDecl.has(name)) {
|
|
164
|
+
findings.push({
|
|
165
|
+
ruleId: 'BUG-SCOPE-005', category: 'bugs', severity: 'medium',
|
|
166
|
+
title: `Variable "${name}" shadows declaration on line ${topDecl.get(name)}`,
|
|
167
|
+
description: 'Re-declaring a variable with the same name in a nested scope hides the outer one, making bugs hard to spot. Rename one of them.',
|
|
168
|
+
file: fp, line: i + 1, fix: null,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
depth += opens - closes;
|
|
174
|
+
if (depth < 0) depth = 0;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
return findings;
|
|
178
|
+
},
|
|
179
|
+
},
|
|
180
|
+
];
|
|
181
|
+
|
|
182
|
+
export default rules;
|