@sister.software/oxlint-config 10.0.0 → 12.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.
- package/README.md +169 -0
- package/out/acronym-case-plugin.d.ts +80 -0
- package/out/acronym-case-plugin.d.ts.map +1 -0
- package/out/acronym-case-plugin.js +272 -0
- package/out/acronym-case-plugin.js.map +1 -0
- package/out/browser-globals.d.ts +15 -0
- package/out/browser-globals.d.ts.map +1 -0
- package/out/browser-globals.js +76 -0
- package/out/browser-globals.js.map +1 -0
- package/out/console-padding-plugin.d.ts +16 -0
- package/out/console-padding-plugin.d.ts.map +1 -0
- package/out/console-padding-plugin.js +68 -0
- package/out/console-padding-plugin.js.map +1 -0
- package/out/constant-doc-plugin.d.ts +9 -3
- package/out/constant-doc-plugin.d.ts.map +1 -1
- package/out/constant-doc-plugin.js +6 -2
- package/out/constant-doc-plugin.js.map +1 -1
- package/out/headers-plugin.d.ts +6 -2
- package/out/headers-plugin.d.ts.map +1 -1
- package/out/headers-plugin.js +3 -1
- package/out/headers-plugin.js.map +1 -1
- package/out/index.d.ts +123 -27
- package/out/index.d.ts.map +1 -1
- package/out/index.js +63 -5
- package/out/index.js.map +1 -1
- package/out/jsdoc-plugin.d.ts +16 -0
- package/out/jsdoc-plugin.d.ts.map +1 -0
- package/out/jsdoc-plugin.js +68 -0
- package/out/jsdoc-plugin.js.map +1 -0
- package/out/length-truthiness-plugin.d.ts.map +1 -1
- package/out/length-truthiness-plugin.js +15 -5
- package/out/length-truthiness-plugin.js.map +1 -1
- package/out/multiline-statement-plugin.d.ts +18 -0
- package/out/multiline-statement-plugin.d.ts.map +1 -0
- package/out/multiline-statement-plugin.js +93 -0
- package/out/multiline-statement-plugin.js.map +1 -0
- package/out/padding-plugin.d.ts +4 -3
- package/out/padding-plugin.d.ts.map +1 -1
- package/out/padding-plugin.js +44 -27
- package/out/padding-plugin.js.map +1 -1
- package/out/padding-utils.d.ts +34 -0
- package/out/padding-utils.d.ts.map +1 -0
- package/out/padding-utils.js +50 -0
- package/out/padding-utils.js.map +1 -0
- package/out/plugin-types.d.ts +75 -13
- package/out/plugin-types.d.ts.map +1 -1
- package/out/plugin.d.ts.map +1 -1
- package/out/plugin.js +13 -0
- package/out/plugin.js.map +1 -1
- package/out/process-globals-plugin.d.ts.map +1 -1
- package/out/process-globals-plugin.js +6 -2
- package/out/process-globals-plugin.js.map +1 -1
- package/out/restrictions.d.ts +9 -3
- package/out/restrictions.d.ts.map +1 -1
- package/out/restrictions.js +4 -70
- package/out/restrictions.js.map +1 -1
- package/out/section-marker-plugin.d.ts +25 -0
- package/out/section-marker-plugin.d.ts.map +1 -0
- package/out/section-marker-plugin.js +234 -0
- package/out/section-marker-plugin.js.map +1 -0
- package/out/threshold-plugin.d.ts +9 -3
- package/out/threshold-plugin.d.ts.map +1 -1
- package/out/threshold-plugin.js +9 -3
- package/out/threshold-plugin.js.map +1 -1
- package/package.json +3 -3
- package/src/acronym-case-plugin.ts +370 -0
- package/src/browser-globals.ts +77 -0
- package/src/console-padding-plugin.ts +76 -0
- package/src/constant-doc-plugin.ts +15 -5
- package/src/headers-plugin.ts +6 -2
- package/src/index.ts +183 -29
- package/src/jsdoc-plugin.ts +75 -0
- package/src/length-truthiness-plugin.ts +15 -5
- package/src/multiline-statement-plugin.ts +104 -0
- package/src/padding-plugin.ts +44 -29
- package/src/padding-utils.ts +70 -0
- package/src/plugin-types.ts +75 -13
- package/src/plugin.ts +18 -0
- package/src/process-globals-plugin.ts +6 -2
- package/src/restrictions.ts +16 -77
- package/src/section-marker-plugin.ts +306 -0
- 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
|
-
/**
|
|
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
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Numeric ceilings for the legibility-guardrail rules. Each is a hard ceiling, not a target.
|
|
19
|
+
*/
|
|
16
20
|
export interface OxlintConfigLimits {
|
|
17
|
-
/**
|
|
21
|
+
/**
|
|
22
|
+
* Maximum block nesting depth.
|
|
23
|
+
*/
|
|
18
24
|
maxDepth: number
|
|
19
|
-
/**
|
|
25
|
+
/**
|
|
26
|
+
* Maximum parameters on a single function.
|
|
27
|
+
*/
|
|
20
28
|
maxParams: number
|
|
21
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Maximum statements in a single function body.
|
|
31
|
+
*/
|
|
22
32
|
maxStatements: number
|
|
23
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Maximum lines in a single function body, blank lines and comments excluded.
|
|
35
|
+
*/
|
|
24
36
|
maxLinesPerFunction: number
|
|
25
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* Maximum lines in a single file, blank lines and comments excluded.
|
|
39
|
+
*/
|
|
26
40
|
maxLines: number
|
|
27
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Maximum nested callback depth.
|
|
43
|
+
*/
|
|
28
44
|
maxNestedCallbacks: number
|
|
29
|
-
/**
|
|
45
|
+
/**
|
|
46
|
+
* Maximum nested call-expression depth, e.g. `a(b(c(d())))`.
|
|
47
|
+
*/
|
|
30
48
|
maxNestedCalls: number
|
|
31
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
118
|
+
/**
|
|
119
|
+
* The package namespace whose runtime boundaries are enforced, e.g. `@sister.software`.
|
|
120
|
+
*/
|
|
88
121
|
packageNamespace?: string
|
|
89
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* The copyright holder stamped into file headers.
|
|
124
|
+
*/
|
|
90
125
|
copyrightHolder?: string
|
|
91
|
-
/**
|
|
126
|
+
/**
|
|
127
|
+
* The SPDX license identifier stamped into file headers.
|
|
128
|
+
*/
|
|
92
129
|
spdxLicenseIdentifier?: string
|
|
93
|
-
/**
|
|
130
|
+
/**
|
|
131
|
+
* The author stamped into file headers.
|
|
132
|
+
*/
|
|
94
133
|
author?: string
|
|
95
|
-
/**
|
|
134
|
+
/**
|
|
135
|
+
* Enable oxlint's React plugin (off by default).
|
|
136
|
+
*/
|
|
96
137
|
react?: boolean
|
|
97
|
-
/**
|
|
138
|
+
/**
|
|
139
|
+
* Enforce file headers via the bundled JS plugin (on by default).
|
|
140
|
+
*/
|
|
98
141
|
headers?: boolean
|
|
99
|
-
/**
|
|
142
|
+
/**
|
|
143
|
+
* Require a blank line before `return`/block-like statements (on by default).
|
|
144
|
+
*/
|
|
100
145
|
padding?: boolean
|
|
101
|
-
/**
|
|
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,57 @@ 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
|
-
/**
|
|
173
|
+
/**
|
|
174
|
+
* Flag an acronym title-cased inside an identifier — `parseJson` where the house form is `parseJSON` (off by
|
|
175
|
+
* default). `acronyms` replaces the shipped list, `extraAcronyms` layers a project's own vocabulary on top of it,
|
|
176
|
+
* `ignoreNames` exempts identifiers whose casing is not ours to choose, and `scope` widens the rule past exported
|
|
177
|
+
* declarations.
|
|
178
|
+
*/
|
|
179
|
+
acronymCasing?:
|
|
180
|
+
| boolean
|
|
181
|
+
| { acronyms?: string[]; extraAcronyms?: string[]; ignoreNames?: string[]; scope?: "exported" | "all" }
|
|
182
|
+
/**
|
|
183
|
+
* Rewrite explicit length comparisons to truthiness in boolean positions (on by default).
|
|
184
|
+
*/
|
|
119
185
|
lengthTruthiness?: boolean
|
|
120
|
-
/**
|
|
186
|
+
/**
|
|
187
|
+
* Require JSDoc blocks to span multiple lines (on by default). Off for untyped files.
|
|
188
|
+
*/
|
|
189
|
+
multilineJSDoc?: boolean
|
|
190
|
+
/**
|
|
191
|
+
* Enforce the section-marker ladder — `----` banners become `// MARK:`, long labels are flagged, and a file with many
|
|
192
|
+
* markers is nudged toward regions and then toward being several files (on by default).
|
|
193
|
+
*/
|
|
194
|
+
sectionMarkers?: boolean | { maxBodyLength?: number; maxRegions?: number }
|
|
195
|
+
/**
|
|
196
|
+
* Override individual legibility ceilings. Unspecified keys keep their calibrated default.
|
|
197
|
+
*/
|
|
121
198
|
limits?: Partial<OxlintConfigLimits>
|
|
122
|
-
/**
|
|
199
|
+
/**
|
|
200
|
+
* Replace the globs treated as test files.
|
|
201
|
+
*/
|
|
123
202
|
testFilePatterns?: string[]
|
|
124
|
-
/**
|
|
203
|
+
/**
|
|
204
|
+
* Replace the globs treated as generated files, where only the size ceilings are switched off.
|
|
205
|
+
*/
|
|
125
206
|
generatedFilePatterns?: string[]
|
|
126
|
-
/**
|
|
207
|
+
/**
|
|
208
|
+
* Replace the globs treated as untyped, where the multi-line JSDoc requirement switches off.
|
|
209
|
+
*/
|
|
210
|
+
untypedFilePatterns?: string[]
|
|
211
|
+
/**
|
|
212
|
+
* Override the default ignore patterns.
|
|
213
|
+
*/
|
|
127
214
|
ignorePatterns?: string[]
|
|
128
|
-
/**
|
|
215
|
+
/**
|
|
216
|
+
* Extra config deep-merged last; an escape hatch for per-repo tweaks.
|
|
217
|
+
*/
|
|
129
218
|
overrides?: OxlintConfig
|
|
130
219
|
}
|
|
131
220
|
|
|
132
|
-
/**
|
|
221
|
+
/**
|
|
222
|
+
* Default ignore patterns for generated/build output and vendored tooling.
|
|
223
|
+
*/
|
|
133
224
|
export const DefaultIgnorePatterns = [
|
|
134
225
|
"**/out",
|
|
135
226
|
"**/dist",
|
|
@@ -165,14 +256,20 @@ export function createOxlintConfig(options: OxlintConfigOptions = {}): OxlintCon
|
|
|
165
256
|
react = false,
|
|
166
257
|
headers = true,
|
|
167
258
|
padding = true,
|
|
259
|
+
consolePadding = true,
|
|
260
|
+
multilineStatementPadding = true,
|
|
168
261
|
braces = true,
|
|
169
262
|
restrictProcessGlobals = false,
|
|
170
263
|
unnamedThresholds = false,
|
|
171
264
|
constantDocs = false,
|
|
265
|
+
acronymCasing = false,
|
|
172
266
|
lengthTruthiness = true,
|
|
267
|
+
multilineJSDoc = true,
|
|
268
|
+
sectionMarkers = true,
|
|
173
269
|
limits: limitOverrides = {},
|
|
174
270
|
testFilePatterns = DefaultTestFilePatterns,
|
|
175
271
|
generatedFilePatterns = DefaultGeneratedFilePatterns,
|
|
272
|
+
untypedFilePatterns = DefaultUntypedFilePatterns,
|
|
176
273
|
ignorePatterns = DefaultIgnorePatterns,
|
|
177
274
|
overrides = {},
|
|
178
275
|
} = options
|
|
@@ -432,10 +529,48 @@ export function createOxlintConfig(options: OxlintConfigOptions = {}): OxlintCon
|
|
|
432
529
|
rules["sister-software/require-constant-doc"] = ["error", typeof constantDocs === "object" ? constantDocs : {}]
|
|
433
530
|
}
|
|
434
531
|
|
|
532
|
+
if (acronymCasing) {
|
|
533
|
+
// Error severity: on exported surface a title-cased acronym costs a consumer a breaking rename
|
|
534
|
+
// later, and the convention it drifts from is one a reviewer cannot enforce by reading.
|
|
535
|
+
rules["sister-software/no-title-case-acronym"] = ["error", typeof acronymCasing === "object" ? acronymCasing : {}]
|
|
536
|
+
}
|
|
537
|
+
|
|
435
538
|
if (lengthTruthiness) {
|
|
436
539
|
rules["sister-software/prefer-length-truthiness"] = "error"
|
|
437
540
|
}
|
|
438
541
|
|
|
542
|
+
if (consolePadding) {
|
|
543
|
+
rules["sister-software/console-padding"] = "warn"
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (multilineStatementPadding) {
|
|
547
|
+
rules["sister-software/multiline-statement-padding"] = "warn"
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
if (multilineJSDoc) {
|
|
551
|
+
rules["sister-software/multiline-jsdoc"] = "error"
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (sectionMarkers) {
|
|
555
|
+
const markerOptions = typeof sectionMarkers === "object" ? sectionMarkers : {}
|
|
556
|
+
// The two mechanical rungs are errors: a banner has exactly one correct rewrite, and a label
|
|
557
|
+
// length is measurable. The two judgment rungs warn — where a section ends, and whether it
|
|
558
|
+
// should become its own file, are not decisions a linter gets to make.
|
|
559
|
+
rules["sister-software/prefer-mark-comment"] = "error"
|
|
560
|
+
|
|
561
|
+
rules["sister-software/concise-section-marker"] = [
|
|
562
|
+
"error",
|
|
563
|
+
markerOptions.maxBodyLength === undefined ? {} : { maxBodyLength: markerOptions.maxBodyLength },
|
|
564
|
+
]
|
|
565
|
+
|
|
566
|
+
rules["sister-software/prefer-region-over-marks"] = "warn"
|
|
567
|
+
|
|
568
|
+
rules["sister-software/max-regions"] = [
|
|
569
|
+
"warn",
|
|
570
|
+
markerOptions.maxRegions === undefined ? {} : { max: markerOptions.maxRegions },
|
|
571
|
+
]
|
|
572
|
+
}
|
|
573
|
+
|
|
439
574
|
// Rules switched off inside test files. Table-driven test bodies are legitimately long, and
|
|
440
575
|
// expected values are legitimately unnamed numbers. oxlint validates override entries against the
|
|
441
576
|
// registered rule set, so an entry may only name a rule this config actually turned on.
|
|
@@ -460,7 +595,16 @@ export function createOxlintConfig(options: OxlintConfigOptions = {}): OxlintCon
|
|
|
460
595
|
testFileRules["sister-software/require-constant-doc"] = "off"
|
|
461
596
|
}
|
|
462
597
|
|
|
463
|
-
|
|
598
|
+
if (sectionMarkers) {
|
|
599
|
+
// A test file's sections track the suite's shape, which the code under test dictates — a big
|
|
600
|
+
// table of cases legitimately wants many markers, and splitting it would scatter the suite.
|
|
601
|
+
testFileRules["sister-software/prefer-region-over-marks"] = "off"
|
|
602
|
+
testFileRules["sister-software/max-regions"] = "off"
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
/**
|
|
606
|
+
* Generated files: size ceilings only. Everything else still applies — generated code ships.
|
|
607
|
+
*/
|
|
464
608
|
const generatedFileRules: Record<string, unknown> = {
|
|
465
609
|
"max-lines": "off",
|
|
466
610
|
"max-lines-per-function": "off",
|
|
@@ -470,7 +614,16 @@ export function createOxlintConfig(options: OxlintConfigOptions = {}): OxlintCon
|
|
|
470
614
|
|
|
471
615
|
return {
|
|
472
616
|
plugins,
|
|
473
|
-
...(headers ||
|
|
617
|
+
...(headers ||
|
|
618
|
+
padding ||
|
|
619
|
+
braces ||
|
|
620
|
+
restrictProcessGlobals ||
|
|
621
|
+
unnamedThresholds ||
|
|
622
|
+
constantDocs ||
|
|
623
|
+
acronymCasing ||
|
|
624
|
+
lengthTruthiness ||
|
|
625
|
+
multilineJSDoc ||
|
|
626
|
+
sectionMarkers
|
|
474
627
|
? { jsPlugins: ["@sister.software/oxlint-config/plugin"] }
|
|
475
628
|
: {}),
|
|
476
629
|
categories: { correctness: "error" },
|
|
@@ -480,6 +633,7 @@ export function createOxlintConfig(options: OxlintConfigOptions = {}): OxlintCon
|
|
|
480
633
|
...createRuntimeOverrides(packageNamespace),
|
|
481
634
|
{ files: testFilePatterns, rules: testFileRules },
|
|
482
635
|
{ files: generatedFilePatterns, rules: generatedFileRules },
|
|
636
|
+
...(multilineJSDoc ? [{ files: untypedFilePatterns, rules: { "sister-software/multiline-jsdoc": "off" } }] : []),
|
|
483
637
|
],
|
|
484
638
|
...overrides,
|
|
485
639
|
}
|
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* Comparisons meaning "empty", which become a negated length.
|
|
25
|
+
*/
|
|
22
26
|
const FALSY_FORMS = new Set(["=== 0", "== 0", "< 1"])
|
|
23
27
|
|
|
24
|
-
/**
|
|
28
|
+
/**
|
|
29
|
+
* Members whose length-ness the rule understands.
|
|
30
|
+
*/
|
|
25
31
|
const LENGTH_PROPERTIES = new Set(["length", "size"])
|
|
26
32
|
|
|
27
|
-
/**
|
|
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
|
-
/**
|
|
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
|
+
}
|
package/src/padding-plugin.ts
CHANGED
|
@@ -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
|
|
7
|
-
* `
|
|
8
|
-
*
|
|
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:
|
|
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
|
|
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
|
-
|
|
55
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
|