@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
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
* @file The blank-line machinery the three padding rules share: how to tell whether two statements are
|
|
6
|
+
* already separated, and how to ask for the separation when they are not.
|
|
7
|
+
*
|
|
8
|
+
* All three anchor their fix to the END of the earlier statement. That is deliberate: when two rules
|
|
9
|
+
* want the same blank line — a console call before a `return`, say — they emit the identical edit, so
|
|
10
|
+
* one is applied and the other is a no-op, instead of each inserting a newline and producing a gap of
|
|
11
|
+
* two.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { AstNode, Fixer, RuleContext, SourceCode } from "./plugin-types.js"
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* What a padding rule needs from its context, resolved once per `create`.
|
|
18
|
+
*/
|
|
19
|
+
export interface PaddingHelpers {
|
|
20
|
+
/**
|
|
21
|
+
* Is there already a blank line between these two statements?
|
|
22
|
+
*/
|
|
23
|
+
isPadded(from: AstNode, to: AstNode): boolean
|
|
24
|
+
/**
|
|
25
|
+
* Report and fix a missing blank line between `from` and `to`, unless one is impossible or present.
|
|
26
|
+
*/
|
|
27
|
+
requirePadding(from: AstNode, to: AstNode, node: AstNode, message: string): void
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Build the helpers for one rule invocation.
|
|
32
|
+
*
|
|
33
|
+
* A gap is measured to the earliest comment that leads the later statement, so the blank line lands above a statement's
|
|
34
|
+
* own comments rather than between them and their subject.
|
|
35
|
+
*/
|
|
36
|
+
export function createPaddingHelpers(context: RuleContext): PaddingHelpers {
|
|
37
|
+
const sourceCode: SourceCode = context.sourceCode ?? context.getSourceCode!()
|
|
38
|
+
const text = sourceCode.getText()
|
|
39
|
+
const comments = sourceCode.getAllComments()
|
|
40
|
+
|
|
41
|
+
function isPadded(from: AstNode, to: AstNode): boolean {
|
|
42
|
+
// Two statements with no newline between them cannot be separated by a blank line. This is the
|
|
43
|
+
// `;(expr)` ASI guard: the semicolon terminates the PREVIOUS statement, so the gap here is zero
|
|
44
|
+
// characters wide. Asking for padding there is asking for the impossible.
|
|
45
|
+
if (!text.slice(from.range[1], to.range[0]).includes("\n")) return true
|
|
46
|
+
let start = to.range[0]
|
|
47
|
+
|
|
48
|
+
for (const comment of comments) {
|
|
49
|
+
if (comment.range[0] >= from.range[1] && comment.range[1] <= to.range[0]) {
|
|
50
|
+
start = Math.min(start, comment.range[0])
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return (text.slice(from.range[1], start).match(/\n/g) ?? []).length >= 2
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function requirePadding(from: AstNode, to: AstNode, node: AstNode, message: string): void {
|
|
58
|
+
if (isPadded(from, to)) return
|
|
59
|
+
|
|
60
|
+
context.report({
|
|
61
|
+
node,
|
|
62
|
+
message,
|
|
63
|
+
fix(fixer: Fixer) {
|
|
64
|
+
return fixer.insertTextAfterRange([from.range[1], from.range[1]], "\n")
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { isPadded, requirePadding }
|
|
70
|
+
}
|
package/src/plugin-types.ts
CHANGED
|
@@ -12,39 +12,101 @@ export interface Comment {
|
|
|
12
12
|
range: [number, number]
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
/**
|
|
15
|
+
/**
|
|
16
|
+
* A loosely-typed AST node — only the fields the bundled rules read.
|
|
17
|
+
*/
|
|
16
18
|
export interface AstNode {
|
|
17
19
|
type: string
|
|
18
20
|
range: [number, number]
|
|
19
21
|
parent?: AstNode
|
|
20
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* A statement-list body (block/program) is an array; a loop body is a single statement.
|
|
24
|
+
*/
|
|
21
25
|
body?: AstNode[] | AstNode
|
|
22
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* `if`/`else` branches, for the require-braces rule.
|
|
28
|
+
*/
|
|
23
29
|
consequent?: AstNode
|
|
24
30
|
alternate?: AstNode | null
|
|
25
|
-
/**
|
|
31
|
+
/**
|
|
32
|
+
* Binary/unary operator text, for the threshold rule.
|
|
33
|
+
*/
|
|
26
34
|
operator?: string
|
|
27
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Binary-expression operands.
|
|
37
|
+
*/
|
|
28
38
|
left?: AstNode
|
|
29
39
|
right?: AstNode
|
|
30
|
-
/**
|
|
40
|
+
/**
|
|
41
|
+
* Unary-expression operand.
|
|
42
|
+
*/
|
|
31
43
|
argument?: AstNode
|
|
32
|
-
/**
|
|
44
|
+
/**
|
|
45
|
+
* A literal's value, and its verbatim source text (`raw` preserves a `0x` prefix).
|
|
46
|
+
*/
|
|
33
47
|
value?: unknown
|
|
34
48
|
raw?: string
|
|
35
|
-
/**
|
|
49
|
+
/**
|
|
50
|
+
* `const` / `let` / `var`, for the constant-doc rule.
|
|
51
|
+
*/
|
|
36
52
|
kind?: string
|
|
37
|
-
/**
|
|
53
|
+
/**
|
|
54
|
+
* Declarators of a variable declaration.
|
|
55
|
+
*/
|
|
38
56
|
declarations?: AstNode[]
|
|
39
|
-
/**
|
|
57
|
+
/**
|
|
58
|
+
* A declarator's binding identifier and initializer.
|
|
59
|
+
*/
|
|
40
60
|
id?: AstNode
|
|
41
61
|
init?: AstNode | null
|
|
42
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* An identifier's name.
|
|
64
|
+
*/
|
|
43
65
|
name?: string
|
|
44
|
-
/**
|
|
66
|
+
/**
|
|
67
|
+
* The condition of an `if`/`while`/`for`/ternary, for the length-truthiness rule.
|
|
68
|
+
*/
|
|
45
69
|
test?: AstNode | null
|
|
46
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* A member expression's accessed property.
|
|
72
|
+
*/
|
|
47
73
|
property?: AstNode
|
|
74
|
+
/**
|
|
75
|
+
* An expression statement's expression, for the console-padding rule.
|
|
76
|
+
*/
|
|
77
|
+
expression?: AstNode
|
|
78
|
+
/**
|
|
79
|
+
* An export statement's inner declaration, for the padding rules.
|
|
80
|
+
*/
|
|
81
|
+
declaration?: AstNode | null
|
|
82
|
+
/**
|
|
83
|
+
* A call expression's callee.
|
|
84
|
+
*/
|
|
85
|
+
callee?: AstNode
|
|
86
|
+
/**
|
|
87
|
+
* A member expression's object.
|
|
88
|
+
*/
|
|
89
|
+
object?: AstNode
|
|
90
|
+
/**
|
|
91
|
+
* The key of a class member or a type member, for the acronym-case rule. Computed keys are an expression rather than
|
|
92
|
+
* a name, which `computed` distinguishes.
|
|
93
|
+
*/
|
|
94
|
+
key?: AstNode
|
|
95
|
+
computed?: boolean
|
|
96
|
+
/**
|
|
97
|
+
* The two halves of `export { local as exported }`. `exported` also carries the name of an `export * as name from
|
|
98
|
+
* "…"`.
|
|
99
|
+
*/
|
|
100
|
+
exported?: AstNode | null
|
|
101
|
+
local?: AstNode
|
|
102
|
+
/**
|
|
103
|
+
* A function's parameter list.
|
|
104
|
+
*/
|
|
105
|
+
params?: AstNode[]
|
|
106
|
+
/**
|
|
107
|
+
* A TypeScript class member's `public` / `protected` / `private` modifier.
|
|
108
|
+
*/
|
|
109
|
+
accessibility?: string
|
|
48
110
|
}
|
|
49
111
|
|
|
50
112
|
export interface SourceCode {
|
package/src/plugin.ts
CHANGED
|
@@ -6,13 +6,23 @@
|
|
|
6
6
|
* names) exposing all of Sister Software's custom rules under the `sister-software/` namespace.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
+
import { noTitleCaseAcronymRule } from "./acronym-case-plugin.js"
|
|
9
10
|
import { bracesRule } from "./braces-plugin.js"
|
|
11
|
+
import { consolePaddingRule } from "./console-padding-plugin.js"
|
|
10
12
|
import { requireConstantDocRule } from "./constant-doc-plugin.js"
|
|
11
13
|
import { headerRule } from "./headers-plugin.js"
|
|
14
|
+
import { multilineJSDocRule } from "./jsdoc-plugin.js"
|
|
12
15
|
import { preferLengthTruthinessRule } from "./length-truthiness-plugin.js"
|
|
16
|
+
import { multilineStatementPaddingRule } from "./multiline-statement-plugin.js"
|
|
13
17
|
import { paddingRule } from "./padding-plugin.js"
|
|
14
18
|
import type { Plugin } from "./plugin-types.js"
|
|
15
19
|
import { noProcessGlobalsRule } from "./process-globals-plugin.js"
|
|
20
|
+
import {
|
|
21
|
+
conciseSectionMarkerRule,
|
|
22
|
+
maxRegionsRule,
|
|
23
|
+
preferMarkCommentRule,
|
|
24
|
+
preferRegionOverMarksRule,
|
|
25
|
+
} from "./section-marker-plugin.js"
|
|
16
26
|
import { noUnnamedThresholdRule } from "./threshold-plugin.js"
|
|
17
27
|
|
|
18
28
|
const sisterSoftwarePlugin: Plugin = {
|
|
@@ -20,11 +30,19 @@ const sisterSoftwarePlugin: Plugin = {
|
|
|
20
30
|
rules: {
|
|
21
31
|
"require-file-header": headerRule,
|
|
22
32
|
"padding-lines": paddingRule,
|
|
33
|
+
"console-padding": consolePaddingRule,
|
|
34
|
+
"multiline-statement-padding": multilineStatementPaddingRule,
|
|
23
35
|
"require-braces": bracesRule,
|
|
24
36
|
"no-process-globals": noProcessGlobalsRule,
|
|
25
37
|
"no-unnamed-threshold": noUnnamedThresholdRule,
|
|
38
|
+
"no-title-case-acronym": noTitleCaseAcronymRule,
|
|
26
39
|
"require-constant-doc": requireConstantDocRule,
|
|
27
40
|
"prefer-length-truthiness": preferLengthTruthinessRule,
|
|
41
|
+
"multiline-jsdoc": multilineJSDocRule,
|
|
42
|
+
"prefer-mark-comment": preferMarkCommentRule,
|
|
43
|
+
"concise-section-marker": conciseSectionMarkerRule,
|
|
44
|
+
"prefer-region-over-marks": preferRegionOverMarksRule,
|
|
45
|
+
"max-regions": maxRegionsRule,
|
|
28
46
|
},
|
|
29
47
|
}
|
|
30
48
|
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
|
|
11
11
|
import type { AstNode, Rule } from "./plugin-types.js"
|
|
12
12
|
|
|
13
|
-
/**
|
|
13
|
+
/**
|
|
14
|
+
* `process` members that must be reached through a blessed helper, not accessed directly.
|
|
15
|
+
*/
|
|
14
16
|
const RESTRICTED_MEMBERS = new Set(["env", "argv"])
|
|
15
17
|
|
|
16
18
|
interface Identifierish extends AstNode {
|
|
@@ -24,7 +26,9 @@ interface MemberNode extends AstNode {
|
|
|
24
26
|
computed?: boolean
|
|
25
27
|
}
|
|
26
28
|
|
|
27
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* The accessed member name for `process.env` (identifier) or `process["env"]` (string literal).
|
|
31
|
+
*/
|
|
28
32
|
function accessedMember(node: MemberNode): string | null {
|
|
29
33
|
const property = node.property
|
|
30
34
|
|
package/src/restrictions.ts
CHANGED
|
@@ -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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
|
91
|
+
//#region Override generation
|
|
84
92
|
|
|
85
93
|
/**
|
|
86
|
-
*
|
|
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
|
-
/**
|
|
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
|
+
}
|