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,272 @@
1
+ // Bug detection: State management, mutation, and data flow 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-STATE-001',
8
+ category: 'bugs',
9
+ severity: 'high',
10
+ confidence: 'likely',
11
+ title: 'Shared mutable object between modules',
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
+ // export const/let config/state/defaults = { ... } (mutable exported object)
19
+ if (/^export\s+(let|var)\s+\w+\s*=\s*(\{|\[)/.test(lines[i])) {
20
+ findings.push({
21
+ ruleId: 'BUG-STATE-001', category: 'bugs', severity: 'high',
22
+ title: 'Mutable export with let/var — importers share and mutate the same object',
23
+ description: 'Exported let/var objects are shared across all importers. Mutations in one module affect all others. Use const with Object.freeze() or export a factory function.',
24
+ file: fp, line: i + 1, fix: null,
25
+ });
26
+ }
27
+ }
28
+ }
29
+ return findings;
30
+ },
31
+ },
32
+ {
33
+ id: 'BUG-STATE-002',
34
+ category: 'bugs',
35
+ severity: 'high',
36
+ confidence: 'likely',
37
+ title: 'Array/object parameter mutated inside function',
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 funcMatch = lines[i].match(/(?:function\s+\w+|(?:const|let)\s+\w+\s*=\s*(?:async\s*)?)\s*\(\s*(\w+)/);
45
+ if (!funcMatch) continue;
46
+ const paramName = funcMatch[1];
47
+ if (['e', 'err', 'error', 'event', 'req', 'res', 'next', 'ctx'].includes(paramName)) continue;
48
+ // Check next 15 lines for mutations on the parameter
49
+ for (let j = i + 1; j < Math.min(i + 15, lines.length); j++) {
50
+ const mutateRe = new RegExp(`\\b${paramName}\\s*\\.\\s*(push|pop|shift|unshift|splice|sort|reverse)\\s*\\(`);
51
+ const assignRe = new RegExp(`\\b${paramName}\\s*\\[.+\\]\\s*=`);
52
+ const propRe = new RegExp(`\\b${paramName}\\s*\\.\\s*\\w+\\s*=`);
53
+ if (mutateRe.test(lines[j]) || assignRe.test(lines[j]) || propRe.test(lines[j])) {
54
+ // Skip if the function is clearly a method that should mutate (has 'this')
55
+ const context = lines.slice(i, Math.min(i + 5, lines.length)).join('\n');
56
+ if (/\bthis\b/.test(context)) break;
57
+ findings.push({
58
+ ruleId: 'BUG-STATE-002', category: 'bugs', severity: 'high',
59
+ title: `Function parameter "${paramName}" mutated — caller's data modified unexpectedly`,
60
+ description: 'Mutating function parameters creates spooky action-at-a-distance. Clone the parameter first: [...arr] or {...obj}.',
61
+ file: fp, line: j + 1, fix: null,
62
+ });
63
+ break;
64
+ }
65
+ }
66
+ }
67
+ }
68
+ return findings;
69
+ },
70
+ },
71
+ {
72
+ id: 'BUG-STATE-003',
73
+ category: 'bugs',
74
+ severity: 'medium',
75
+ confidence: 'likely',
76
+ title: 'Global variable used for state',
77
+ check({ files }) {
78
+ const findings = [];
79
+ for (const [fp, content] of files) {
80
+ if (!isJS(fp)) continue;
81
+ const lines = content.split('\n');
82
+ // File-level let/var that's not a constant
83
+ for (let i = 0; i < lines.length; i++) {
84
+ if (/^(?:let|var)\s+(\w+)\s*=/.test(lines[i]) && !/^(?:let|var)\s+\w+\s*=\s*(?:require|import)/.test(lines[i])) {
85
+ const varName = lines[i].match(/^(?:let|var)\s+(\w+)/)[1];
86
+ // Check if it's reassigned later (used as state)
87
+ let reassigned = false;
88
+ for (let j = i + 1; j < lines.length; j++) {
89
+ const reassignRe = new RegExp(`^\\s*${varName}\\s*[+\\-*/]?=(?!=)`);
90
+ if (reassignRe.test(lines[j])) {
91
+ reassigned = true;
92
+ break;
93
+ }
94
+ }
95
+ if (reassigned) {
96
+ findings.push({
97
+ ruleId: 'BUG-STATE-003', category: 'bugs', severity: 'medium',
98
+ title: `Module-level mutable variable "${varName}" used as state`,
99
+ description: 'Module-level mutable state causes bugs in concurrent/serverless environments. In Node.js, modules are cached so state persists across requests.',
100
+ file: fp, line: i + 1, fix: null,
101
+ });
102
+ }
103
+ }
104
+ }
105
+ }
106
+ return findings;
107
+ },
108
+ },
109
+ {
110
+ id: 'BUG-STATE-004',
111
+ category: 'bugs',
112
+ severity: 'high',
113
+ confidence: 'definite',
114
+ title: 'Object spread only does shallow copy',
115
+ check({ files }) {
116
+ const findings = [];
117
+ for (const [fp, content] of files) {
118
+ if (!isJS(fp)) continue;
119
+ const lines = content.split('\n');
120
+ for (let i = 0; i < lines.length; i++) {
121
+ // Spread copy followed by nested mutation: const copy = {...obj}; copy.nested.prop = x
122
+ if (/(?:const|let|var)\s+(\w+)\s*=\s*\{\s*\.\.\./.test(lines[i])) {
123
+ const copyName = lines[i].match(/(?:const|let|var)\s+(\w+)/)[1];
124
+ for (let j = i + 1; j < Math.min(i + 10, lines.length); j++) {
125
+ // copy.nested.something = or copy.nested.push(
126
+ const deepMutate = new RegExp(`\\b${copyName}\\.(\\w+)\\.(\\w+)\\s*=|\\b${copyName}\\.(\\w+)\\.\\w+\\s*\\.\\s*(push|pop|splice)\\s*\\(`);
127
+ if (deepMutate.test(lines[j])) {
128
+ findings.push({
129
+ ruleId: 'BUG-STATE-004', category: 'bugs', severity: 'high',
130
+ title: 'Shallow copy via spread then deep mutation — original object modified',
131
+ description: 'Spread ({...obj}) only copies one level deep. Nested objects are still shared references. Use structuredClone() or a deep clone utility.',
132
+ file: fp, line: j + 1, fix: null,
133
+ });
134
+ break;
135
+ }
136
+ }
137
+ }
138
+ }
139
+ }
140
+ return findings;
141
+ },
142
+ },
143
+ {
144
+ id: 'BUG-STATE-005',
145
+ category: 'bugs',
146
+ severity: 'medium',
147
+ confidence: 'likely',
148
+ title: 'Date object mutated unexpectedly',
149
+ check({ files }) {
150
+ const findings = [];
151
+ for (const [fp, content] of files) {
152
+ if (!isJS(fp)) continue;
153
+ const lines = content.split('\n');
154
+ for (let i = 0; i < lines.length; i++) {
155
+ // date.setHours/setDate/setMonth etc. — mutates in place
156
+ if (/\b\w+\.(setFullYear|setMonth|setDate|setHours|setMinutes|setSeconds|setTime|setUTCDate)\s*\(/.test(lines[i])) {
157
+ // Check if the date was a parameter or prop
158
+ const context = lines.slice(Math.max(0, i - 5), i).join('\n');
159
+ if (/function|=>|\.map|\.forEach/.test(context)) {
160
+ findings.push({
161
+ ruleId: 'BUG-STATE-005', category: 'bugs', severity: 'medium',
162
+ title: 'Date set*() mutates the original Date object',
163
+ description: 'Date.set*() methods mutate in place. If the Date came from outside (prop, parameter), the original is modified. Create a new Date first.',
164
+ file: fp, line: i + 1, fix: null,
165
+ });
166
+ }
167
+ }
168
+ }
169
+ }
170
+ return findings;
171
+ },
172
+ },
173
+ {
174
+ id: 'BUG-STATE-006',
175
+ category: 'bugs',
176
+ severity: 'high',
177
+ confidence: 'likely',
178
+ title: 'Redux/Zustand state mutated directly',
179
+ check({ files }) {
180
+ const findings = [];
181
+ for (const [fp, content] of files) {
182
+ if (!isJS(fp)) continue;
183
+ const lines = content.split('\n');
184
+ // Detect Redux reducer pattern
185
+ for (let i = 0; i < lines.length; i++) {
186
+ if (/(?:case\s+['"`]\w+['"`]\s*:|createReducer|createSlice)/.test(lines[i])) {
187
+ // Look for state.x.push, state.x = in next lines (OK in createSlice/immer, not in plain reducers)
188
+ const isImmer = /createSlice|immer|produce/.test(content);
189
+ if (isImmer) continue;
190
+ for (let j = i + 1; j < Math.min(i + 10, lines.length); j++) {
191
+ if (/state\.\w+\.(push|pop|splice|shift|unshift|sort|reverse)\s*\(/.test(lines[j]) ||
192
+ /state\.\w+\s*=\s*(?!\.\.\.state)/.test(lines[j])) {
193
+ findings.push({
194
+ ruleId: 'BUG-STATE-006', category: 'bugs', severity: 'high',
195
+ title: 'Direct state mutation in reducer — Redux won\'t detect the change',
196
+ description: 'Redux requires immutable updates. Mutating state directly means React won\'t re-render. Return a new object: { ...state, prop: newValue }.',
197
+ file: fp, line: j + 1, fix: null,
198
+ });
199
+ break;
200
+ }
201
+ }
202
+ }
203
+ }
204
+ }
205
+ return findings;
206
+ },
207
+ },
208
+ {
209
+ id: 'BUG-STATE-007',
210
+ category: 'bugs',
211
+ severity: 'medium',
212
+ confidence: 'likely',
213
+ title: 'localStorage used without try/catch',
214
+ check({ files }) {
215
+ const findings = [];
216
+ for (const [fp, content] of files) {
217
+ if (!isJS(fp)) continue;
218
+ const lines = content.split('\n');
219
+ for (let i = 0; i < lines.length; i++) {
220
+ if (/localStorage\.(getItem|setItem|removeItem)\s*\(/.test(lines[i])) {
221
+ // Check if inside try/catch
222
+ const context = lines.slice(Math.max(0, i - 5), i).join('\n');
223
+ if (!/try\s*\{/.test(context)) {
224
+ findings.push({
225
+ ruleId: 'BUG-STATE-007', category: 'bugs', severity: 'medium',
226
+ title: 'localStorage access without try/catch — throws in private browsing and when storage is full',
227
+ description: 'localStorage throws in Safari private mode, when storage quota is exceeded, and in some security contexts. Always wrap in try/catch.',
228
+ file: fp, line: i + 1, fix: null,
229
+ });
230
+ }
231
+ }
232
+ }
233
+ }
234
+ return findings;
235
+ },
236
+ },
237
+ {
238
+ id: 'BUG-STATE-008',
239
+ category: 'bugs',
240
+ severity: 'high',
241
+ confidence: 'likely',
242
+ title: 'Mutable default parameter value',
243
+ check({ files }) {
244
+ const findings = [];
245
+ for (const [fp, content] of files) {
246
+ if (!isJS(fp)) continue;
247
+ const lines = content.split('\n');
248
+ for (let i = 0; i < lines.length; i++) {
249
+ // function foo(items = []) and then items.push inside
250
+ const match = lines[i].match(/(?:function\s+\w+|(?:const|let)\s+\w+\s*=\s*(?:async\s*)?)\s*\([^)]*(\w+)\s*=\s*\[\s*\]/);
251
+ if (!match) continue;
252
+ const paramName = match[1];
253
+ for (let j = i + 1; j < Math.min(i + 15, lines.length); j++) {
254
+ const mutateRe = new RegExp(`\\b${paramName}\\s*\\.\\s*(push|pop|splice|shift|unshift)\\s*\\(`);
255
+ if (mutateRe.test(lines[j])) {
256
+ findings.push({
257
+ ruleId: 'BUG-STATE-008', category: 'bugs', severity: 'high',
258
+ title: 'Default parameter array/object mutated — shared across calls in some engines',
259
+ description: 'While JS creates new defaults each call (unlike Python), mutating default params is confusing and error-prone. Clone first or use a different pattern.',
260
+ file: fp, line: j + 1, fix: null,
261
+ });
262
+ break;
263
+ }
264
+ }
265
+ }
266
+ }
267
+ return findings;
268
+ },
269
+ },
270
+ ];
271
+
272
+ export default rules;
@@ -0,0 +1,318 @@
1
+ // Bug detection: JavaScript type coercion and comparison 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-TYPE-001',
8
+ category: 'bugs',
9
+ severity: 'high',
10
+ confidence: 'likely',
11
+ title: 'Array.sort() without comparator — sorts numbers as strings',
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 (/\.sort\(\s*\)/.test(lines[i])) {
19
+ // Check if array likely contains numbers
20
+ const context = lines.slice(Math.max(0, i - 5), i + 1).join('\n');
21
+ if (/(?:number|count|age|price|score|amount|total|id|num|index|length)/i.test(context) || /\[\s*\d/.test(context)) {
22
+ findings.push({
23
+ ruleId: 'BUG-TYPE-001', category: 'bugs', severity: 'high',
24
+ title: 'Array.sort() without comparator — [10, 9, 2].sort() returns [10, 2, 9]',
25
+ description: 'Array.sort() converts elements to strings by default. Use .sort((a, b) => a - b) for numeric sorting.',
26
+ file: fp, line: i + 1, fix: null,
27
+ });
28
+ }
29
+ }
30
+ }
31
+ }
32
+ return findings;
33
+ },
34
+ },
35
+ {
36
+ id: 'BUG-TYPE-002',
37
+ category: 'bugs',
38
+ severity: 'high',
39
+ confidence: 'likely',
40
+ title: 'typeof null === "object" — null check will fail',
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
+ if (/typeof\s+\w+\s*===?\s*['"]object['"]/.test(lines[i]) && !lines[i].includes('null')) {
48
+ const context = lines.slice(Math.max(0, i - 3), i + 3).join('\n');
49
+ if (!context.includes('!== null') && !context.includes('!= null') && !context.includes('&& ')) {
50
+ findings.push({
51
+ ruleId: 'BUG-TYPE-002', category: 'bugs', severity: 'high',
52
+ title: 'typeof check for "object" without null guard',
53
+ description: 'typeof null === "object" is true in JavaScript. Always check for null before checking typeof === "object".',
54
+ file: fp, line: i + 1, fix: null,
55
+ });
56
+ }
57
+ }
58
+ }
59
+ }
60
+ return findings;
61
+ },
62
+ },
63
+ {
64
+ id: 'BUG-TYPE-003',
65
+ category: 'bugs',
66
+ severity: 'high',
67
+ confidence: 'likely',
68
+ title: 'parseInt without radix — octal/hex parsing surprise',
69
+ check({ files }) {
70
+ const findings = [];
71
+ for (const [fp, content] of files) {
72
+ if (!isJS(fp)) continue;
73
+ const lines = content.split('\n');
74
+ for (let i = 0; i < lines.length; i++) {
75
+ if (/parseInt\s*\(\s*\w+\s*\)/.test(lines[i]) && !/parseInt\s*\([^,]+,/.test(lines[i])) {
76
+ findings.push({
77
+ ruleId: 'BUG-TYPE-003', category: 'bugs', severity: 'high',
78
+ title: 'parseInt() without radix parameter',
79
+ description: 'parseInt("08") may return 0 in older engines (octal). Always pass radix: parseInt(str, 10).',
80
+ file: fp, line: i + 1, fix: null,
81
+ });
82
+ }
83
+ }
84
+ }
85
+ return findings;
86
+ },
87
+ },
88
+ {
89
+ id: 'BUG-TYPE-004',
90
+ category: 'bugs',
91
+ severity: 'medium',
92
+ confidence: 'likely',
93
+ title: 'Floating point comparison — 0.1 + 0.2 !== 0.3',
94
+ check({ files }) {
95
+ const findings = [];
96
+ for (const [fp, content] of files) {
97
+ if (!isJS(fp)) continue;
98
+ const lines = content.split('\n');
99
+ for (let i = 0; i < lines.length; i++) {
100
+ const line = lines[i];
101
+ // Detect direct equality comparison with floating point arithmetic
102
+ if (/===?\s*\d+\.\d+/.test(line) && /[\+\-\*\/]/.test(line) && /\d+\.\d+/.test(line)) {
103
+ findings.push({
104
+ ruleId: 'BUG-TYPE-004', category: 'bugs', severity: 'medium',
105
+ title: 'Direct floating-point equality comparison',
106
+ description: '0.1 + 0.2 !== 0.3 in IEEE 754. Use Math.abs(a - b) < Number.EPSILON or a tolerance value.',
107
+ file: fp, line: i + 1, fix: null,
108
+ });
109
+ }
110
+ }
111
+ }
112
+ return findings;
113
+ },
114
+ },
115
+ {
116
+ id: 'BUG-TYPE-005',
117
+ category: 'bugs',
118
+ severity: 'high',
119
+ confidence: 'likely',
120
+ title: 'isNaN() instead of Number.isNaN() — type coercion bug',
121
+ check({ files }) {
122
+ const findings = [];
123
+ for (const [fp, content] of files) {
124
+ if (!isJS(fp)) continue;
125
+ const lines = content.split('\n');
126
+ for (let i = 0; i < lines.length; i++) {
127
+ if (/[^.]isNaN\s*\(/.test(lines[i]) && !lines[i].includes('Number.isNaN')) {
128
+ findings.push({
129
+ ruleId: 'BUG-TYPE-005', category: 'bugs', severity: 'high',
130
+ title: 'isNaN() coerces argument — isNaN("hello") is true',
131
+ description: 'Global isNaN() converts the argument to a number first. Use Number.isNaN() for strict NaN checking.',
132
+ file: fp, line: i + 1, fix: null,
133
+ });
134
+ }
135
+ }
136
+ }
137
+ return findings;
138
+ },
139
+ },
140
+ {
141
+ id: 'BUG-TYPE-006',
142
+ category: 'bugs',
143
+ severity: 'medium',
144
+ confidence: 'likely',
145
+ title: 'String concatenation with + instead of template literal',
146
+ check({ files }) {
147
+ const findings = [];
148
+ for (const [fp, content] of files) {
149
+ if (!isJS(fp)) continue;
150
+ const lines = content.split('\n');
151
+ for (let i = 0; i < lines.length; i++) {
152
+ const line = lines[i];
153
+ // Detect "string" + variable + "string" patterns (prone to coercion bugs)
154
+ if (/['"][^'"]*['"]\s*\+\s*\w+\s*\+\s*['"]/.test(line)) {
155
+ // Only flag if it looks like it could have a number coercion issue
156
+ if (/(?:id|count|num|amount|total|index|length|size|port)/i.test(line)) {
157
+ findings.push({
158
+ ruleId: 'BUG-TYPE-006', category: 'bugs', severity: 'medium',
159
+ title: 'String concatenation with + may cause type coercion',
160
+ description: '"Count: " + count + " items" may produce unexpected results. Use template literals: `Count: ${count} items`.',
161
+ file: fp, line: i + 1, fix: null,
162
+ });
163
+ }
164
+ }
165
+ }
166
+ }
167
+ return findings;
168
+ },
169
+ },
170
+ {
171
+ id: 'BUG-TYPE-007',
172
+ category: 'bugs',
173
+ severity: 'high',
174
+ confidence: 'likely',
175
+ title: 'JSON.parse without try/catch — crashes on malformed input',
176
+ check({ files }) {
177
+ const findings = [];
178
+ for (const [fp, content] of files) {
179
+ if (!isJS(fp)) continue;
180
+ const lines = content.split('\n');
181
+ for (let i = 0; i < lines.length; i++) {
182
+ if (/JSON\.parse\s*\(/.test(lines[i])) {
183
+ const context = lines.slice(Math.max(0, i - 5), Math.min(lines.length, i + 5)).join('\n');
184
+ if (!context.includes('try') && !context.includes('catch')) {
185
+ findings.push({
186
+ ruleId: 'BUG-TYPE-007', category: 'bugs', severity: 'high',
187
+ title: 'JSON.parse without try/catch — throws on invalid JSON',
188
+ description: 'JSON.parse() throws SyntaxError on malformed input. Wrap in try/catch to handle invalid JSON gracefully.',
189
+ file: fp, line: i + 1, fix: null,
190
+ });
191
+ }
192
+ }
193
+ }
194
+ }
195
+ return findings;
196
+ },
197
+ },
198
+ {
199
+ id: 'BUG-TYPE-008',
200
+ category: 'bugs',
201
+ severity: 'high',
202
+ confidence: 'likely',
203
+ title: 'Date month is 0-indexed — new Date(2024, 1, 1) is February',
204
+ check({ files }) {
205
+ const findings = [];
206
+ for (const [fp, content] of files) {
207
+ if (!isJS(fp)) continue;
208
+ const lines = content.split('\n');
209
+ for (let i = 0; i < lines.length; i++) {
210
+ const match = lines[i].match(/new\s+Date\s*\(\s*\d{4}\s*,\s*(\d{1,2})/);
211
+ if (match) {
212
+ const month = parseInt(match[1], 10);
213
+ if (month >= 1 && month <= 12) {
214
+ findings.push({
215
+ ruleId: 'BUG-TYPE-008', category: 'bugs', severity: 'high',
216
+ title: 'Date constructor month is 0-indexed — month ' + month + ' is actually ' + ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'][month],
217
+ description: 'JavaScript Date months are 0-indexed. new Date(2024, 1, 1) is February 1st, not January. Subtract 1 from the month.',
218
+ file: fp, line: i + 1, fix: null,
219
+ });
220
+ }
221
+ }
222
+ }
223
+ }
224
+ return findings;
225
+ },
226
+ },
227
+
228
+ // BUG-TYPE-009: Loose equality (== instead of ===)
229
+ {
230
+ id: 'BUG-TYPE-009',
231
+ category: 'bugs',
232
+ severity: 'medium',
233
+ confidence: 'likely',
234
+ title: 'Loose equality (==) used instead of strict equality (===)',
235
+ check({ files }) {
236
+ const findings = [];
237
+ for (const [fp, content] of files) {
238
+ if (!isJS(fp)) continue;
239
+ const lines = content.split('\n');
240
+ for (let i = 0; i < lines.length; i++) {
241
+ const line = lines[i];
242
+ if (line.trim().startsWith('//')) continue;
243
+ // Match == or != that are NOT === or !==
244
+ // Use negative lookahead/lookbehind to avoid matching === and !==
245
+ const matches = line.matchAll(/(?<!=)(?:==|!=)(?!=)/g);
246
+ for (const m of matches) {
247
+ const idx = m.index;
248
+ // Skip if inside a string
249
+ const before = line.slice(0, idx);
250
+ const singleQuotes = (before.match(/'/g) || []).length;
251
+ const doubleQuotes = (before.match(/"/g) || []).length;
252
+ const backticks = (before.match(/`/g) || []).length;
253
+ if (singleQuotes % 2 !== 0 || doubleQuotes % 2 !== 0 || backticks % 2 !== 0) continue;
254
+ // Skip == null / != null checks (common intentional pattern to check null/undefined)
255
+ const after = line.slice(idx + 2).trim();
256
+ if (/^null\b/.test(after) || /^undefined\b/.test(after)) continue;
257
+ const beforeTrimmed = line.slice(0, idx).trimEnd();
258
+ if (/null$/.test(beforeTrimmed) || /undefined$/.test(beforeTrimmed)) continue;
259
+ findings.push({
260
+ ruleId: 'BUG-TYPE-009', category: 'bugs', severity: 'medium',
261
+ title: `Loose equality (${m[0]}) — use ${m[0] === '==' ? '===' : '!=='} instead`,
262
+ description: `Loose equality (${m[0]}) performs type coercion: "0" == false is true, "" == 0 is true. Use strict equality (${m[0] === '==' ? '===' : '!=='}) to avoid unexpected type coercion bugs.`,
263
+ file: fp, line: i + 1, fix: {
264
+ type: 'replace',
265
+ old: m[0],
266
+ new: m[0] === '==' ? '===' : '!==',
267
+ },
268
+ });
269
+ break; // One finding per line
270
+ }
271
+ }
272
+ }
273
+ return findings;
274
+ },
275
+ },
276
+
277
+ // BUG-TYPE-010: typeof check for arrays (should use Array.isArray)
278
+ {
279
+ id: 'BUG-TYPE-010',
280
+ category: 'bugs',
281
+ severity: 'medium',
282
+ confidence: 'likely',
283
+ title: 'typeof used for array check — use Array.isArray() instead',
284
+ check({ files }) {
285
+ const findings = [];
286
+ for (const [fp, content] of files) {
287
+ if (!isJS(fp)) continue;
288
+ const lines = content.split('\n');
289
+ for (let i = 0; i < lines.length; i++) {
290
+ const line = lines[i];
291
+ if (line.trim().startsWith('//')) continue;
292
+ // Check for typeof x === 'object' in context of arrays
293
+ if (/typeof\s+\w+\s*===?\s*['"]object['"]/.test(line)) {
294
+ // Look for array-related variable names or context
295
+ const varMatch = line.match(/typeof\s+(\w+)/);
296
+ if (!varMatch) continue;
297
+ const varName = varMatch[1];
298
+ // Check if it's in a context suggesting array check
299
+ if (/arr|list|items|elements|data|result|values|entries/i.test(varName)) {
300
+ // Make sure Array.isArray isn't also used nearby
301
+ const context = lines.slice(Math.max(0, i - 2), Math.min(lines.length, i + 3)).join(' ');
302
+ if (context.includes('Array.isArray')) continue;
303
+ findings.push({
304
+ ruleId: 'BUG-TYPE-010', category: 'bugs', severity: 'medium',
305
+ title: `typeof ${varName} === "object" doesn't distinguish arrays from objects`,
306
+ description: `typeof returns "object" for arrays, objects, null, Date, Map, etc. If you're checking for an array, use Array.isArray(${varName}) instead.`,
307
+ file: fp, line: i + 1, fix: null,
308
+ });
309
+ }
310
+ }
311
+ }
312
+ }
313
+ return findings;
314
+ },
315
+ },
316
+ ];
317
+
318
+ export default rules;