@sister.software/oxlint-config 9.3.0 → 11.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 (78) hide show
  1. package/README.md +198 -10
  2. package/out/browser-globals.d.ts +15 -0
  3. package/out/browser-globals.d.ts.map +1 -0
  4. package/out/browser-globals.js +76 -0
  5. package/out/browser-globals.js.map +1 -0
  6. package/out/console-padding-plugin.d.ts +16 -0
  7. package/out/console-padding-plugin.d.ts.map +1 -0
  8. package/out/console-padding-plugin.js +68 -0
  9. package/out/console-padding-plugin.js.map +1 -0
  10. package/out/constant-doc-plugin.d.ts +32 -0
  11. package/out/constant-doc-plugin.d.ts.map +1 -0
  12. package/out/constant-doc-plugin.js +91 -0
  13. package/out/constant-doc-plugin.js.map +1 -0
  14. package/out/headers-plugin.d.ts +6 -2
  15. package/out/headers-plugin.d.ts.map +1 -1
  16. package/out/headers-plugin.js +4 -2
  17. package/out/headers-plugin.js.map +1 -1
  18. package/out/index.d.ts +166 -13
  19. package/out/index.d.ts.map +1 -1
  20. package/out/index.js +332 -6
  21. package/out/index.js.map +1 -1
  22. package/out/jsdoc-plugin.d.ts +16 -0
  23. package/out/jsdoc-plugin.d.ts.map +1 -0
  24. package/out/jsdoc-plugin.js +68 -0
  25. package/out/jsdoc-plugin.js.map +1 -0
  26. package/out/length-truthiness-plugin.d.ts +18 -0
  27. package/out/length-truthiness-plugin.d.ts.map +1 -0
  28. package/out/length-truthiness-plugin.js +123 -0
  29. package/out/length-truthiness-plugin.js.map +1 -0
  30. package/out/multiline-statement-plugin.d.ts +18 -0
  31. package/out/multiline-statement-plugin.d.ts.map +1 -0
  32. package/out/multiline-statement-plugin.js +93 -0
  33. package/out/multiline-statement-plugin.js.map +1 -0
  34. package/out/padding-plugin.d.ts +4 -3
  35. package/out/padding-plugin.d.ts.map +1 -1
  36. package/out/padding-plugin.js +44 -27
  37. package/out/padding-plugin.js.map +1 -1
  38. package/out/padding-utils.d.ts +34 -0
  39. package/out/padding-utils.d.ts.map +1 -0
  40. package/out/padding-utils.js +50 -0
  41. package/out/padding-utils.js.map +1 -0
  42. package/out/plugin-types.d.ts +68 -3
  43. package/out/plugin-types.d.ts.map +1 -1
  44. package/out/plugin.d.ts.map +1 -1
  45. package/out/plugin.js +17 -0
  46. package/out/plugin.js.map +1 -1
  47. package/out/process-globals-plugin.d.ts.map +1 -1
  48. package/out/process-globals-plugin.js +6 -2
  49. package/out/process-globals-plugin.js.map +1 -1
  50. package/out/restrictions.d.ts +9 -3
  51. package/out/restrictions.d.ts.map +1 -1
  52. package/out/restrictions.js +4 -70
  53. package/out/restrictions.js.map +1 -1
  54. package/out/section-marker-plugin.d.ts +25 -0
  55. package/out/section-marker-plugin.d.ts.map +1 -0
  56. package/out/section-marker-plugin.js +234 -0
  57. package/out/section-marker-plugin.js.map +1 -0
  58. package/out/threshold-plugin.d.ts +27 -0
  59. package/out/threshold-plugin.d.ts.map +1 -0
  60. package/out/threshold-plugin.js +78 -0
  61. package/out/threshold-plugin.js.map +1 -0
  62. package/package.json +2 -2
  63. package/src/browser-globals.ts +77 -0
  64. package/src/console-padding-plugin.ts +76 -0
  65. package/src/constant-doc-plugin.ts +124 -0
  66. package/src/headers-plugin.ts +7 -3
  67. package/src/index.ts +490 -17
  68. package/src/jsdoc-plugin.ts +75 -0
  69. package/src/length-truthiness-plugin.ts +142 -0
  70. package/src/multiline-statement-plugin.ts +104 -0
  71. package/src/padding-plugin.ts +44 -29
  72. package/src/padding-utils.ts +70 -0
  73. package/src/plugin-types.ts +68 -3
  74. package/src/plugin.ts +22 -0
  75. package/src/process-globals-plugin.ts +6 -2
  76. package/src/restrictions.ts +16 -77
  77. package/src/section-marker-plugin.ts +306 -0
  78. package/src/threshold-plugin.ts +105 -0
