@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.
- package/README.md +96 -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 +111 -27
- package/out/index.d.ts.map +1 -1
- package/out/index.js +57 -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 +55 -13
- package/out/plugin-types.d.ts.map +1 -1
- package/out/plugin.d.ts.map +1 -1
- package/out/plugin.js +11 -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 +1 -1
- 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 +166 -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 +55 -13
- package/src/plugin.ts +16 -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,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
|
-
/**
|
|
173
|
+
/**
|
|
174
|
+
* Rewrite explicit length comparisons to truthiness in boolean positions (on by default).
|
|
175
|
+
*/
|
|
119
176
|
lengthTruthiness?: boolean
|
|
120
|
-
/**
|
|
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
|
-
/**
|
|
190
|
+
/**
|
|
191
|
+
* Replace the globs treated as test files.
|
|
192
|
+
*/
|
|
123
193
|
testFilePatterns?: string[]
|
|
124
|
-
/**
|
|
194
|
+
/**
|
|
195
|
+
* Replace the globs treated as generated files, where only the size ceilings are switched off.
|
|
196
|
+
*/
|
|
125
197
|
generatedFilePatterns?: string[]
|
|
126
|
-
/**
|
|
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
|
-
/**
|
|
206
|
+
/**
|
|
207
|
+
* Extra config deep-merged last; an escape hatch for per-repo tweaks.
|
|
208
|
+
*/
|
|
129
209
|
overrides?: OxlintConfig
|
|
130
210
|
}
|
|
131
211
|
|
|
132
|
-
/**
|
|
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
|
-
|
|
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 ||
|
|
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
|
-
/**
|
|
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
|
|