@sister.software/oxlint-config 10.0.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 (77) hide show
  1. package/README.md +96 -0
  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 +9 -3
  11. package/out/constant-doc-plugin.d.ts.map +1 -1
  12. package/out/constant-doc-plugin.js +6 -2
  13. package/out/constant-doc-plugin.js.map +1 -1
  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 +3 -1
  17. package/out/headers-plugin.js.map +1 -1
  18. package/out/index.d.ts +111 -27
  19. package/out/index.d.ts.map +1 -1
  20. package/out/index.js +57 -5
  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.map +1 -1
  27. package/out/length-truthiness-plugin.js +15 -5
  28. package/out/length-truthiness-plugin.js.map +1 -1
  29. package/out/multiline-statement-plugin.d.ts +18 -0
  30. package/out/multiline-statement-plugin.d.ts.map +1 -0
  31. package/out/multiline-statement-plugin.js +93 -0
  32. package/out/multiline-statement-plugin.js.map +1 -0
  33. package/out/padding-plugin.d.ts +4 -3
  34. package/out/padding-plugin.d.ts.map +1 -1
  35. package/out/padding-plugin.js +44 -27
  36. package/out/padding-plugin.js.map +1 -1
  37. package/out/padding-utils.d.ts +34 -0
  38. package/out/padding-utils.d.ts.map +1 -0
  39. package/out/padding-utils.js +50 -0
  40. package/out/padding-utils.js.map +1 -0
  41. package/out/plugin-types.d.ts +55 -13
  42. package/out/plugin-types.d.ts.map +1 -1
  43. package/out/plugin.d.ts.map +1 -1
  44. package/out/plugin.js +11 -0
  45. package/out/plugin.js.map +1 -1
  46. package/out/process-globals-plugin.d.ts.map +1 -1
  47. package/out/process-globals-plugin.js +6 -2
  48. package/out/process-globals-plugin.js.map +1 -1
  49. package/out/restrictions.d.ts +9 -3
  50. package/out/restrictions.d.ts.map +1 -1
  51. package/out/restrictions.js +4 -70
  52. package/out/restrictions.js.map +1 -1
  53. package/out/section-marker-plugin.d.ts +25 -0
  54. package/out/section-marker-plugin.d.ts.map +1 -0
  55. package/out/section-marker-plugin.js +234 -0
  56. package/out/section-marker-plugin.js.map +1 -0
  57. package/out/threshold-plugin.d.ts +9 -3
  58. package/out/threshold-plugin.d.ts.map +1 -1
  59. package/out/threshold-plugin.js +9 -3
  60. package/out/threshold-plugin.js.map +1 -1
  61. package/package.json +1 -1
  62. package/src/browser-globals.ts +77 -0
  63. package/src/console-padding-plugin.ts +76 -0
  64. package/src/constant-doc-plugin.ts +15 -5
  65. package/src/headers-plugin.ts +6 -2
  66. package/src/index.ts +166 -29
  67. package/src/jsdoc-plugin.ts +75 -0
  68. package/src/length-truthiness-plugin.ts +15 -5
  69. package/src/multiline-statement-plugin.ts +104 -0
  70. package/src/padding-plugin.ts +44 -29
  71. package/src/padding-utils.ts +70 -0
  72. package/src/plugin-types.ts +55 -13
  73. package/src/plugin.ts +16 -0
  74. package/src/process-globals-plugin.ts +6 -2
  75. package/src/restrictions.ts +16 -77
  76. package/src/section-marker-plugin.ts +306 -0
  77. package/src/threshold-plugin.ts +18 -6
package/src/index.ts CHANGED
@@ -9,26 +9,46 @@ import { createRuntimeOverrides } from "./restrictions.js"
9
9
 
10
10
  export * from "./restrictions.js"
11
11
 
12
- /** An oxlint configuration object, as consumed by `oxlint.config.ts` / `.oxlintrc.json`. */
12
+ /**
13
+ * An oxlint configuration object, as consumed by `oxlint.config.ts` / `.oxlintrc.json`.
14
+ */
13
15
  export type OxlintConfig = Record<string, unknown>