@@ -7,6 +7,8 @@
7
7
 
8
8
  import { builtinModules } from "node:module"
9
9
 
10
+ import { restrictedBrowserGlobalsRule } from "./browser-globals.js"
11
+
10
12
  //#region Runtime data
11
13
 
12
14
  /**
@@ -24,7 +26,9 @@ export const RuntimePackageNamesRecord = {
24
26
 
25
27
  export type RuntimePackageNamesRecord = typeof RuntimePackageNamesRecord
26
28
 
27
- /** Valid runtime names. */
29
+ /**
30
+ * Valid runtime names.
31
+ */
28
32
  export type RuntimeName = keyof RuntimePackageNamesRecord
29
33
 
30
34
  /**
@@ -38,7 +42,9 @@ export function createPackageFileMatcher(packageName: string): string[] {
38
42
  return [`**/${packageName}.{js,mjs,cjs,ts,d.ts,mts,tsx}`]
39
43
  }
40
44
 
41
- /** Joins a namespace and package name into a specifier, e.g. `@sister.software/client`. */
45
+ /**
46
+ * Joins a namespace and package name into a specifier, e.g. `@sister.software/client`.
47
+ */
42
48
  function namespaced(packageNamespace: string, packageName: string): string {
43
49
  return [packageNamespace, packageName].filter(Boolean).join("/")
44
50
  }
@@ -53,7 +59,9 @@ const NODE_BUILTINS_NO_PREFIX = builtinModules.filter(
53
59
 
54
60
  const NODE_BUILTINS_PREFIXED = NODE_BUILTINS_NO_PREFIX.map((moduleName) => `node:${moduleName}`)
55
61
 
56
- /** A restricted-import entry: an exact module name plus the message shown when it is imported. */
62
+ /**
63
+ * A restricted-import entry: an exact module name plus the message shown when it is imported.
64
+ */
57
65
  export interface RestrictedPath {
58
66
  name: string
59
67
  message: string
@@ -80,88 +88,19 @@ export function allNodeBuiltinPaths(message: string): RestrictedPath[] {
80
88
 
81
89
  //#endregion
82
90
 
83
- //#region Browser globals
91
+ //#region Override generation
84
92
 
85
93
  /**
86
- * Browser globals whose bare use is ambiguous (they collide with common identifiers). Browser- and node-runtime files
87
- * warn on these, nudging toward an explicit `window.` access.
94
+ * A per-file-glob oxlint override: applies `rules` only to files matching `files`.
88
95
  */
89
- const BROWSER_GLOBALS = [
90
- "addEventListener",
91
- "blur",
92
- "close",
93
- "closed",
94
- "confirm",
95
- "defaultStatus",
96
- "defaultstatus",
97
- "event",
98
- "external",
99
- "find",
100
- "focus",
101
- "frameElement",
102
- "frames",
103
- "history",
104
- "innerHeight",
105
- "innerWidth",
106
- "length",
107
- "location",
108
- "locationbar",
109
- "menubar",
110
- "moveBy",
111
- "moveTo",
112
- "name",
113
- "onblur",
114
- "onerror",
115
- "onfocus",
116
- "onload",
117
- "onresize",
118
- "onunload",
119
- "open",
120
- "opener",
121
- "opera",
122
- "outerHeight",
123
- "outerWidth",
124
- "pageXOffset",
125
- "pageYOffset",
126
- "parent",
127
- "print",
128
- "removeEventListener",
129
- "resizeBy",
130
- "resizeTo",
131
- "screen",
132
- "screenLeft",
133
- "screenTop",
134
- "screenX",
135
- "screenY",
136
- "scroll",
137
- "scrollbars",
138
- "scrollBy",
139
- "scrollTo",
140
- "scrollX",
141
- "scrollY",
142
- "self",
143
- "status",
144
- "statusbar",
145
- "stop",
146
- "toolbar",
147
- "top",
148
- ] as const
149
-
150
- function restrictedBrowserGlobalsRule(): unknown {
151
- return ["warn", ...BROWSER_GLOBALS.map((name) => ({ name, message: `Ambiguous: did you mean \`window.${name}\`?` }))]
152
- }
153
-
154
- //#endregion
155
-
156
- //#region Override generation
157
-
158
- /** A per-file-glob oxlint override: applies `rules` only to files matching `files`. */
159
96
  export interface OxlintOverride {
160
97
  files: string[]
161
98
  rules: Record<string, unknown>
162
99
  }
