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,261 @@
|
|
|
1
|
+
// Bug detection: Silent failure patterns — empty catch blocks, swallowed errors, silent data loss
|
|
2
|
+
const JS_EXT = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
|
|
3
|
+
const PY_EXT = ['.py'];
|
|
4
|
+
const ALL_EXT = [...JS_EXT, ...PY_EXT, '.go', '.java', '.cs', '.rb', '.php', '.rs', '.swift', '.dart'];
|
|
5
|
+
function isJS(f) { return JS_EXT.some(e => f.endsWith(e)); }
|
|
6
|
+
function isPy(f) { return PY_EXT.some(e => f.endsWith(e)); }
|
|
7
|
+
function isSource(f) { return ALL_EXT.some(e => f.endsWith(e)); }
|
|
8
|
+
|
|
9
|
+
const rules = [
|
|
10
|
+
// BUG-SILENT-001: Empty catch block (JS/TS)
|
|
11
|
+
{
|
|
12
|
+
id: 'BUG-SILENT-001',
|
|
13
|
+
category: 'bugs',
|
|
14
|
+
severity: 'medium',
|
|
15
|
+
confidence: 'likely',
|
|
16
|
+
title: 'Empty catch block silently swallows errors',
|
|
17
|
+
check({ files }) {
|
|
18
|
+
const findings = [];
|
|
19
|
+
for (const [fp, content] of files) {
|
|
20
|
+
if (!isJS(fp)) continue;
|
|
21
|
+
const lines = content.split('\n');
|
|
22
|
+
for (let i = 0; i < lines.length; i++) {
|
|
23
|
+
const line = lines[i];
|
|
24
|
+
// Match catch with empty body or body containing only a comment
|
|
25
|
+
if (/\bcatch\s*(?:\([^)]*\))?\s*\{\s*\}/.test(line)) {
|
|
26
|
+
findings.push({
|
|
27
|
+
ruleId: 'BUG-SILENT-001', category: 'bugs', severity: 'medium',
|
|
28
|
+
title: 'Empty catch block — errors are silently swallowed',
|
|
29
|
+
description: 'This catch block discards all errors. At minimum, log the error: catch (e) { console.error(e); }. Silent failures make debugging extremely difficult.',
|
|
30
|
+
file: fp, line: i + 1,
|
|
31
|
+
fix: null,
|
|
32
|
+
});
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
// Multi-line: catch { \n // comment \n }
|
|
36
|
+
if (/\bcatch\s*(?:\([^)]*\))?\s*\{\s*$/.test(line)) {
|
|
37
|
+
const bodyLines = [];
|
|
38
|
+
for (let j = i + 1; j < Math.min(lines.length, i + 5); j++) {
|
|
39
|
+
const bodyLine = lines[j].trim();
|
|
40
|
+
if (bodyLine === '}') {
|
|
41
|
+
// Check if body was empty or only comments
|
|
42
|
+
const onlyComments = bodyLines.every(l => !l || l.startsWith('//') || l.startsWith('*') || l.startsWith('/*'));
|
|
43
|
+
if (onlyComments) {
|
|
44
|
+
findings.push({
|
|
45
|
+
ruleId: 'BUG-SILENT-001', category: 'bugs', severity: 'medium',
|
|
46
|
+
title: 'Empty catch block — errors are silently swallowed',
|
|
47
|
+
description: 'This catch block discards all errors (only has comments). At minimum, log the error: catch (e) { console.error(e); }.',
|
|
48
|
+
file: fp, line: i + 1,
|
|
49
|
+
fix: null,
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
bodyLines.push(bodyLine);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return findings;
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// BUG-SILENT-002: Catch returns null/undefined without logging (JS/TS)
|
|
64
|
+
{
|
|
65
|
+
id: 'BUG-SILENT-002',
|
|
66
|
+
category: 'bugs',
|
|
67
|
+
severity: 'medium',
|
|
68
|
+
confidence: 'likely',
|
|
69
|
+
title: 'Catch block returns fallback without logging the error',
|
|
70
|
+
check({ files }) {
|
|
71
|
+
const findings = [];
|
|
72
|
+
for (const [fp, content] of files) {
|
|
73
|
+
if (!isJS(fp)) continue;
|
|
74
|
+
const lines = content.split('\n');
|
|
75
|
+
for (let i = 0; i < lines.length; i++) {
|
|
76
|
+
const line = lines[i];
|
|
77
|
+
// Match: catch (optional param) { return null/undefined/false/[]/{}; }
|
|
78
|
+
const catchMatch = line.match(/\bcatch\s*(?:\(([^)]*)\))?\s*\{\s*$/);
|
|
79
|
+
if (!catchMatch) continue;
|
|
80
|
+
|
|
81
|
+
const errVar = catchMatch[1]?.trim();
|
|
82
|
+
const bodyLines = [];
|
|
83
|
+
let closingLine = -1;
|
|
84
|
+
for (let j = i + 1; j < Math.min(lines.length, i + 6); j++) {
|
|
85
|
+
if (lines[j].trim() === '}') { closingLine = j; break; }
|
|
86
|
+
bodyLines.push(lines[j].trim());
|
|
87
|
+
}
|
|
88
|
+
if (closingLine === -1) continue;
|
|
89
|
+
|
|
90
|
+
const body = bodyLines.join(' ');
|
|
91
|
+
// Return a fallback value without referencing the error
|
|
92
|
+
if (/return\s+(null|undefined|false|\[\]|\{\}|''|""|0|-1)\s*;?/.test(body)) {
|
|
93
|
+
// If the error variable is referenced (logged, rethrown, etc.), it's fine
|
|
94
|
+
if (errVar && body.includes(errVar)) continue;
|
|
95
|
+
// If there's any console/log call, it's fine
|
|
96
|
+
if (/console\.|log\(|logger\.|warn\(/.test(body)) continue;
|
|
97
|
+
|
|
98
|
+
findings.push({
|
|
99
|
+
ruleId: 'BUG-SILENT-002', category: 'bugs', severity: 'medium',
|
|
100
|
+
title: 'Catch block returns fallback without logging the error',
|
|
101
|
+
description: 'This catch block returns a default value but silently discards the error. Add logging: catch (e) { console.error(e); return null; }. Silent data loss makes bugs invisible.',
|
|
102
|
+
file: fp, line: i + 1,
|
|
103
|
+
fix: null,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return findings;
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
|
|
112
|
+
// BUG-SILENT-003: Python bare except with pass
|
|
113
|
+
{
|
|
114
|
+
id: 'BUG-SILENT-003',
|
|
115
|
+
category: 'bugs',
|
|
116
|
+
severity: 'high',
|
|
117
|
+
confidence: 'likely',
|
|
118
|
+
title: 'except/pass silently swallows all errors',
|
|
119
|
+
check({ files }) {
|
|
120
|
+
const findings = [];
|
|
121
|
+
for (const [fp, content] of files) {
|
|
122
|
+
if (!isPy(fp)) continue;
|
|
123
|
+
const lines = content.split('\n');
|
|
124
|
+
for (let i = 0; i < lines.length; i++) {
|
|
125
|
+
// except (optional type): followed by pass on next line
|
|
126
|
+
if (/^\s*except\s*(?:\w+(?:\s+as\s+\w+)?)?\s*:\s*$/.test(lines[i])) {
|
|
127
|
+
const nextLine = (lines[i + 1] || '').trim();
|
|
128
|
+
if (nextLine === 'pass' || nextLine === '...') {
|
|
129
|
+
findings.push({
|
|
130
|
+
ruleId: 'BUG-SILENT-003', category: 'bugs', severity: 'high',
|
|
131
|
+
title: 'except/pass silently swallows all errors',
|
|
132
|
+
description: 'This except block catches errors and does nothing. At minimum log: except Exception as e: logging.error(e). Silent failures hide bugs.',
|
|
133
|
+
file: fp, line: i + 1,
|
|
134
|
+
fix: null,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return findings;
|
|
141
|
+
},
|
|
142
|
+
},
|
|
143
|
+
|
|
144
|
+
// BUG-SILENT-004: Go error ignored (err = ... without check)
|
|
145
|
+
{
|
|
146
|
+
id: 'BUG-SILENT-004',
|
|
147
|
+
category: 'bugs',
|
|
148
|
+
severity: 'high',
|
|
149
|
+
confidence: 'likely',
|
|
150
|
+
title: 'Go error value ignored — not checked after assignment',
|
|
151
|
+
check({ files }) {
|
|
152
|
+
const findings = [];
|
|
153
|
+
for (const [fp, content] of files) {
|
|
154
|
+
if (!fp.endsWith('.go')) continue;
|
|
155
|
+
const lines = content.split('\n');
|
|
156
|
+
for (let i = 0; i < lines.length; i++) {
|
|
157
|
+
const line = lines[i];
|
|
158
|
+
// Pattern: _, err := someFunc() or val, err := someFunc()
|
|
159
|
+
// Then next line doesn't check err
|
|
160
|
+
if (/,\s*err\s*:?=/.test(line) || /\berr\s*:?=\s*\w+/.test(line)) {
|
|
161
|
+
const nextLines = lines.slice(i + 1, Math.min(lines.length, i + 4)).join(' ');
|
|
162
|
+
if (!nextLines.includes('err') && !nextLines.includes('if err')) {
|
|
163
|
+
findings.push({
|
|
164
|
+
ruleId: 'BUG-SILENT-004', category: 'bugs', severity: 'high',
|
|
165
|
+
title: 'Go error returned but never checked',
|
|
166
|
+
description: 'Error from function call is assigned but not checked. Always handle errors: if err != nil { return err }.',
|
|
167
|
+
file: fp, line: i + 1,
|
|
168
|
+
fix: null,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return findings;
|
|
175
|
+
},
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
// BUG-SILENT-005: Promise without .catch() or try/catch
|
|
179
|
+
{
|
|
180
|
+
id: 'BUG-SILENT-005',
|
|
181
|
+
category: 'bugs',
|
|
182
|
+
severity: 'high',
|
|
183
|
+
confidence: 'likely',
|
|
184
|
+
title: 'Promise chain without .catch() — unhandled rejection risk',
|
|
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 (line.trim().startsWith('//')) continue;
|
|
193
|
+
// .then() at end of statement (no .catch after)
|
|
194
|
+
if (/\.then\s*\(/.test(line) && /[);]\s*$/.test(line) && !line.includes('.catch')) {
|
|
195
|
+
// Check next line for .catch
|
|
196
|
+
const nextLine = (lines[i + 1] || '').trim();
|
|
197
|
+
if (!nextLine.startsWith('.catch') && !nextLine.startsWith('.finally')) {
|
|
198
|
+
// Check if inside a try block (look back up to 10 lines)
|
|
199
|
+
let inTry = false;
|
|
200
|
+
for (let j = Math.max(0, i - 10); j < i; j++) {
|
|
201
|
+
if (/\btry\s*\{/.test(lines[j])) inTry = true;
|
|
202
|
+
if (/\bcatch\s*\(/.test(lines[j])) inTry = false;
|
|
203
|
+
}
|
|
204
|
+
if (!inTry) {
|
|
205
|
+
findings.push({
|
|
206
|
+
ruleId: 'BUG-SILENT-005', category: 'bugs', severity: 'high',
|
|
207
|
+
title: 'Promise .then() without .catch() — unhandled rejection',
|
|
208
|
+
description: 'This promise chain has no .catch() handler. Unhandled rejections will crash Node.js. Add .catch(err => ...) or use async/await with try/catch.',
|
|
209
|
+
file: fp, line: i + 1,
|
|
210
|
+
fix: null,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return findings;
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
// BUG-SILENT-006: JSON.parse without try/catch
|
|
222
|
+
{
|
|
223
|
+
id: 'BUG-SILENT-006',
|
|
224
|
+
category: 'bugs',
|
|
225
|
+
severity: 'high',
|
|
226
|
+
confidence: 'likely',
|
|
227
|
+
title: 'JSON.parse without try/catch — will crash on malformed input',
|
|
228
|
+
check({ files }) {
|
|
229
|
+
const findings = [];
|
|
230
|
+
for (const [fp, content] of files) {
|
|
231
|
+
if (!isJS(fp)) continue;
|
|
232
|
+
const lines = content.split('\n');
|
|
233
|
+
for (let i = 0; i < lines.length; i++) {
|
|
234
|
+
const line = lines[i];
|
|
235
|
+
if (line.trim().startsWith('//')) continue;
|
|
236
|
+
if (!/JSON\.parse\s*\(/.test(line)) continue;
|
|
237
|
+
// Skip if it's a static/hardcoded JSON string
|
|
238
|
+
if (/JSON\.parse\s*\(\s*['"`]\s*\{/.test(line)) continue;
|
|
239
|
+
// Check if we're inside a try block
|
|
240
|
+
let inTry = false;
|
|
241
|
+
for (let j = Math.max(0, i - 15); j < i; j++) {
|
|
242
|
+
if (/\btry\s*\{/.test(lines[j])) inTry = true;
|
|
243
|
+
if (/\}\s*catch/.test(lines[j])) inTry = false;
|
|
244
|
+
}
|
|
245
|
+
if (!inTry) {
|
|
246
|
+
findings.push({
|
|
247
|
+
ruleId: 'BUG-SILENT-006', category: 'bugs', severity: 'high',
|
|
248
|
+
title: 'JSON.parse() without try/catch — crashes on malformed input',
|
|
249
|
+
description: 'JSON.parse throws SyntaxError on invalid JSON. Wrap in try/catch or use a safe parser. Unhandled parse errors will crash the process.',
|
|
250
|
+
file: fp, line: i + 1,
|
|
251
|
+
fix: null,
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
return findings;
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
];
|
|
260
|
+
|
|
261
|
+
export default rules;
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
// Bug detection: TypeScript-specific traps and anti-patterns
|
|
2
|
+
const TS_EXT = ['.ts', '.tsx'];
|
|
3
|
+
function isTS(f) { return TS_EXT.some(e => f.endsWith(e)); }
|
|
4
|
+
|
|
5
|
+
const rules = [
|
|
6
|
+
{
|
|
7
|
+
id: 'BUG-TS-001',
|
|
8
|
+
category: 'bugs',
|
|
9
|
+
severity: 'high',
|
|
10
|
+
confidence: 'definite',
|
|
11
|
+
title: 'Type assertion used to bypass type safety (as any)',
|
|
12
|
+
check({ files }) {
|
|
13
|
+
const findings = [];
|
|
14
|
+
for (const [fp, content] of files) {
|
|
15
|
+
if (!isTS(fp)) continue;
|
|
16
|
+
const lines = content.split('\n');
|
|
17
|
+
for (let i = 0; i < lines.length; i++) {
|
|
18
|
+
if (/\bas\s+any\b/.test(lines[i])) {
|
|
19
|
+
findings.push({
|
|
20
|
+
ruleId: 'BUG-TS-001', category: 'bugs', severity: 'high',
|
|
21
|
+
title: '"as any" bypasses type safety — defeats purpose of TypeScript',
|
|
22
|
+
description: 'AI generators use "as any" to silence errors instead of fixing types. This hides real bugs at compile time.',
|
|
23
|
+
file: fp, line: i + 1, fix: null,
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return findings;
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'BUG-TS-002',
|
|
33
|
+
category: 'bugs',
|
|
34
|
+
severity: 'high',
|
|
35
|
+
confidence: 'likely',
|
|
36
|
+
title: 'Non-null assertion operator (!) on potentially null value',
|
|
37
|
+
check({ files }) {
|
|
38
|
+
const findings = [];
|
|
39
|
+
for (const [fp, content] of files) {
|
|
40
|
+
if (!isTS(fp)) continue;
|
|
41
|
+
const lines = content.split('\n');
|
|
42
|
+
for (let i = 0; i < lines.length; i++) {
|
|
43
|
+
// variable!.property or variable!.method() — non-null assertion
|
|
44
|
+
if (/\w+!\.\w+/.test(lines[i]) && !/\/\//.test(lines[i].split('!.')[0])) {
|
|
45
|
+
// Skip common safe patterns
|
|
46
|
+
if (/document\.getElementById|querySelector/.test(lines[i])) continue;
|
|
47
|
+
findings.push({
|
|
48
|
+
ruleId: 'BUG-TS-002', category: 'bugs', severity: 'high',
|
|
49
|
+
title: 'Non-null assertion (!) — will throw if value is actually null',
|
|
50
|
+
description: 'The ! operator tells TypeScript to trust you, but the value can still be null at runtime. Use optional chaining (?.) or null checks instead.',
|
|
51
|
+
file: fp, line: i + 1, fix: null,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return findings;
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
id: 'BUG-TS-003',
|
|
61
|
+
category: 'bugs',
|
|
62
|
+
severity: 'medium',
|
|
63
|
+
confidence: 'definite',
|
|
64
|
+
title: '@ts-ignore suppressing type error',
|
|
65
|
+
check({ files }) {
|
|
66
|
+
const findings = [];
|
|
67
|
+
for (const [fp, content] of files) {
|
|
68
|
+
if (!isTS(fp)) continue;
|
|
69
|
+
const lines = content.split('\n');
|
|
70
|
+
for (let i = 0; i < lines.length; i++) {
|
|
71
|
+
if (/@ts-ignore|@ts-nocheck/.test(lines[i])) {
|
|
72
|
+
findings.push({
|
|
73
|
+
ruleId: 'BUG-TS-003', category: 'bugs', severity: 'medium',
|
|
74
|
+
title: `${lines[i].includes('@ts-nocheck') ? '@ts-nocheck disables ALL type checking' : '@ts-ignore hides a type error'}`,
|
|
75
|
+
description: 'AI generators add @ts-ignore to bypass errors instead of fixing them. Use @ts-expect-error with a comment explaining why, or fix the type.',
|
|
76
|
+
file: fp, line: i + 1, fix: null,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return findings;
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: 'BUG-TS-004',
|
|
86
|
+
category: 'bugs',
|
|
87
|
+
severity: 'medium',
|
|
88
|
+
confidence: 'likely',
|
|
89
|
+
title: 'Enum used where union type is better',
|
|
90
|
+
check({ files }) {
|
|
91
|
+
const findings = [];
|
|
92
|
+
for (const [fp, content] of files) {
|
|
93
|
+
if (!isTS(fp)) continue;
|
|
94
|
+
const lines = content.split('\n');
|
|
95
|
+
for (let i = 0; i < lines.length; i++) {
|
|
96
|
+
// Numeric enum (not string enum)
|
|
97
|
+
if (/^\s*(?:export\s+)?enum\s+\w+\s*\{/.test(lines[i])) {
|
|
98
|
+
let block = '';
|
|
99
|
+
for (let j = i; j < Math.min(i + 15, lines.length); j++) {
|
|
100
|
+
block += lines[j] + '\n';
|
|
101
|
+
if (lines[j].includes('}')) break;
|
|
102
|
+
}
|
|
103
|
+
// If it's a simple enum without computed values, a union type is safer
|
|
104
|
+
const members = block.match(/\w+\s*[,}]/g) || [];
|
|
105
|
+
const hasValues = /=\s*['"`]/.test(block);
|
|
106
|
+
if (!hasValues && members.length <= 8) {
|
|
107
|
+
findings.push({
|
|
108
|
+
ruleId: 'BUG-TS-004', category: 'bugs', severity: 'medium',
|
|
109
|
+
title: 'Numeric enum generates runtime code — prefer union type',
|
|
110
|
+
description: 'Numeric enums produce JavaScript objects. String unions (type Status = "active" | "inactive") are simpler, tree-shakeable, and don\'t have the reverse-mapping footgun.',
|
|
111
|
+
file: fp, line: i + 1, fix: null,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return findings;
|
|
118
|
+
},
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: 'BUG-TS-005',
|
|
122
|
+
category: 'bugs',
|
|
123
|
+
severity: 'high',
|
|
124
|
+
confidence: 'likely',
|
|
125
|
+
title: 'Optional chaining with function call may silently return undefined',
|
|
126
|
+
check({ files }) {
|
|
127
|
+
const findings = [];
|
|
128
|
+
for (const [fp, content] of files) {
|
|
129
|
+
if (!isTS(fp)) continue;
|
|
130
|
+
const lines = content.split('\n');
|
|
131
|
+
for (let i = 0; i < lines.length; i++) {
|
|
132
|
+
// Chaining on optional call result: obj?.method().property
|
|
133
|
+
if (/\w+\?\.\w+\(\)\./.test(lines[i]) && !/\?\.\w+\(\)\?\./.test(lines[i])) {
|
|
134
|
+
findings.push({
|
|
135
|
+
ruleId: 'BUG-TS-005', category: 'bugs', severity: 'high',
|
|
136
|
+
title: 'Optional chain on method call but not on result — crashes if method returns undefined',
|
|
137
|
+
description: 'obj?.method().prop will crash if obj is null (optional chaining stops) but also if method() returns undefined (.prop throws). Use obj?.method()?.prop.',
|
|
138
|
+
file: fp, line: i + 1, fix: null,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return findings;
|
|
144
|
+
},
|
|
145
|
+
},
|
|
146
|
+
{
|
|
147
|
+
id: 'BUG-TS-006',
|
|
148
|
+
category: 'bugs',
|
|
149
|
+
severity: 'medium',
|
|
150
|
+
confidence: 'likely',
|
|
151
|
+
title: 'Type assertion instead of type guard',
|
|
152
|
+
check({ files }) {
|
|
153
|
+
const findings = [];
|
|
154
|
+
for (const [fp, content] of files) {
|
|
155
|
+
if (!isTS(fp)) continue;
|
|
156
|
+
const lines = content.split('\n');
|
|
157
|
+
for (let i = 0; i < lines.length; i++) {
|
|
158
|
+
// (variable as SpecificType).method() — unsafe assertion before method call
|
|
159
|
+
if (/\(\w+\s+as\s+\w+\)\.\w+/.test(lines[i]) && !/as\s+(string|number|boolean|any|unknown)/.test(lines[i])) {
|
|
160
|
+
findings.push({
|
|
161
|
+
ruleId: 'BUG-TS-006', category: 'bugs', severity: 'medium',
|
|
162
|
+
title: 'Type assertion before property access — use type guard instead',
|
|
163
|
+
description: 'Type assertions lie to the compiler. If the type is wrong at runtime, you get a crash. Use a type guard (if ("prop" in obj)) for safety.',
|
|
164
|
+
file: fp, line: i + 1, fix: null,
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return findings;
|
|
170
|
+
},
|
|
171
|
+
},
|
|
172
|
+
{
|
|
173
|
+
id: 'BUG-TS-007',
|
|
174
|
+
category: 'bugs',
|
|
175
|
+
severity: 'high',
|
|
176
|
+
confidence: 'likely',
|
|
177
|
+
title: 'Promise<void> return type hiding errors',
|
|
178
|
+
check({ files }) {
|
|
179
|
+
const findings = [];
|
|
180
|
+
for (const [fp, content] of files) {
|
|
181
|
+
if (!isTS(fp)) continue;
|
|
182
|
+
const lines = content.split('\n');
|
|
183
|
+
for (let i = 0; i < lines.length; i++) {
|
|
184
|
+
// async function that returns void but does important work
|
|
185
|
+
if (/async\s+\w+\s*\([^)]*\)\s*:\s*Promise<void>/.test(lines[i])) {
|
|
186
|
+
let block = '';
|
|
187
|
+
for (let j = i; j < Math.min(i + 20, lines.length); j++) {
|
|
188
|
+
block += lines[j] + '\n';
|
|
189
|
+
}
|
|
190
|
+
// Contains save/create/update/delete operations
|
|
191
|
+
if (/\.(save|create|update|delete|insert|remove|send|write|post|put)\s*\(/.test(block)) {
|
|
192
|
+
if (!/try\s*\{/.test(block)) {
|
|
193
|
+
findings.push({
|
|
194
|
+
ruleId: 'BUG-TS-007', category: 'bugs', severity: 'high',
|
|
195
|
+
title: 'async function with side effects returns void — callers can\'t know if it failed',
|
|
196
|
+
description: 'Returning void from async functions that do important work hides failures. Return a result type or throw on error.',
|
|
197
|
+
file: fp, line: i + 1, fix: null,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return findings;
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
id: 'BUG-TS-008',
|
|
209
|
+
category: 'bugs',
|
|
210
|
+
severity: 'medium',
|
|
211
|
+
confidence: 'likely',
|
|
212
|
+
title: 'Record/object type with string index allows any key',
|
|
213
|
+
check({ files }) {
|
|
214
|
+
const findings = [];
|
|
215
|
+
for (const [fp, content] of files) {
|
|
216
|
+
if (!isTS(fp)) continue;
|
|
217
|
+
const lines = content.split('\n');
|
|
218
|
+
for (let i = 0; i < lines.length; i++) {
|
|
219
|
+
// Record<string, any> or { [key: string]: any }
|
|
220
|
+
if (/Record\s*<\s*string\s*,\s*any\s*>/.test(lines[i]) || /\[\s*\w+\s*:\s*string\s*\]\s*:\s*any/.test(lines[i])) {
|
|
221
|
+
findings.push({
|
|
222
|
+
ruleId: 'BUG-TS-008', category: 'bugs', severity: 'medium',
|
|
223
|
+
title: 'Record<string, any> provides no type safety',
|
|
224
|
+
description: 'This type accepts any key and any value. Use specific key unions and value types, or at least Record<string, unknown>.',
|
|
225
|
+
file: fp, line: i + 1, fix: null,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return findings;
|
|
231
|
+
},
|
|
232
|
+
},
|
|
233
|
+
];
|
|
234
|
+
|
|
235
|
+
export default rules;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// Bug detection: Unused destructured variables
|
|
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
|
+
// BUG-UNUSED-001: Destructured variable never used
|
|
7
|
+
{
|
|
8
|
+
id: 'BUG-UNUSED-001',
|
|
9
|
+
category: 'bugs',
|
|
10
|
+
severity: 'low',
|
|
11
|
+
confidence: 'suggestion',
|
|
12
|
+
title: 'Destructured variable appears unused — possible typo',
|
|
13
|
+
check({ files }) {
|
|
14
|
+
const findings = [];
|
|
15
|
+
for (const [fp, content] of files) {
|
|
16
|
+
if (!isJS(fp)) continue;
|
|
17
|
+
const lines = content.split('\n');
|
|
18
|
+
for (let i = 0; i < lines.length; i++) {
|
|
19
|
+
const line = lines[i];
|
|
20
|
+
if (line.trim().startsWith('//')) continue;
|
|
21
|
+
// Match: const { a, b, c } = expr or const [ a, b, c ] = expr
|
|
22
|
+
const objDestructure = line.match(/(?:const|let|var)\s+\{\s*([^}]+)\}\s*=/);
|
|
23
|
+
const arrDestructure = line.match(/(?:const|let|var)\s+\[\s*([^\]]+)\]\s*=/);
|
|
24
|
+
const match = objDestructure || arrDestructure;
|
|
25
|
+
if (!match) continue;
|
|
26
|
+
|
|
27
|
+
// Extract variable names
|
|
28
|
+
const names = match[1]
|
|
29
|
+
.split(',')
|
|
30
|
+
.map(part => {
|
|
31
|
+
const p = part.trim();
|
|
32
|
+
// Handle { key: alias } — the alias is the binding
|
|
33
|
+
if (p.includes(':')) {
|
|
34
|
+
return p.split(':')[1].trim().split(/[\s=]/)[0].trim();
|
|
35
|
+
}
|
|
36
|
+
// Handle { key = default } — key is the binding
|
|
37
|
+
return p.split('=')[0].trim();
|
|
38
|
+
})
|
|
39
|
+
.filter(n => n && /^\w+$/.test(n) && n !== '...');
|
|
40
|
+
|
|
41
|
+
// Check rest of file for each name
|
|
42
|
+
const restOfFile = content.slice(content.indexOf(line) + line.length);
|
|
43
|
+
for (const name of names) {
|
|
44
|
+
// Skip common intentionally unused patterns
|
|
45
|
+
if (name.startsWith('_')) continue;
|
|
46
|
+
// Check if name appears in the rest of the file (as word boundary)
|
|
47
|
+
const re = new RegExp(`\\b${name}\\b`);
|
|
48
|
+
if (!re.test(restOfFile)) {
|
|
49
|
+
findings.push({
|
|
50
|
+
ruleId: 'BUG-UNUSED-001', category: 'bugs', severity: 'low',
|
|
51
|
+
title: `Destructured variable "${name}" is never used — possible typo`,
|
|
52
|
+
description: `"${name}" is declared via destructuring but never referenced. This could be a typo or a variable that should be removed. Prefix with _ to mark as intentionally unused.`,
|
|
53
|
+
file: fp, line: i + 1,
|
|
54
|
+
fix: null,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return findings;
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
export default rules;
|