14
16
 
15
- /** Numeric ceilings for the legibility-guardrail rules. Each is a hard ceiling, not a target. */
17
+ /**
18
+ * Numeric ceilings for the legibility-guardrail rules. Each is a hard ceiling, not a target.
19
+ */
16
20
  export interface OxlintConfigLimits {
17
- /** Maximum block nesting depth. */
21
+ /**
22
+ * Maximum block nesting depth.
23
+ */
18
24
  maxDepth: number
19
- /** Maximum parameters on a single function. */
25
+ /**
26
+ * Maximum parameters on a single function.
27
+ */
20
28
  maxParams: number
21
- /** Maximum statements in a single function body. */
29
+ /**
30
+ * Maximum statements in a single function body.
31
+ */
22
32
  maxStatements: number
23
- /** Maximum lines in a single function body, blank lines and comments excluded. */
33
+ /**
34
+ * Maximum lines in a single function body, blank lines and comments excluded.
35
+ */
24
36
  maxLinesPerFunction: number
25
- /** Maximum lines in a single file, blank lines and comments excluded. */
37
+ /**
38
+ * Maximum lines in a single file, blank lines and comments excluded.
39
+ */
26
40
  maxLines: number
27
- /** Maximum nested callback depth. */
41
+ /**
42
+ * Maximum nested callback depth.
43
+ */
28
44
  maxNestedCallbacks: number
29
- /** Maximum nested call-expression depth, e.g. `a(b(c(d())))`. */
45
+ /**
46
+ * Maximum nested call-expression depth, e.g. `a(b(c(d())))`.
47
+ */
30
48
  maxNestedCalls: number
31
- /** Maximum cyclomatic complexity of a single function. */
49
+ /**
50
+ * Maximum cyclomatic complexity of a single function.
51
+ */
32
52
  complexity: number
33
53
  }
34
54
 
@@ -61,7 +81,9 @@ export const DefaultLimits: OxlintConfigLimits = {
61
81
  complexity: 85,
62
82
  }
63
83
 