163
100
 
164
- /** A restricted-import pattern: a set of gitignore-style globs plus a message. */
101
+ /**
102
+ * A restricted-import pattern: a set of gitignore-style globs plus a message.
103
+ */
165
104
  interface RestrictedPattern {
166
105
  group: string[]
167
106
  message: string
@@ -0,0 +1,306 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ * @file Four rules over the comments people use to carve a file into sections. They form one
6
+ * escalation ladder, and each rung is a smaller file than the last:
7
+ *
8
+ * 1. `prefer-mark-comment` — a `----` banner becomes `// MARK: <label>` (autofixed). A banner is
9
+ * three lines of decoration to carry one label.
10
+ * 2. `concise-section-marker` — a marker's label is a label, not a paragraph. Past the limit, the
11
+ * detail belongs in the documented symbol's JSDoc or in the file's own `@file` block.
12
+ * 3. `prefer-region-over-marks` — past one marker, a file has sections, and sections have ENDS.
13
+ * Regions fold, so a reader can collapse what they are not reading.
14
+ * 4. `max-regions` — past ten regions, the sections want to be files.
15
+ *
16
+ * Rules 3 and 4 are advisory: where a section ends, and whether it should move, are judgment calls
17
+ * no fixer can make. Rules 1 and 2 are not — the first has one correct rewrite, the second has a
18
+ * measurable limit.
19
+ */
20
+
21
+ import type { Comment, Rule, RuleContext, SourceCode } from "./plugin-types.js"
22
+
23
+ /**
24
+ * A run of at least this many dashes reads as decoration rather than as prose.
25
+ */
26
+ const MIN_BANNER_DASHES = 10
27
+
28
+ /**
29
+ * Default ceiling for a marker's label, in characters.
30
+ */
31
+ const DEFAULT_MAX_BODY_LENGTH = 60
32
+
33
+ /**
34
+ * Default region ceiling. The rule fires past it, so the eleventh region is the one that trips it.
35
+ *
36
+ * Deliberately high. A file with four well-named regions is usually fine — data plus the operations on it, where
37
+ * splitting would separate a type from its helpers. This is a runaway detector, not a style nudge: past ten sections a
38
+ * file is doing too many jobs by any reading.
39
+ */
40
+ const DEFAULT_MAX_REGIONS = 10
41
+
42
+ /**
43
+ * A line that is nothing but a comment marker and a rule of dashes.
44
+ */
45
+ const BANNER_SEPARATOR = new RegExp(String.raw`^[ \t]*//[ \t]*-{${MIN_BANNER_DASHES},}[ \t]*$`)
46
+
47
+ /**
48
+ * `// MARK: label`, with the leading dash Xcode allows (`// MARK: - label`) folded away.
49
+ */
50
+ const MARK_COMMENT = /^[ \t]*\/\/[ \t]*MARK:[ \t]*-?[ \t]*(.*)$/
51
+
52
+ /**
53
+ * `//#region label` — the space after `//` is optional, which is how most editors emit it.
54
+ */
55
+ const REGION_COMMENT = /^[ \t]*\/\/[ \t]*#region\b[ \t]*(.*)$/
56
+
57
+ /**
58
+ * One `----`-wrapped banner found in the source.
59
+ */
60
+ interface Banner {
61
+ /**
62
+ * Zero-based index of the opening separator line.
63
+ */
64
+ startLine: number
65
+ /**
66
+ * Zero-based index of the closing separator line.
67
+ */
68
+ endLine: number
69
+ /**
70
+ * The comment lines between the separators, stripped of their `//` and surrounding space.
71
+ */
72
+ body: string[]
73
+ /**
74
+ * Leading whitespace of the opening separator, so the replacement keeps the block's indentation.
75
+ */
76
+ indent: string
77
+ }
78
+
79
+ /**
80
+ * Strip a line comment's `//` and the space that conventionally follows it.
81
+ */
82
+ function commentBody(line: string): string {
83
+ return line
84
+ .trim()
85
+ .replace(/^\/\/[ \t]?/, "")
86
+ .trim()
87
+ }
88
+
89
+ /**
90
+ * Scan for banner blocks: a separator line, one or more comment lines, then another separator. Blocks are matched on
91
+ * raw lines rather than on the comment list because a banner is a visual construct — what makes it one is that the
92
+ * lines are adjacent, which the AST does not model.
93
+ */
94
+ function findBanners(lines: string[]): Banner[] {
95
+ const banners: Banner[] = []
96
+ let i = 0
97
+
98
+ while (i < lines.length) {
99
+ if (!BANNER_SEPARATOR.test(lines[i]!)) {
100
+ i++
101
+
102
+ continue
103
+ }
104
+
105
+ let end = i + 1
106
+ const body: string[] = []
107
+
108
+ while (end < lines.length && lines[end]!.trim().startsWith("//") && !BANNER_SEPARATOR.test(lines[end]!)) {
109
+ body.push(commentBody(lines[end]!))
110
+
111
+ end++
112
+ }
113
+
114
+ if (body.length && end < lines.length && BANNER_SEPARATOR.test(lines[end]!)) {
115
+ banners.push({ startLine: i, endLine: end, body, indent: /^[ \t]*/.exec(lines[i]!)![0] })
116
+ i = end + 1
117
+
118
+ continue
119
+ }
120
+
121
+ i = end
122
+ }
123
+
124
+ return banners
125
+ }
126
+
127
+ /**
128
+ * Byte offset of the first character of a zero-based line.
129
+ */
130
+ function offsetOfLine(lines: string[], line: number): number {
131
+ let offset = 0
132
+
133
+ for (let i = 0; i < line; i++) {
134
+ offset += lines[i]!.length + 1
135
+ }
136
+
137
+ return offset
138
+ }
139
+
140
+ /**
141
+ * Every line comment in the file, paired with its label under `pattern`.
142
+ */
143
+ function markersMatching(
144
+ sourceCode: SourceCode,
145
+ text: string,
146
+ pattern: RegExp
147
+ ): Array<{ comment: Comment; label: string }> {
148
+ const found: Array<{ comment: Comment; label: string }> = []
149
+
150
+ for (const comment of sourceCode.getAllComments()) {
151
+ if (comment.type !== "Line") continue
152
+ const match = pattern.exec(text.slice(comment.range[0], comment.range[1]))
153
+
154
+ if (match) {
155
+ found.push({ comment, label: match[1]!.trim() })
156
+ }
157
+ }
158
+
159
+ return found
160
+ }
161
+
162
+ /**
163
+ * Shared preamble: the source text and its lines, which every rule here walks.
164
+ */
165
+ function read(context: RuleContext): { sourceCode: SourceCode; text: string; lines: string[] } {
166
+ const sourceCode = context.sourceCode ?? context.getSourceCode!()
167
+ const text = sourceCode.getText()
168
+
169
+ return { sourceCode, text, lines: text.split("\n") }
170
+ }
171
+
172
+ export const preferMarkCommentRule: Rule = {
173
+ meta: {
174
+ name: "prefer-mark-comment",
175
+ type: "suggestion",
176
+ fixable: "code",
177
+ schema: [{ type: "object", additionalProperties: true }],
178
+ },
179
+ create(context) {
180
+ const { text, lines } = read(context)
181
+
182
+ return {
183
+ Program() {
184
+ for (const banner of findBanners(lines)) {
185
+ const start = offsetOfLine(lines, banner.startLine)
186
+ const end = offsetOfLine(lines, banner.endLine) + lines[banner.endLine]!.length
187
+ const node = { type: "Line", range: [start, end] as [number, number] }
188
+
189
+ // A banner wrapping real prose is a comment someone wrote, not decoration around a
190
+ // label. Collapsing it would delete the prose, so say what to do and leave it alone.
191
+ if (banner.body.length > 1) {
192
+ context.report({
193
+ node,
194
+ message:
195
+ "Banner comment: keep the prose in a JSDoc block on what it describes, and leave a `// MARK: <label>` here.",
196
+ })
197
+
198
+ continue
199
+ }
200
+
201
+ const label = banner.body[0]!
202
+
203
+ context.report({
204
+ node,
205
+ message: "Banner comment: use `// MARK: <label>`.",
206
+ fix(fixer) {
207
+ // The marker wants a blank line on each side. Add one only where the source is
208
+ // missing it, so a correctly-spaced banner does not gain stray lines.
209
+ const before = text.slice(0, start).endsWith("\n\n") || start === 0 ? "" : "\n"
210
+ const after = text.slice(end).startsWith("\n\n") ? "" : "\n"
211
+
212
+ return fixer.replaceTextRange([start, end], `${before}${banner.indent}// MARK: ${label}${after}`)
213
+ },
214
+ })
215
+ }
216
+ },
217
+ }
218
+ },
219
+ }
220
+
221
+ export const conciseSectionMarkerRule: Rule = {
222
+ meta: {
223
+ name: "concise-section-marker",
224
+ type: "suggestion",
225
+ schema: [{ type: "object", additionalProperties: true }],
226
+ },
227
+ create(context) {
228
+ const { sourceCode, text } = read(context)
229
+ const options = (context.options[0] ?? {}) as { maxBodyLength?: number }
230
+ const limit = options.maxBodyLength ?? DEFAULT_MAX_BODY_LENGTH
231
+
232
+ return {
233
+ Program() {
234
+ const markers = [
235
+ ...markersMatching(sourceCode, text, MARK_COMMENT).map((m) => ({ ...m, kind: "MARK" })),
236
+ ...markersMatching(sourceCode, text, REGION_COMMENT).map((m) => ({ ...m, kind: "#region" })),
237
+ ]
238
+
239
+ for (const { comment, label, kind } of markers) {
240
+ if (label.length <= limit) continue
241
+
242
+ context.report({
243
+ node: { type: "Line", range: comment.range },
244
+ message:
245
+ `${kind} label is ${label.length} characters (max ${limit}) — shorten it, move the detail into ` +
246
+ "the JSDoc of what it describes, or split the file and put it in a `@file` block.",
247
+ })
248
+ }
249
+ },
250
+ }
251
+ },
252
+ }
253
+
254
+ export const preferRegionOverMarksRule: Rule = {
255
+ meta: {
256
+ name: "prefer-region-over-marks",
257
+ type: "suggestion",
258
+ schema: [{ type: "object", additionalProperties: true }],
259
+ },
260
+ create(context) {
261
+ const { sourceCode, text } = read(context)
262
+
263
+ return {
264
+ Program() {
265
+ const marks = markersMatching(sourceCode, text, MARK_COMMENT)
266
+
267
+ if (marks.length < 2) return
268
+
269
+ context.report({
270
+ node: { type: "Line", range: marks[0]!.comment.range },
271
+ message:
272
+ `${marks.length} MARK comments in one file — give each section an END with ` +
273
+ "`//#region <label>` / `//#endregion`, so a reader can fold what they are not reading.",
274
+ })
275
+ },
276
+ }
277
+ },
278
+ }
279
+
280
+ export const maxRegionsRule: Rule = {
281
+ meta: {
282
+ name: "max-regions",
283
+ type: "suggestion",
284
+ schema: [{ type: "object", additionalProperties: true }],
285
+ },
286
+ create(context) {
287
+ const { sourceCode, text } = read(context)
288
+ const options = (context.options[0] ?? {}) as { max?: number }
289
+ const limit = options.max ?? DEFAULT_MAX_REGIONS
290
+
291
+ return {
292
+ Program() {
293
+ const regions = markersMatching(sourceCode, text, REGION_COMMENT)
294
+
295
+ if (regions.length <= limit) return
296
+
297
+ context.report({
298
+ node: { type: "Line", range: regions[0]!.comment.range },
299
+ message:
300
+ `${regions.length} regions in one file (max ${limit}) — a file with this many sections is ` +
301
+ "several files. Split it, and let each one's `@file` block say what it is.",
302
+ })
303
+ },
304
+ }
305
+ },
306
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ * @file A no-unnamed-threshold rule, authored as an oxlint JS plugin (ESLint v9-compatible API). It
6
+ * flags a numeric literal used as a threshold — an operand of a comparison — and nothing else.
7
+ * Numbers inside array and object literals are left alone, because a bounding box or a codepoint
8
+ * table is data rather than a tuning knob. That distinction is why this exists instead of
9
+ * `no-magic-numbers`, which cannot express it.
10
+ */
11
+
12
+ import type { AstNode, Rule } from "./plugin-types.js"
13
+
14
+ /**
15
+ * Operators whose operands read as thresholds.
16
+ */
17
+ const COMPARISON_OPERATORS = new Set(["<", ">", "<=", ">=", "===", "!==", "==", "!="])
18
+
19
+ /**
20
+ * Numbers so conventional that naming them costs more clarity than it buys.
21
+ */
22
+ const DEFAULT_IGNORE = [-1, 0, 1, 2, 0.5, 10, 100, 1000]
23
+
24
+ /**
25
+ * Radix-prefixed literals: `0x1f`, `0b1010`, `0o777`.
26
+ */
27
+ const RADIX_PREFIXED = /^0[xXbBoO]/
28
+
29
+ /**
30
+ * Options accepted by {@link noUnnamedThresholdRule}.
31
+ */
32
+ export interface ThresholdOptions {
33
+ /**
34
+ * Values that may appear unnamed. Replaces the default list rather than extending it.
35
+ */
36
+ ignore?: number[]
37
+ /**
38
+ * Exempt radix-prefixed literals, which already read as codepoints or bit masks.
39
+ */
40
+ allowHex?: boolean
41
+ }
42
+
43
+ export const noUnnamedThresholdRule: Rule = {
44
+ meta: {
45
+ name: "no-unnamed-threshold",
46
+ type: "suggestion",
47
+ schema: [{ type: "object", additionalProperties: true }],
48
+ },
49
+ create(context) {
50
+ const options = (context.options[0] ?? {}) as ThresholdOptions
51
+ const ignore = new Set(options.ignore ?? DEFAULT_IGNORE)
52
+ const allowHex = options.allowHex ?? true
53
+
54
+ function check(operand: AstNode | undefined) {
55
+ if (!operand) return
56
+
57
+ let literal = operand
58
+ let sign = 1
59
+
60
+ // `x < -273.15` parses as a unary minus wrapping the literal.
61
+ if (operand.type === "UnaryExpression") {
62
+ if (operand.operator !== "-" && operand.operator !== "+") return
63
+
64
+ if (!operand.argument) return
65
+
66
+ sign = operand.operator === "-" ? -1 : 1
67
+ literal = operand.argument
68
+ }
69
+
70
+ // oxlint's AST uses ESTree `Literal` in some positions and Babel-style `NumericLiteral` in
71
+ // others, so both are accepted.
72
+ if (literal.type !== "Literal" && literal.type !== "NumericLiteral") return
73
+
74
+ if (typeof literal.value !== "number") return
75
+
76
+ const raw = literal.raw ?? String(literal.value)
77
+
78
+ // `cp >= 0x3040` already reads as a codepoint boundary; a name adds nothing.
79
+ if (allowHex && RADIX_PREFIXED.test(raw)) return
80
+
81
+ if (ignore.has(sign * literal.value)) return
82
+
83
+ // `raw` is the literal's own text, so a negated value reads as `273.15` without this.
84
+ const shown = sign === -1 ? `-${raw}` : raw
85
+
86
+ context.report({
87
+ node: literal,
88
+ message:
89
+ `Unnamed threshold \`${shown}\` — extract it to a documented named constant so a reader can tell ` +
90
+ `what it means and where the value came from.`,
91
+ })
92
+ }
93
+
94
+ return {
95
+ BinaryExpression(node) {
96
+ if (!node.operator || !COMPARISON_OPERATORS.has(node.operator)) return
97
+
98
+ check(node.left)
99
+ check(node.right)
100
+ },
101
+ }
102
+ },
103
+ }
104
+
105
+ export default noUnnamedThresholdRule