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.
Files changed (123) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +181 -0
  3. package/bin/doorman.js +444 -0
  4. package/package.json +74 -0
  5. package/src/ai-fixer.js +559 -0
  6. package/src/ast-scanner.js +434 -0
  7. package/src/auth.js +149 -0
  8. package/src/baseline.js +48 -0
  9. package/src/compliance.js +539 -0
  10. package/src/config.js +466 -0
  11. package/src/custom-rules.js +32 -0
  12. package/src/dashboard.js +202 -0
  13. package/src/detector.js +142 -0
  14. package/src/fix-engine.js +48 -0
  15. package/src/fix-registry-extra.js +95 -0
  16. package/src/fix-registry-go-rust.js +77 -0
  17. package/src/fix-registry-java-csharp.js +77 -0
  18. package/src/fix-registry-js.js +99 -0
  19. package/src/fix-registry-mcp-ai.js +57 -0
  20. package/src/fix-registry-python.js +87 -0
  21. package/src/fixer-ruby-php.js +608 -0
  22. package/src/fixer.js +2113 -0
  23. package/src/hooks.js +115 -0
  24. package/src/ignore.js +176 -0
  25. package/src/index.js +384 -0
  26. package/src/metrics.js +126 -0
  27. package/src/monorepo.js +65 -0
  28. package/src/presets.js +54 -0
  29. package/src/reporter.js +975 -0
  30. package/src/rule-worker.js +36 -0
  31. package/src/rules/ast-rules.js +756 -0
  32. package/src/rules/bugs/accessibility.js +235 -0
  33. package/src/rules/bugs/ai-codegen-fixable.js +172 -0
  34. package/src/rules/bugs/ai-codegen.js +365 -0
  35. package/src/rules/bugs/code-smell-bugs.js +247 -0
  36. package/src/rules/bugs/crypto-bugs.js +195 -0
  37. package/src/rules/bugs/docker-bugs.js +158 -0
  38. package/src/rules/bugs/general.js +361 -0
  39. package/src/rules/bugs/go-bugs.js +279 -0
  40. package/src/rules/bugs/index.js +73 -0
  41. package/src/rules/bugs/js-api.js +257 -0
  42. package/src/rules/bugs/js-array-object.js +210 -0
  43. package/src/rules/bugs/js-async-fixable.js +223 -0
  44. package/src/rules/bugs/js-async.js +211 -0
  45. package/src/rules/bugs/js-closure-scope.js +182 -0
  46. package/src/rules/bugs/js-database.js +203 -0
  47. package/src/rules/bugs/js-error-handling.js +148 -0
  48. package/src/rules/bugs/js-logic.js +261 -0
  49. package/src/rules/bugs/js-memory.js +214 -0
  50. package/src/rules/bugs/js-node.js +361 -0
  51. package/src/rules/bugs/js-react.js +373 -0
  52. package/src/rules/bugs/js-regex.js +200 -0
  53. package/src/rules/bugs/js-state.js +272 -0
  54. package/src/rules/bugs/js-type-coercion.js +318 -0
  55. package/src/rules/bugs/nextjs-bugs.js +242 -0
  56. package/src/rules/bugs/nextjs-fixable.js +120 -0
  57. package/src/rules/bugs/node-fixable.js +178 -0
  58. package/src/rules/bugs/python-advanced.js +245 -0
  59. package/src/rules/bugs/python-fixable.js +98 -0
  60. package/src/rules/bugs/python.js +284 -0
  61. package/src/rules/bugs/react-fixable.js +207 -0
  62. package/src/rules/bugs/ruby-bugs.js +182 -0
  63. package/src/rules/bugs/shell-bugs.js +181 -0
  64. package/src/rules/bugs/silent-failures.js +261 -0
  65. package/src/rules/bugs/ts-bugs.js +235 -0
  66. package/src/rules/bugs/unused-vars.js +65 -0
  67. package/src/rules/compliance/accessibility-ext.js +468 -0
  68. package/src/rules/compliance/education.js +322 -0
  69. package/src/rules/compliance/financial.js +421 -0
  70. package/src/rules/compliance/frameworks.js +507 -0
  71. package/src/rules/compliance/healthcare.js +520 -0
  72. package/src/rules/compliance/index.js +2714 -0
  73. package/src/rules/compliance/regional-eu.js +480 -0
  74. package/src/rules/compliance/regional-international.js +903 -0
  75. package/src/rules/cost/index.js +1993 -0
  76. package/src/rules/data/index.js +2503 -0
  77. package/src/rules/dependencies/index.js +1684 -0
  78. package/src/rules/deployment/index.js +2050 -0
  79. package/src/rules/index.js +71 -0
  80. package/src/rules/infrastructure/index.js +3048 -0
  81. package/src/rules/performance/index.js +3455 -0
  82. package/src/rules/quality/index.js +3175 -0
  83. package/src/rules/reliability/index.js +3040 -0
  84. package/src/rules/scope-rules.js +815 -0
  85. package/src/rules/security/ai-api.js +1177 -0
  86. package/src/rules/security/auth.js +1328 -0
  87. package/src/rules/security/cors.js +127 -0
  88. package/src/rules/security/crypto.js +527 -0
  89. package/src/rules/security/csharp.js +862 -0
  90. package/src/rules/security/csrf.js +193 -0
  91. package/src/rules/security/dart.js +835 -0
  92. package/src/rules/security/deserialization.js +291 -0
  93. package/src/rules/security/file-upload.js +187 -0
  94. package/src/rules/security/go.js +850 -0
  95. package/src/rules/security/headers.js +235 -0
  96. package/src/rules/security/index.js +65 -0
  97. package/src/rules/security/injection.js +1639 -0
  98. package/src/rules/security/mcp-server.js +71 -0
  99. package/src/rules/security/misconfiguration.js +660 -0
  100. package/src/rules/security/oauth-jwt.js +329 -0
  101. package/src/rules/security/path-traversal.js +295 -0
  102. package/src/rules/security/php.js +1054 -0
  103. package/src/rules/security/prototype-pollution.js +283 -0
  104. package/src/rules/security/rate-limiting.js +208 -0
  105. package/src/rules/security/ruby.js +1061 -0
  106. package/src/rules/security/rust.js +693 -0
  107. package/src/rules/security/secrets.js +747 -0
  108. package/src/rules/security/shell.js +647 -0
  109. package/src/rules/security/ssrf.js +298 -0
  110. package/src/rules/security/supply-chain-advanced.js +393 -0
  111. package/src/rules/security/supply-chain.js +734 -0
  112. package/src/rules/security/swift.js +835 -0
  113. package/src/rules/security/taint.js +27 -0
  114. package/src/rules/security/xss.js +520 -0
  115. package/src/scan-cache.js +71 -0
  116. package/src/scanner.js +710 -0
  117. package/src/scope-analyzer.js +685 -0
  118. package/src/share.js +88 -0
  119. package/src/taint.js +300 -0
  120. package/src/telemetry.js +183 -0
  121. package/src/tracer.js +190 -0
  122. package/src/upload.js +35 -0
  123. 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;