64
- /** Globs treated as test files, where the size and named-constant rules are switched off. */
84
+ /**
85
+ * Globs treated as test files, where the size and named-constant rules are switched off.
86
+ */
65
87
  export const DefaultTestFilePatterns = [
66
88
  "**/*.test.ts",
67
89
  "**/*.test.tsx",
@@ -82,23 +104,56 @@ export const DefaultTestFilePatterns = [
82
104
  */
83
105
  export const DefaultGeneratedFilePatterns = ["**/*.gen.ts", "**/*.gen.tsx", "**/*.generated.ts", "**/generated/**"]
84
106
 
85
- /** Options for {@link createOxlintConfig}. */
107
+ /**
108
+ * Globs for files JavaScript checks at runtime rather than TypeScript checking ahead of it. A JSDoc block there often
109
+ * IS the type annotation, and a one-line `@type` cast is the idiomatic form — so the multi-line requirement, which
110
+ * exists to make prose read as documentation, does not apply.
111
+ */
112
+ export const DefaultUntypedFilePatterns = ["**/*.js", "**/*.mjs", "**/*.cjs", "**/*.jsx"]
113
+
114
+ /**
115
+ * Options for {@link createOxlintConfig}.
116
+ */
86
117
  export interface OxlintConfigOptions {
87
- /** The package namespace whose runtime boundaries are enforced, e.g. `@sister.software`. */
118
+ /**
119
+ * The package namespace whose runtime boundaries are enforced, e.g. `@sister.software`.
120
+ */
88
121
  packageNamespace?: string
89
- /** The copyright holder stamped into file headers. */
122
+ /**
123
+ * The copyright holder stamped into file headers.
124
+ */
90
125
  copyrightHolder?: string
91
- /** The SPDX license identifier stamped into file headers. */
126
+ /**
127
+ * The SPDX license identifier stamped into file headers.
128
+ */
92
129
  spdxLicenseIdentifier?: string
93
- /** The author stamped into file headers. */
130
+ /**
131
+ * The author stamped into file headers.
132
+ */
94
133
  author?: string
95
- /** Enable oxlint's React plugin (off by default). */
134
+ /**
135
+ * Enable oxlint's React plugin (off by default).
136
+ */
96
137
  react?: boolean
97
- /** Enforce file headers via the bundled JS plugin (on by default). */
138
+ /**
139
+ * Enforce file headers via the bundled JS plugin (on by default).
140
+ */
98
141
  headers?: boolean
99
- /** Require a blank line before `return`/block-like statements (on by default). */
142
+ /**
143
+ * Require a blank line before `return`/block-like statements (on by default).
144
+ */
100
145
  padding?: boolean
101
- /** Require braces around single-statement work bodies (bare `return` exempt; on by default). */
146
+ /**
147
+ * Blank line on each side of a `console.*` call, with runs of them grouped (on by default).
148
+ */
149
+ consolePadding?: boolean
150
+ /**
151
+ * Blank line on each side of a statement that spans lines (on by default).
152
+ */
153
+ multilineStatementPadding?: boolean
154
+ /**
155
+ * Require braces around single-statement work bodies (bare `return` exempt; on by default).
156
+ */
102
157
  braces?: boolean
103
158
  /**
104
159
  * Forbid direct `process.env` / `process.argv` access (off by default). Turn on once the project funnels those reads
@@ -115,21 +170,48 @@ export interface OxlintConfigOptions {
115
170
  * `"screaming"`, or the default `"exported-or-screaming"`.
116
171
  */
117
172
  constantDocs?: boolean | { scope?: "exported" | "screaming" | "exported-or-screaming" }
118
- /** Rewrite explicit length comparisons to truthiness in boolean positions (on by default). */
173
+ /**
174
+ * Rewrite explicit length comparisons to truthiness in boolean positions (on by default).
175
+ */
119
176
  lengthTruthiness?: boolean
120
- /** Override individual legibility ceilings. Unspecified keys keep their calibrated default. */
177
+ /**
178
+ * Require JSDoc blocks to span multiple lines (on by default). Off for untyped files.
179
+ */
180
+ multilineJSDoc?: boolean
181
+ /**
182
+ * Enforce the section-marker ladder — `----` banners become `// MARK:`, long labels are flagged, and a file with many
183
+ * markers is nudged toward regions and then toward being several files (on by default).
184
+ */
185
+ sectionMarkers?: boolean | { maxBodyLength?: number; maxRegions?: number }
186
+ /**
187
+ * Override individual legibility ceilings. Unspecified keys keep their calibrated default.
188
+ */
121
189
  limits?: Partial<OxlintConfigLimits>
122
- /** Replace the globs treated as test files. */
190
+ /**
191
+ * Replace the globs treated as test files.
192
+ */
123
193
  testFilePatterns?: string[]
124
- /** Replace the globs treated as generated files, where only the size ceilings are switched off. */
194
+ /**
195
+ * Replace the globs treated as generated files, where only the size ceilings are switched off.
196
+ */
125
197
  generatedFilePatterns?: string[]
126
- /** Override the default ignore patterns. */
198
+ /**
199
+ * Replace the globs treated as untyped, where the multi-line JSDoc requirement switches off.
200
+ */
201
+ untypedFilePatterns?: string[]
202
+ /**
203
+ * Override the default ignore patterns.
204
+ */
127
205
  ignorePatterns?: string[]
128
- /** Extra config deep-merged last; an escape hatch for per-repo tweaks. */
206
+ /**
207
+ * Extra config deep-merged last; an escape hatch for per-repo tweaks.
208
+ */
129
209
  overrides?: OxlintConfig
130
210
  }
131
211
 
132
- /** Default ignore patterns for generated/build output and vendored tooling. */
212
+ /**
213
+ * Default ignore patterns for generated/build output and vendored tooling.
214
+ */
133
215
  export const DefaultIgnorePatterns = [
134
216
  "**/out",
135
217
  "**/dist",
@@ -165,14 +247,19 @@ export function createOxlintConfig(options: OxlintConfigOptions = {}): OxlintCon
165
247
  react = false,
166
248
  headers = true,
167
249
  padding = true,
250
+ consolePadding = true,
251
+ multilineStatementPadding = true,
168
252
  braces = true,
169
253
  restrictProcessGlobals = false,
170
254
  unnamedThresholds = false,
171
255
  constantDocs = false,
172
256
  lengthTruthiness = true,
257
+ multilineJSDoc = true,
258
+ sectionMarkers = true,
173
259
  limits: limitOverrides = {},
174
260
  testFilePatterns = DefaultTestFilePatterns,
175
261
  generatedFilePatterns = DefaultGeneratedFilePatterns,
262
+ untypedFilePatterns = DefaultUntypedFilePatterns,
176
263
  ignorePatterns = DefaultIgnorePatterns,
177
264
  overrides = {},
178
265
  } = options
@@ -436,6 +523,38 @@ export function createOxlintConfig(options: OxlintConfigOptions = {}): OxlintCon
436
523
  rules["sister-software/prefer-length-truthiness"] = "error"
437
524
  }
438
525
 
526
+ if (consolePadding) {
527
+ rules["sister-software/console-padding"] = "warn"
528
+ }
529
+
530
+ if (multilineStatementPadding) {
531
+ rules["sister-software/multiline-statement-padding"] = "warn"
532
+ }
533
+
534
+ if (multilineJSDoc) {
535
+ rules["sister-software/multiline-jsdoc"] = "error"
536
+ }
537
+
538
+ if (sectionMarkers) {
539
+ const markerOptions = typeof sectionMarkers === "object" ? sectionMarkers : {}
540
+ // The two mechanical rungs are errors: a banner has exactly one correct rewrite, and a label
541
+ // length is measurable. The two judgment rungs warn — where a section ends, and whether it
542
+ // should become its own file, are not decisions a linter gets to make.
543
+ rules["sister-software/prefer-mark-comment"] = "error"
544
+
545
+ rules["sister-software/concise-section-marker"] = [
546
+ "error",
547
+ markerOptions.maxBodyLength === undefined ? {} : { maxBodyLength: markerOptions.maxBodyLength },
548
+ ]
549
+
550
+ rules["sister-software/prefer-region-over-marks"] = "warn"
551
+
552
+ rules["sister-software/max-regions"] = [
553
+ "warn",
554
+ markerOptions.maxRegions === undefined ? {} : { max: markerOptions.maxRegions },
555
+ ]
556
+ }
557
+
439
558
  // Rules switched off inside test files. Table-driven test bodies are legitimately long, and
440
559
  // expected values are legitimately unnamed numbers. oxlint validates override entries against the
441
560
  // registered rule set, so an entry may only name a rule this config actually turned on.
@@ -460,7 +579,16 @@ export function createOxlintConfig(options: OxlintConfigOptions = {}): OxlintCon
460
579
  testFileRules["sister-software/require-constant-doc"] = "off"
461
580
  }
