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,235 @@
1
+ // Bug detection: Accessibility (a11y) patterns — things AI generators almost always miss
2
+ const JSX_EXT = ['.jsx', '.tsx'];
3
+ function isJSX(f) { return JSX_EXT.some(e => f.endsWith(e)); }
4
+ function isHTML(f) { return f.endsWith('.html') || f.endsWith('.htm') || f.endsWith('.erb') || f.endsWith('.ejs') || f.endsWith('.hbs') || f.endsWith('.svelte') || f.endsWith('.vue'); }
5
+ function isTemplate(f) { return isJSX(f) || isHTML(f); }
6
+
7
+ const rules = [
8
+ {
9
+ id: 'BUG-A11Y-001',
10
+ category: 'bugs',
11
+ severity: 'medium',
12
+ confidence: 'definite',
13
+ title: 'Image without alt attribute',
14
+ check({ files }) {
15
+ const findings = [];
16
+ for (const [fp, content] of files) {
17
+ if (!isTemplate(fp)) continue;
18
+ const lines = content.split('\n');
19
+ for (let i = 0; i < lines.length; i++) {
20
+ if (/<img\b/.test(lines[i])) {
21
+ // Check this line and next couple for alt
22
+ const block = lines.slice(i, Math.min(i + 3, lines.length)).join(' ');
23
+ if (!/alt\s*=/.test(block) && !/\/\>/.test(lines[i]) || (/<img\b/.test(lines[i]) && /\/>/.test(block) && !/alt\s*=/.test(block))) {
24
+ findings.push({
25
+ ruleId: 'BUG-A11Y-001', category: 'bugs', severity: 'medium',
26
+ title: '<img> without alt attribute — screen readers can\'t describe the image',
27
+ description: 'All images must have alt text. Use alt="" for decorative images, or a descriptive alt for meaningful ones.',
28
+ file: fp, line: i + 1, fix: null,
29
+ });
30
+ }
31
+ }
32
+ }
33
+ }
34
+ return findings;
35
+ },
36
+ },
37
+ {
38
+ id: 'BUG-A11Y-002',
39
+ category: 'bugs',
40
+ severity: 'high',
41
+ confidence: 'definite',
42
+ title: 'Click handler on non-interactive element',
43
+ check({ files }) {
44
+ const findings = [];
45
+ for (const [fp, content] of files) {
46
+ if (!isJSX(fp)) continue;
47
+ const lines = content.split('\n');
48
+ for (let i = 0; i < lines.length; i++) {
49
+ // <div onClick= or <span onClick= without role
50
+ if (/<(?:div|span|p|li|section|article|header|footer|main)\b[^>]*onClick/.test(lines[i])) {
51
+ const block = lines.slice(i, Math.min(i + 3, lines.length)).join(' ');
52
+ if (!/role\s*=/.test(block) && !/tabIndex/.test(block)) {
53
+ findings.push({
54
+ ruleId: 'BUG-A11Y-002', category: 'bugs', severity: 'high',
55
+ title: 'onClick on non-interactive element (div/span) without role and tabIndex',
56
+ description: 'Divs and spans are not focusable or interactive by default. Keyboard users can\'t trigger onClick. Use <button> instead, or add role="button" tabIndex={0} onKeyDown.',
57
+ file: fp, line: i + 1, fix: null,
58
+ });
59
+ }
60
+ }
61
+ }
62
+ }
63
+ return findings;
64
+ },
65
+ },
66
+ {
67
+ id: 'BUG-A11Y-003',
68
+ category: 'bugs',
69
+ severity: 'medium',
70
+ confidence: 'likely',
71
+ title: 'Form input without associated label',
72
+ check({ files }) {
73
+ const findings = [];
74
+ for (const [fp, content] of files) {
75
+ if (!isTemplate(fp)) continue;
76
+ const lines = content.split('\n');
77
+ for (let i = 0; i < lines.length; i++) {
78
+ if (/<input\b/.test(lines[i]) && !/type\s*=\s*['"](?:hidden|submit|button|reset|image)['"]/.test(lines[i])) {
79
+ const block = lines.slice(Math.max(0, i - 3), Math.min(i + 3, lines.length)).join(' ');
80
+ if (!/aria-label\s*=|aria-labelledby\s*=|<label|id\s*=/.test(block) && !/placeholder/.test(lines[i])) {
81
+ findings.push({
82
+ ruleId: 'BUG-A11Y-003', category: 'bugs', severity: 'medium',
83
+ title: '<input> without label, aria-label, or aria-labelledby',
84
+ description: 'Screen readers need labels to identify form fields. Use <label htmlFor="id">, aria-label, or aria-labelledby.',
85
+ file: fp, line: i + 1, fix: null,
86
+ });
87
+ }
88
+ }
89
+ }
90
+ }
91
+ return findings;
92
+ },
93
+ },
94
+ {
95
+ id: 'BUG-A11Y-004',
96
+ category: 'bugs',
97
+ severity: 'medium',
98
+ confidence: 'definite',
99
+ title: 'Missing lang attribute on html element',
100
+ check({ files }) {
101
+ const findings = [];
102
+ for (const [fp, content] of files) {
103
+ if (!isHTML(fp) && !/layout\.(tsx|jsx)$/.test(fp)) continue;
104
+ if (/<html\b/.test(content) && !/<html[^>]*\blang\s*=/.test(content)) {
105
+ findings.push({
106
+ ruleId: 'BUG-A11Y-004', category: 'bugs', severity: 'medium',
107
+ title: '<html> without lang attribute — screen readers don\'t know the language',
108
+ description: 'Add lang="en" (or appropriate language) to the <html> tag. Screen readers use this to select the correct pronunciation.',
109
+ file: fp, line: 1, fix: null,
110
+ });
111
+ }
112
+ }
113
+ return findings;
114
+ },
115
+ },
116
+ {
117
+ id: 'BUG-A11Y-005',
118
+ category: 'bugs',
119
+ severity: 'medium',
120
+ confidence: 'likely',
121
+ title: 'Heading level skipped',
122
+ check({ files }) {
123
+ const findings = [];
124
+ for (const [fp, content] of files) {
125
+ if (!isTemplate(fp)) continue;
126
+ const lines = content.split('\n');
127
+ const headings = [];
128
+ for (let i = 0; i < lines.length; i++) {
129
+ const match = lines[i].match(/<h([1-6])\b/);
130
+ if (match) {
131
+ headings.push({ level: parseInt(match[1], 10), line: i + 1 });
132
+ }
133
+ }
134
+ for (let h = 1; h < headings.length; h++) {
135
+ if (headings[h].level > headings[h - 1].level + 1) {
136
+ findings.push({
137
+ ruleId: 'BUG-A11Y-005', category: 'bugs', severity: 'medium',
138
+ title: `Heading level skipped: h${headings[h - 1].level} → h${headings[h].level}`,
139
+ description: 'Heading levels should not skip (e.g., h2 → h4). Screen reader users navigate by headings and expect a logical hierarchy.',
140
+ file: fp, line: headings[h].line, fix: null,
141
+ });
142
+ }
143
+ }
144
+ }
145
+ return findings;
146
+ },
147
+ },
148
+ {
149
+ id: 'BUG-A11Y-006',
150
+ category: 'bugs',
151
+ severity: 'high',
152
+ confidence: 'definite',
153
+ title: 'Anchor tag used as button',
154
+ check({ files }) {
155
+ const findings = [];
156
+ for (const [fp, content] of files) {
157
+ if (!isTemplate(fp)) continue;
158
+ const lines = content.split('\n');
159
+ for (let i = 0; i < lines.length; i++) {
160
+ // <a href="#" onClick= or <a href="javascript:" onClick=
161
+ if (/<a\b[^>]*href\s*=\s*['"](?:#|javascript:)['"][^>]*onClick/.test(lines[i])) {
162
+ findings.push({
163
+ ruleId: 'BUG-A11Y-006', category: 'bugs', severity: 'high',
164
+ title: '<a href="#"> used as button — use <button> instead',
165
+ description: 'Links with href="#" or "javascript:" are fake buttons. They confuse screen readers (announced as "link"), don\'t work with keyboard (Enter vs Space), and scroll to top. Use <button>.',
166
+ file: fp, line: i + 1, fix: null,
167
+ });
168
+ }
169
+ }
170
+ }
171
+ return findings;
172
+ },
173
+ },
174
+ {
175
+ id: 'BUG-A11Y-007',
176
+ category: 'bugs',
177
+ severity: 'medium',
178
+ confidence: 'likely',
179
+ title: 'Color used as only indicator',
180
+ check({ files }) {
181
+ const findings = [];
182
+ for (const [fp, content] of files) {
183
+ if (!isTemplate(fp)) continue;
184
+ const lines = content.split('\n');
185
+ for (let i = 0; i < lines.length; i++) {
186
+ // Conditional styling based on status without text indicator
187
+ if (/className\s*=.*(?:text-red|text-green|bg-red|bg-green|color:\s*red|color:\s*green)/.test(lines[i]) ||
188
+ /style\s*=.*(?:color:\s*['"]?(?:red|green|#[0-9a-f]{3,6}))/.test(lines[i])) {
189
+ // Check if there's also a text/icon indicator nearby
190
+ const block = lines.slice(i, Math.min(i + 3, lines.length)).join(' ');
191
+ if (/error|success|warning|status|valid|invalid/.test(block) && !/aria-label|sr-only|screen-reader|icon|✓|✗|⚠|❌|✅/.test(block)) {
192
+ findings.push({
193
+ ruleId: 'BUG-A11Y-007', category: 'bugs', severity: 'medium',
194
+ title: 'Status indicated by color only — inaccessible to colorblind users',
195
+ description: 'Don\'t rely on color alone to convey information. Add text, icons, or aria-labels alongside color indicators.',
196
+ file: fp, line: i + 1, fix: null,
197
+ });
198
+ }
199
+ }
200
+ }
201
+ }
202
+ return findings;
203
+ },
204
+ },
205
+ {
206
+ id: 'BUG-A11Y-008',
207
+ category: 'bugs',
208
+ severity: 'medium',
209
+ confidence: 'definite',
210
+ title: 'Auto-playing media without user control',
211
+ check({ files }) {
212
+ const findings = [];
213
+ for (const [fp, content] of files) {
214
+ if (!isTemplate(fp)) continue;
215
+ const lines = content.split('\n');
216
+ for (let i = 0; i < lines.length; i++) {
217
+ if (/<(?:video|audio)\b[^>]*autoPlay|<(?:video|audio)\b[^>]*autoplay/.test(lines[i])) {
218
+ const block = lines.slice(i, Math.min(i + 3, lines.length)).join(' ');
219
+ if (!/muted/.test(block)) {
220
+ findings.push({
221
+ ruleId: 'BUG-A11Y-008', category: 'bugs', severity: 'medium',
222
+ title: 'Auto-playing media with sound — disorienting for screen reader users',
223
+ description: 'Auto-playing audio interferes with screen readers. Add muted attribute or provide a way to pause.',
224
+ file: fp, line: i + 1, fix: null,
225
+ });
226
+ }
227
+ }
228
+ }
229
+ }
230
+ return findings;
231
+ },
232
+ },
233
+ ];
234
+
235
+ export default rules;
@@ -0,0 +1,172 @@
1
+ // Auto-fixable patterns commonly left by AI code generators
2
+ const JS_EXT = ['.js', '.jsx', '.ts', '.tsx', '.mjs', '.cjs'];
3
+ function isJS(f) { return JS_EXT.some(e => f.endsWith(e)); }
4
+ function isPy(f) { return f.endsWith('.py'); }
5
+
6
+ const rules = [
7
+ {
8
+ id: 'BUG-AIFIX-001',
9
+ category: 'bugs',
10
+ severity: 'medium',
11
+ confidence: 'definite',
12
+ title: '"your-api-key" placeholder → process.env reference',
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 match = lines[i].match(/['"`](your[-_]?api[-_]?key|your[-_]?secret[-_]?key|sk-[.x]+)['"`]/i);
20
+ if (match) {
21
+ findings.push({
22
+ ruleId: 'BUG-AIFIX-001', category: 'bugs', severity: 'medium',
23
+ title: 'AI placeholder API key → use environment variable',
24
+ file: fp, line: i + 1,
25
+ fix: {
26
+ type: 'replace',
27
+ old: match[0],
28
+ new: "process.env.API_KEY",
29
+ },
30
+ });
31
+ }
32
+ }
33
+ }
34
+ return findings;
35
+ },
36
+ },
37
+ {
38
+ id: 'BUG-AIFIX-002',
39
+ category: 'bugs',
40
+ severity: 'high',
41
+ confidence: 'definite',
42
+ title: 'localhost URL → env variable',
43
+ check({ files }) {
44
+ const findings = [];
45
+ for (const [fp, content] of files) {
46
+ if (!isJS(fp)) continue;
47
+ if (/config|\.env|constant|setting|test|spec/i.test(fp)) continue;
48
+ const lines = content.split('\n');
49
+ for (let i = 0; i < lines.length; i++) {
50
+ const match = lines[i].match(/(['"`])(https?:\/\/localhost:\d+)\1/);
51
+ if (match) {
52
+ findings.push({
53
+ ruleId: 'BUG-AIFIX-002', category: 'bugs', severity: 'high',
54
+ title: 'Hardcoded localhost URL → use environment variable',
55
+ file: fp, line: i + 1,
56
+ fix: {
57
+ type: 'replace',
58
+ old: match[0],
59
+ new: "process.env.API_URL || " + match[0],
60
+ },
61
+ });
62
+ }
63
+ }
64
+ }
65
+ return findings;
66
+ },
67
+ },
68
+ {
69
+ id: 'BUG-AIFIX-003',
70
+ category: 'bugs',
71
+ severity: 'medium',
72
+ confidence: 'definite',
73
+ title: '=== undefined → optional chaining or nullish coalescing',
74
+ check({ files }) {
75
+ const findings = [];
76
+ for (const [fp, content] of files) {
77
+ if (!isJS(fp)) continue;
78
+ const lines = content.split('\n');
79
+ for (let i = 0; i < lines.length; i++) {
80
+ // if (x.y === undefined) or if (x.y !== undefined)
81
+ const match = lines[i].match(/if\s*\(\s*(\w+\.\w+)\s*===\s*undefined\s*\)/);
82
+ if (match) {
83
+ findings.push({
84
+ ruleId: 'BUG-AIFIX-003', category: 'bugs', severity: 'medium',
85
+ title: '=== undefined check → use optional chaining',
86
+ file: fp, line: i + 1,
87
+ fix: {
88
+ type: 'replace',
89
+ old: `${match[1]} === undefined`,
90
+ new: `${match[1]} == null`,
91
+ },
92
+ });
93
+ }
94
+ }
95
+ }
96
+ return findings;
97
+ },
98
+ },
99
+ {
100
+ id: 'BUG-AIFIX-004',
101
+ category: 'bugs',
102
+ severity: 'medium',
103
+ confidence: 'definite',
104
+ title: 'Python print() debug statement → logging',
105
+ check({ files }) {
106
+ const findings = [];
107
+ for (const [fp, content] of files) {
108
+ if (!isPy(fp)) continue;
109
+ if (/test_|_test\.py|conftest/.test(fp)) continue;
110
+ const lines = content.split('\n');
111
+ for (let i = 0; i < lines.length; i++) {
112
+ // print(f"Debug: ... or print("DEBUG or print(f"Error
113
+ if (/^\s*print\s*\(\s*f?['"`](?:DEBUG|Error|Warning|Info|debug|error|WARN)/i.test(lines[i])) {
114
+ const match = lines[i].match(/^(\s*)print\s*\(\s*(f?['"`])/);
115
+ if (match) {
116
+ const isError = /error/i.test(lines[i]);
117
+ const level = isError ? 'error' : 'debug';
118
+ findings.push({
119
+ ruleId: 'BUG-AIFIX-004', category: 'bugs', severity: 'medium',
120
+ title: `print() debug statement → logging.${level}()`,
121
+ file: fp, line: i + 1,
122
+ fix: {
123
+ type: 'replace',
124
+ old: `${match[1]}print(`,
125
+ new: `${match[1]}logging.${level}(`,
126
+ },
127
+ });
128
+ }
129
+ }
130
+ }
131
+ }
132
+ return findings;
133
+ },
134
+ },
135
+ {
136
+ id: 'BUG-AIFIX-005',
137
+ category: 'bugs',
138
+ severity: 'medium',
139
+ confidence: 'definite',
140
+ title: 'var declaration → const/let',
141
+ check({ files }) {
142
+ const findings = [];
143
+ for (const [fp, content] of files) {
144
+ if (!isJS(fp)) continue;
145
+ const lines = content.split('\n');
146
+ for (let i = 0; i < lines.length; i++) {
147
+ // var outside of for loop (for loops handled by BUG-FIX-003)
148
+ if (/^\s*var\s+(\w+)\s*=/.test(lines[i]) && !/^\s*for\s*\(/.test(lines[i])) {
149
+ const varName = lines[i].match(/var\s+(\w+)/)[1];
150
+ // Check if reassigned
151
+ const rest = lines.slice(i + 1).join('\n');
152
+ const reassignRe = new RegExp(`^\\s*${varName}\\s*=(?!=)`, 'm');
153
+ const isConst = !reassignRe.test(rest);
154
+ findings.push({
155
+ ruleId: 'BUG-AIFIX-005', category: 'bugs', severity: 'medium',
156
+ title: `var ${varName} → ${isConst ? 'const' : 'let'} ${varName}`,
157
+ file: fp, line: i + 1,
158
+ fix: {
159
+ type: 'replace',
160
+ old: `var ${varName}`,
161
+ new: `${isConst ? 'const' : 'let'} ${varName}`,
162
+ },
163
+ });
164
+ }
165
+ }
166
+ }
167
+ return findings;
168
+ },
169
+ },
170
+ ];
171
+
172
+ export default rules;