462
581
 
463
- /** Generated files: size ceilings only. Everything else still applies — generated code ships. */
582
+ if (sectionMarkers) {
583
+ // A test file's sections track the suite's shape, which the code under test dictates — a big
584
+ // table of cases legitimately wants many markers, and splitting it would scatter the suite.
585
+ testFileRules["sister-software/prefer-region-over-marks"] = "off"
586
+ testFileRules["sister-software/max-regions"] = "off"
587
+ }
588
+
589
+ /**
590
+ * Generated files: size ceilings only. Everything else still applies — generated code ships.
591
+ */
464
592
  const generatedFileRules: Record<string, unknown> = {
465
593
  "max-lines": "off",
466
594
  "max-lines-per-function": "off",
@@ -470,7 +598,15 @@ export function createOxlintConfig(options: OxlintConfigOptions = {}): OxlintCon
470
598
 
471
599
  return {
472
600
  plugins,
473
- ...(headers || padding || braces || restrictProcessGlobals || unnamedThresholds || constantDocs || lengthTruthiness
601
+ ...(headers ||
602
+ padding ||
603
+ braces ||
604
+ restrictProcessGlobals ||
605
+ unnamedThresholds ||
606
+ constantDocs ||
607
+ lengthTruthiness ||
608
+ multilineJSDoc ||
609
+ sectionMarkers
474
610
  ? { jsPlugins: ["@sister.software/oxlint-config/plugin"] }
475
611
  : {}),
476
612
  categories: { correctness: "error" },
@@ -480,6 +616,7 @@ export function createOxlintConfig(options: OxlintConfigOptions = {}): OxlintCon
480
616
  ...createRuntimeOverrides(packageNamespace),
481
617
  { files: testFilePatterns, rules: testFileRules },
482
618
  { files: generatedFilePatterns, rules: generatedFileRules },
619
+ ...(multilineJSDoc ? [{ files: untypedFilePatterns, rules: { "sister-software/multiline-jsdoc": "off" } }] : []),
483
620
  ],
484
621
  ...overrides,
485
622
  }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ * @file The `sister-software/multiline-jsdoc` rule: a JSDoc block always spans multiple lines, even
6
+ * when its content would fit on one. A one-line block reads as an aside; the multi-line form reads
7
+ * as documentation, and it leaves somewhere to put the second sentence when one is needed.
8
+ *
9
+ * Two shapes are left alone. A JSDoc that shares its line with code is a type cast or an inline
10
+ * annotation, and expanding those changes what the line means. A JSDoc on a union or intersection
11
+ * member labels one alternative in what reads as a list — three lines per entry turns a list you
12
+ * can scan into a page you have to read.
13
+ */
14
+
15
+ import type { Rule } from "./plugin-types.js"
16
+
17
+ /**
18
+ * The JSDoc opener, so the rule can tell a documentation block from a plain block comment.
19
+ */
20
+ const JSDOC_OPENER = "/**"
21
+
22
+ /**
23
+ * A `|` or `&` as the next thing after a comment, meaning the comment labels one member of a union or intersection
24
+ * rather than documenting a declaration.
25
+ */
26
+ const UNION_MEMBER = /^\s*[|&]/
27
+
28
+ export const multilineJSDocRule: Rule = {
29
+ meta: {
30
+ name: "multiline-jsdoc",
31
+ type: "layout",
32
+ fixable: "whitespace",
33
+ schema: [{ type: "object", additionalProperties: true }],
34
+ },
35
+ create(context) {
36
+ const sourceCode = context.sourceCode ?? context.getSourceCode!()
37
+ const text = sourceCode.getText()
38
+
39
+ return {
40
+ Program() {
41
+ for (const comment of sourceCode.getAllComments()) {
42
+ if (comment.type !== "Block") continue
43
+ const raw = text.slice(comment.range[0], comment.range[1])
44
+
45
+ if (!raw.startsWith(JSDOC_OPENER) || raw.includes("\n")) continue
46
+
47
+ // `value` excludes the delimiters, so a one-line block arrives as `"* x "`.
48
+ const body = comment.value.replace(/^\*/, "").trim()
49
+
50
+ if (!body) continue
51
+
52
+ // Everything from the start of the line up to the comment. All-whitespace means the
53
+ // block leads its line and owns the indentation; anything else means it is inline.
54
+ const lineStart = text.lastIndexOf("\n", comment.range[0] - 1) + 1
55
+ const indent = text.slice(lineStart, comment.range[0])
56
+
57
+ if (indent.trim()) continue
58
+
59
+ // A block documenting a union or intersection member is a label on one alternative, and
60
+ // the members read as a list. Three lines per entry turns a scannable list into a page,
61
+ // so the requirement does not reach inside one.
62
+ if (UNION_MEMBER.test(text.slice(comment.range[1]))) continue
63
+
64
+ context.report({
65
+ node: { type: "Block", range: comment.range },
66
+ message: "JSDoc should span multiple lines.",
67
+ fix(fixer) {
68
+ return fixer.replaceTextRange(comment.range, `/**\n${indent} * ${body}\n${indent} */`)
69
+ },
70
+ })
71
+ }
72
+ },
73
+ }
74
+ },
75
+ }
@@ -15,16 +15,24 @@
15
15
 
16
16
  import type { AstNode, Fixer, Rule, RuleContext } from "./plugin-types.js"
17
17
 
18
- /** Comparisons meaning "non-empty", which become the bare length. */
18
+ /**
19
+ * Comparisons meaning "non-empty", which become the bare length.
20
+ */
19
21
  const TRUTHY_FORMS = new Set(["> 0", "!== 0", "!= 0", ">= 1"])
20
22
 
21
- /** Comparisons meaning "empty", which become a negated length. */
23
+ /**
24
+ * Comparisons meaning "empty", which become a negated length.
25
+ */
22
26
  const FALSY_FORMS = new Set(["=== 0", "== 0", "< 1"])
23
27
 
24
- /** Members whose length-ness the rule understands. */
28
+ /**
29
+ * Members whose length-ness the rule understands.
30
+ */
25
31
  const LENGTH_PROPERTIES = new Set(["length", "size"])
26
32
 
27
- /** The comparison rendered as `<operator> <literal>`, or null when it is not a length comparison. */
33
+ /**
34
+ * The comparison rendered as `<operator> <literal>`, or null when it is not a length comparison.
35
+ */
28
36
  function classify(node: AstNode): { member: AstNode; negate: boolean } | null {
29
37
  if (node.type !== "BinaryExpression" || !node.operator || !node.left || !node.right) return null
30
38
 
@@ -84,7 +92,9 @@ export const preferLengthTruthinessRule: Rule = {
84
92
  })
85
93
  }
86
94
 
87
- /** Walk into a boolean context: logical operands and `!` arguments stay boolean. */
95
+ /**
96
+ * Walk into a boolean context: logical operands and `!` arguments stay boolean.
97
+ */
88
98
  function visitCondition(node: AstNode | null | undefined) {
89
99
  if (!node) return
90
100
 
@@ -0,0 +1,104 @@
1
+ /**
2
+ * @copyright Sister Software
3
+ * @license AGPL-3.0
4
+ * @author Teffen Ellis, et al.
5
+ * @file The `sister-software/multiline-statement-padding` rule: a statement that spans lines gets a
6
+ * blank line on each side, so a reader can see where it starts and stops without matching brackets.
7
+ *
8
+ * One rule covers what look like two habits — a big object literal jammed against the next
9
+ * declaration, and a multi-line call jammed against the counter after it. Both are the same thing:
10
+ * a statement tall enough to read as a block, sitting flush against its neighbour.
11
+ *
12
+ * Neither edge is required at a block boundary, where the brace already separates. `console.*` calls
13
+ * are left to `console-padding`, which groups consecutive ones — this rule would split a run that
14
+ * happens to contain a tall call, and a run of output is one thing.
15
+ */
16
+
17
+ import { createPaddingHelpers } from "./padding-utils.js"
18
+ import type { AstNode, Rule } from "./plugin-types.js"
19
+
20
+ /**
21
+ * The object whose calls belong to `console-padding` rather than to this rule.
22
+ */
23
+ const CONSOLE_OBJECT = "console"
24
+
25
+ /**
26
+ * Does this statement's own source span more than one line?
27
+ */
28
+ function isMultiline(node: AstNode, text: string): boolean {
29
+ return text.slice(node.range[0], node.range[1]).includes("\n")
30
+ }
31
+
32
+ /**
33
+ * Is this statement a bare `console.<method>(…)` call, which `console-padding` owns?
34
+ */
35
+ function isConsoleStatement(node: AstNode | undefined): boolean {
36
+ if (node?.type !== "ExpressionStatement") return false
37
+ const callee = node.expression?.type === "CallExpression" ? node.expression.callee : undefined
38
+
39
+ return callee?.type === "MemberExpression" && callee.object?.name === CONSOLE_OBJECT
40
+ }
41
+
42
+ export const multilineStatementPaddingRule: Rule = {
43
+ meta: {
44
+ name: "multiline-statement-padding",
45
+ type: "layout",
46
+ fixable: "whitespace",
47
+ schema: [{ type: "object", additionalProperties: true }],
48
+ },
49
+ create(context) {
50
+ const sourceCode = context.sourceCode ?? context.getSourceCode!()
51
+ const text = sourceCode.getText()
52
+ const { requirePadding } = createPaddingHelpers(context)
53
+
54
+ function check(node: AstNode): void {
55
+ const parent = node.parent
56
+
57
+ if (!parent || !Array.isArray(parent.body)) return
58
+
59
+ if (!isMultiline(node, text) || isConsoleStatement(node)) return
60
+ const body = parent.body
61
+ const index = body.indexOf(node)
62
+
63
+ if (index === -1) return
64
+ const previous = body[index - 1]
65
+ const next = body[index + 1]
66
+
67
+ if (previous && !isConsoleStatement(previous)) {
68
+ requirePadding(previous, node, node, "Expected a blank line before this multi-line statement.")
69
+ }
70
+
71
+ if (next && !isConsoleStatement(next)) {
72
+ requirePadding(node, next, node, "Expected a blank line after this multi-line statement.")
73
+ }
74
+ }
75
+
76
+ // Every statement type that can stand in a statement list and span lines. Block-like statements are
77
+ // already padded from the front by `padding-lines`; this adds their trailing edge.
78
+ return Object.fromEntries(
79
+ [
80
+ "VariableDeclaration",
81
+ "ExpressionStatement",
82
+ "IfStatement",
83
+ "ForStatement",
84
+ "ForInStatement",
85
+ "ForOfStatement",
86
+ "WhileStatement",
87
+ "DoWhileStatement",
88
+ "SwitchStatement",
89
+ "TryStatement",
90
+ "FunctionDeclaration",
91
+ "ClassDeclaration",
92
+ "ReturnStatement",
93
+ "TSInterfaceDeclaration",
94
+ "TSTypeAliasDeclaration",
95
+ "TSEnumDeclaration",
96
+ "TSModuleDeclaration",
97
+ // An exported declaration arrives wrapped, and the wrapper spans the same lines as what it
98
+ // wraps — so matching the wrapper is enough. Without these, every `export` was invisible here.
99
+ "ExportNamedDeclaration",
100
+ "ExportDefaultDeclaration",
101
+ ].map((type) => [type, check])
102
+ )
103
+ },
104
+ }
@@ -3,19 +3,23 @@
3
3
  * @license AGPL-3.0
4
4
  * @author Teffen Ellis, et al.
5
5
  * @file A padding-lines rule, authored as an oxlint JS plugin (ESLint v9-compatible API). It
6
- * requires a blank line before `return` and block-like statements (the equivalent of ESLint's
7
- * `padding-line-between-statements` with `{ blankLine: "always", prev: "*", next: "return" | "block-like" }`),
8
- * and autofixes by inserting the missing blank line (before any leading comments).
6
+ * requires a blank line before the statements that END a path or a step — `return`, `continue`,
7
+ * `break`, a bare `x++`/`x--` and before block-like statements. Roughly ESLint's
8
+ * `padding-line-between-statements` with `{ blankLine: "always", prev: "*", next: "return" | "block-like" }`,
9
+ * widened to the rest of that family. Autofixes by inserting the blank line before any leading comments.
9
10
  */
10
11
 
12
+ import { createPaddingHelpers } from "./padding-utils.js"
11
13
  import type { AstNode, Rule } from "./plugin-types.js"
12
14
 
13
15
  /**
14
- * Statement node types that require a preceding blank line: `return` plus all "block-like" statements (matching
16
+ * Statement node types that require a preceding blank line: the path-enders plus all "block-like" statements (matching
15
17
  * ESLint's `block-like` selector).
16
18
  */
17
19
  const PADDED_STATEMENT_TYPES = [
18
20
  "ReturnStatement",
21
+ "ContinueStatement",
22
+ "BreakStatement",
19
23
  "BlockStatement",
20
24
  "IfStatement",
21
25
  "ForStatement",
@@ -25,8 +29,21 @@ const PADDED_STATEMENT_TYPES = [
25
29
  "DoWhileStatement",
26
30
  "SwitchStatement",
27
31
  "TryStatement",
32
+ // An interface or a type alias with a body reads as a block too — same braces, same weight on the
33
+ // page — so it wants the same separation from whatever precedes it.
34
+ "TSInterfaceDeclaration",
35
+ "TSTypeAliasDeclaration",
36
+ "TSEnumDeclaration",
37
+ "TSModuleDeclaration",
28
38
  ] as const
29
39
 
40
+ /**
41
+ * Does this node type require a preceding blank line?
42
+ */
43
+ function isPaddedType(type: string | undefined): boolean {
44
+ return !!type && (PADDED_STATEMENT_TYPES as readonly string[]).includes(type)
45
+ }
46
+
30
47
  export const paddingRule: Rule = {
31
48
  meta: {
32
49
  name: "padding-lines",
@@ -35,9 +52,7 @@ export const paddingRule: Rule = {
35
52
  schema: [{ type: "object", additionalProperties: true }],
36
53
  },
37
54
  create(context) {
38
- const sourceCode = context.sourceCode ?? context.getSourceCode!()
39
- const text = sourceCode.getText()
40
- const comments = sourceCode.getAllComments()
55
+ const { requirePadding } = createPaddingHelpers(context)
41
56
 
42
57
  function check(node: AstNode) {
43
58
  const parent = node.parent
@@ -49,32 +64,32 @@ export const paddingRule: Rule = {
49
64
  const index = parent.body.indexOf(node)
50
65
 
51
66
  if (index <= 0) return // first statement in the block — nothing to pad against.
52
- const previous = parent.body[index - 1]!
53
67
 
54
- // A blank line should sit before the statement's own leading comments, so measure the gap
55
- // up to the earliest comment between the previous statement and this one.
56
- let start = node.range[0]
68
+ requirePadding(parent.body[index - 1]!, node, node, "Expected a blank line before this statement.")
69
+ }
57
70
 
58
- for (const comment of comments) {
59
- if (comment.range[0] >= previous.range[1] && comment.range[1] <= node.range[0]) {
60
- start = Math.min(start, comment.range[0])
71
+ return {
72
+ ...Object.fromEntries(PADDED_STATEMENT_TYPES.map((type) => [type, check])),
73
+ // A bare `x++` / `x--` is a counter step, and reads like one only when it stands apart. It
74
+ // arrives as an ExpressionStatement, so it cannot be matched by node type alone.
75
+ ExpressionStatement(node: AstNode) {
76
+ if (node.expression?.type === "UpdateExpression") {
77
+ check(node)
61
78
  }
62
- }
63
-
64
- const gap = text.slice(previous.range[1], start)
65
-
66
- if ((gap.match(/\n/g) ?? []).length >= 2) return // already a blank line.
67
-
68
- context.report({
69
- node,
70
- message: "Expected a blank line before this statement.",
71
- fix(fixer) {
72
- return fixer.insertTextAfterRange([previous.range[1], previous.range[1]], "\n")
73
- },
74
- })
79
+ },
80
+ // An exported declaration arrives wrapped. Look through the wrapper so `export interface Foo {}`
81
+ // is padded exactly like the unexported form.
82
+ ExportNamedDeclaration(node: AstNode) {
83
+ if (isPaddedType(node.declaration?.type)) {
84
+ check(node)
85
+ }
86
+ },
87
+ ExportDefaultDeclaration(node: AstNode) {
88
+ if (isPaddedType(node.declaration?.type)) {
89
+ check(node)
90
+ }
91
+ },
75
92
  }
76
-
77
- return Object.fromEntries(PADDED_STATEMENT_TYPES.map((type) => [type, check]))
78
93
  },
79
94
  }
80
95