@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,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
* @file A no-title-case-acronym rule, authored as an oxlint JS plugin (ESLint v9-compatible API). An
|
|
6
|
+
* acronym is capitalized as a whole camelCase component — `parseJSON`, `POILookup`,
|
|
7
|
+
* `createWOFResolver` — never title-cased. The mechanism knows nothing about any particular
|
|
8
|
+
* project: the acronyms are options, so adding one is a config edit rather than a code change.
|
|
9
|
+
*
|
|
10
|
+
* The subtlety the rule exists for is the COMPONENT BOUNDARY. `Poi` appears inside `Point`, `Id`
|
|
11
|
+
* inside `Identifier`, `Us` inside `User` — none of those are acronyms, they are prefixes of
|
|
12
|
+
* ordinary words. A substring match reports every one of them; a component match reports none.
|
|
13
|
+
*
|
|
14
|
+
* Deliberately not fixable: renaming a declaration without renaming its references produces code
|
|
15
|
+
* that does not compile, and a linter cannot see the references.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { AstNode, Rule } from "./plugin-types.js"
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Acronyms enforced when the caller names none. Deliberately generic — every entry is an acronym in general programming
|
|
22
|
+
* rather than in some particular domain, because this list ships to consumers who share none of each other's
|
|
23
|
+
* vocabulary. Domain acronyms belong in `extraAcronyms`.
|
|
24
|
+
*
|
|
25
|
+
* `ID` is on the list and `Id` is NOT the house form: `placeID`, `sourceID`, `nodeID`. It is the one entry short enough
|
|
26
|
+
* to hide inside ordinary words — `Id` leads `Identifier`, `Idle`, `Ideal` — which is what the component boundary is
|
|
27
|
+
* for, and it holds: none of those end a component.
|
|
28
|
+
*
|
|
29
|
+
* The other short candidates were measured against a 1,587-file corpus and turned down. Read this before adding one
|
|
30
|
+
* back:
|
|
31
|
+
*
|
|
32
|
+
* - `IO` — `IoT` is the settled spelling of the commonest identifier component starting with `Io`, so the rule would
|
|
33
|
+
* report `IoTGateway` and suggest the wrong `IOTGateway`. Zero drift in the corpus, so nothing offsets that.
|
|
34
|
+
* - `UI` — zero drift in the corpus, and the uppercase form is not settled the way `ID`'s is. `UiButton` is a deliberate
|
|
35
|
+
* choice in plenty of React codebases, and `UiPath` is a vendor that spells itself that way.
|
|
36
|
+
* - `DB` — a shorthand for one word rather than an initialism, so there is no English uppercase form to settle on. It is
|
|
37
|
+
* also the exact collision the exemptions exist for: every hit in the corpus was a Pastel option key, where the
|
|
38
|
+
* framework derives `adminDb` from `--admin-db` and the schema has to match its derivation.
|
|
39
|
+
*/
|
|
40
|
+
export const DefaultAcronyms = [
|
|
41
|
+
"API",
|
|
42
|
+
// oxlint-disable-next-line unicorn/text-encoding-identifier-case -- an entry in an acronym list, not an encoding label.
|
|
43
|
+
"ASCII",
|
|
44
|
+
"CLI",
|
|
45
|
+
"CPU",
|
|
46
|
+
"CSS",
|
|
47
|
+
"CSV",
|
|
48
|
+
"DNS",
|
|
49
|
+
"DOM",
|
|
50
|
+
"GPU",
|
|
51
|
+
"HTML",
|
|
52
|
+
"HTTP",
|
|
53
|
+
"HTTPS",
|
|
54
|
+
"ID",
|
|
55
|
+
"JSON",
|
|
56
|
+
"JSONL",
|
|
57
|
+
"JWT",
|
|
58
|
+
"MIME",
|
|
59
|
+
"PDF",
|
|
60
|
+
"RGB",
|
|
61
|
+
"RGBA",
|
|
62
|
+
"SDK",
|
|
63
|
+
"SQL",
|
|
64
|
+
"SSL",
|
|
65
|
+
"SVG",
|
|
66
|
+
"TLS",
|
|
67
|
+
"TSV",
|
|
68
|
+
"TTL",
|
|
69
|
+
"URI",
|
|
70
|
+
"URL",
|
|
71
|
+
"UTC",
|
|
72
|
+
// oxlint-disable-next-line unicorn/text-encoding-identifier-case -- an entry in an acronym list, not an encoding label.
|
|
73
|
+
"UTF8",
|
|
74
|
+
"UUID",
|
|
75
|
+
"XML",
|
|
76
|
+
"YAML",
|
|
77
|
+
] as const
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Which declarations the rule reads.
|
|
81
|
+
*/
|
|
82
|
+
export type AcronymCaseScope = "exported" | "all"
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Options accepted by {@link noTitleCaseAcronymRule}.
|
|
86
|
+
*/
|
|
87
|
+
export interface AcronymCaseOptions {
|
|
88
|
+
/**
|
|
89
|
+
* Acronyms in the ALL-CAPS spelling identifiers should use; the rule derives the title-case form (`POI` → `Poi`) and
|
|
90
|
+
* reports that. Replaces {@link DefaultAcronyms} rather than extending it.
|
|
91
|
+
*
|
|
92
|
+
* Add an acronym once the project has settled on its capitalized form. This reports drift from an established
|
|
93
|
+
* convention; it does not invent one.
|
|
94
|
+
*/
|
|
95
|
+
acronyms?: string[]
|
|
96
|
+
/**
|
|
97
|
+
* Acronyms added to whichever list is in effect — the usual way to layer a project's own vocabulary on the default.
|
|
98
|
+
*/
|
|
99
|
+
extraAcronyms?: string[]
|
|
100
|
+
/**
|
|
101
|
+
* Identifiers exempt everywhere, for names whose casing is not ours to choose: an external library's own spelling
|
|
102
|
+
* (`HttpStatusCode` from axios), a wire field someone else named (`operationId` in OpenAPI), or a prop a framework
|
|
103
|
+
* derives from something else (Pastel turns `--run-id` into `runId`, so the schema key has to match its derivation).
|
|
104
|
+
*
|
|
105
|
+
* Prefer a scoped `// oxlint-disable-next-line sister-software/no-title-case-acronym` at the one site that needs it.
|
|
106
|
+
* A config entry silently covers every future declaration of the same name.
|
|
107
|
+
*/
|
|
108
|
+
ignoreNames?: string[]
|
|
109
|
+
/**
|
|
110
|
+
* `"exported"` (the default) reads the names a consumer can see: exported declarations, the members of exported
|
|
111
|
+
* interfaces, classes and enums, and the exported half of a re-export. That is where a wrong acronym costs someone a
|
|
112
|
+
* breaking rename later; a local costs nothing to rename at any time.
|
|
113
|
+
*
|
|
114
|
+
* `"all"` reads every declaration in the file, exported or not. Object-literal keys are never read under either
|
|
115
|
+
* setting — a wire contract is a string, and its casing belongs to whoever is on the other end.
|
|
116
|
+
*/
|
|
117
|
+
scope?: AcronymCaseScope
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Characters that may follow a title-cased run for it to be a whole component: the start of the next component, a
|
|
122
|
+
* digit, or an underscore.
|
|
123
|
+
*/
|
|
124
|
+
const COMPONENT_END = /[A-Z0-9_]/
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Characters that may precede a title-cased run for it to START a component.
|
|
128
|
+
*/
|
|
129
|
+
const COMPONENT_START = /[a-z0-9_]/
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Node types opening a scope whose names are private to it. Meeting one on the walk up means the declaration is a
|
|
133
|
+
* local, however deep inside an exported declaration it sits — the body of an exported function is not public surface.
|
|
134
|
+
*/
|
|
135
|
+
const SCOPE_BOUNDARIES = new Set([
|
|
136
|
+
"ArrowFunctionExpression",
|
|
137
|
+
"BlockStatement",
|
|
138
|
+
"FunctionDeclaration",
|
|
139
|
+
"FunctionExpression",
|
|
140
|
+
"Program",
|
|
141
|
+
"StaticBlock",
|
|
142
|
+
"TSModuleBlock",
|
|
143
|
+
])
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Node types that make what they wrap part of the module's public surface.
|
|
147
|
+
*/
|
|
148
|
+
const EXPORT_WRAPPERS = new Set(["ExportNamedDeclaration", "ExportDefaultDeclaration"])
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* One title-cased run found inside an identifier.
|
|
152
|
+
*/
|
|
153
|
+
interface AcronymHit {
|
|
154
|
+
/**
|
|
155
|
+
* Where the run starts in the identifier.
|
|
156
|
+
*/
|
|
157
|
+
index: number
|
|
158
|
+
/**
|
|
159
|
+
* The house spelling, e.g. `POI`.
|
|
160
|
+
*/
|
|
161
|
+
acronym: string
|
|
162
|
+
/**
|
|
163
|
+
* The drifted spelling actually written, e.g. `Poi`.
|
|
164
|
+
*/
|
|
165
|
+
found: string
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Title case of an acronym — the drifted spelling (`POI` → `Poi`, `NZ` → `Nz`).
|
|
170
|
+
*/
|
|
171
|
+
function titleCase(acronym: string): string {
|
|
172
|
+
return acronym[0]! + acronym.slice(1).toLowerCase()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Is the run of `found` at `index` a whole camelCase component of `identifier`?
|
|
177
|
+
*
|
|
178
|
+
* This is the entire subtlety of the rule. A component ENDS where the next character starts another one — uppercase, a
|
|
179
|
+
* digit, `_`, or the end of the identifier — and STARTS at the identifier's start or after a lowercase letter, digit or
|
|
180
|
+
* underscore. `PoiBoard` qualifies (`B` follows); `Point` does not (`n` follows).
|
|
181
|
+
*/
|
|
182
|
+
function isWholeComponent(identifier: string, index: number, found: string): boolean {
|
|
183
|
+
const after = identifier[index + found.length]
|
|
184
|
+
|
|
185
|
+
if (after !== undefined && !COMPONENT_END.test(after)) return false
|
|
186
|
+
|
|
187
|
+
const before = identifier[index - 1]
|
|
188
|
+
|
|
189
|
+
return before === undefined || COMPONENT_START.test(before)
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Every title-cased acronym in `identifier`, plus the identifier as it should have been written.
|
|
194
|
+
*/
|
|
195
|
+
function findTitleCasedAcronyms(
|
|
196
|
+
identifier: string,
|
|
197
|
+
acronyms: readonly string[]
|
|
198
|
+
): { hits: AcronymHit[]; expected: string } {
|
|
199
|
+
const candidates: AcronymHit[] = []
|
|
200
|
+
|
|
201
|
+
for (const acronym of acronyms) {
|
|
202
|
+
const found = titleCase(acronym)
|
|
203
|
+
|
|
204
|
+
// A one-letter acronym, or one whose tail is already caseless (`H3`), has no drifted spelling to
|
|
205
|
+
// look for: title case and house case are the same string, so every match would be a correct one.
|
|
206
|
+
if (found === acronym) continue
|
|
207
|
+
|
|
208
|
+
for (let index = identifier.indexOf(found); index !== -1; index = identifier.indexOf(found, index + 1)) {
|
|
209
|
+
if (isWholeComponent(identifier, index, found)) {
|
|
210
|
+
candidates.push({ index, acronym, found })
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Longest run first where two start together, so `Jsonl` wins over `Json`.
|
|
216
|
+
const ordered = candidates.toSorted(
|
|
217
|
+
(left, right) => left.index - right.index || right.found.length - left.found.length
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
const hits: AcronymHit[] = []
|
|
221
|
+
let expected = ""
|
|
222
|
+
let cursor = 0
|
|
223
|
+
|
|
224
|
+
for (const hit of ordered) {
|
|
225
|
+
if (hit.index < cursor) continue
|
|
226
|
+
|
|
227
|
+
hits.push(hit)
|
|
228
|
+
expected += identifier.slice(cursor, hit.index) + hit.acronym
|
|
229
|
+
cursor = hit.index + hit.found.length
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return { hits, expected: expected + identifier.slice(cursor) }
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Is `node` part of what the module exports?
|
|
237
|
+
*
|
|
238
|
+
* The walk stops at the first scope boundary rather than running to the root, which is what separates an exported
|
|
239
|
+
* declaration from a local one that merely lives inside an exported function.
|
|
240
|
+
*/
|
|
241
|
+
function isPublicSurface(node: AstNode): boolean {
|
|
242
|
+
let current = node.parent
|
|
243
|
+
|
|
244
|
+
while (current) {
|
|
245
|
+
if (EXPORT_WRAPPERS.has(current.type)) return true
|
|
246
|
+
|
|
247
|
+
if (SCOPE_BOUNDARIES.has(current.type)) return false
|
|
248
|
+
|
|
249
|
+
current = current.parent
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return false
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export const noTitleCaseAcronymRule: Rule = {
|
|
256
|
+
meta: {
|
|
257
|
+
name: "no-title-case-acronym",
|
|
258
|
+
type: "suggestion",
|
|
259
|
+
schema: [{ type: "object", additionalProperties: true }],
|
|
260
|
+
},
|
|
261
|
+
create(context) {
|
|
262
|
+
const options = (context.options[0] ?? {}) as AcronymCaseOptions
|
|
263
|
+
const configured = options.acronyms ?? DefaultAcronyms
|
|
264
|
+
const acronyms = [...configured, ...(options.extraAcronyms ?? [])].map((acronym) => acronym.toUpperCase())
|
|
265
|
+
const ignoreNames = new Set(options.ignoreNames)
|
|
266
|
+
const scope = options.scope ?? "exported"
|
|
267
|
+
const reported = new Set<string>()
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Reports the identifier bound by `node`, if it title-cases an acronym.
|
|
271
|
+
*/
|
|
272
|
+
function check(node: AstNode | undefined | null): void {
|
|
273
|
+
if (node?.type !== "Identifier" || !node.name) return
|
|
274
|
+
|
|
275
|
+
if (ignoreNames.has(node.name)) return
|
|
276
|
+
|
|
277
|
+
// A declaration can be reached by two visitors at once (an export specifier and the binding it
|
|
278
|
+
// names, say); the range keys the site so it is reported once.
|
|
279
|
+
const site = `${node.range[0]}:${node.range[1]}`
|
|
280
|
+
|
|
281
|
+
if (reported.has(site)) return
|
|
282
|
+
|
|
283
|
+
const { hits, expected } = findTitleCasedAcronyms(node.name, acronyms)
|
|
284
|
+
|
|
285
|
+
if (!hits.length) return
|
|
286
|
+
|
|
287
|
+
reported.add(site)
|
|
288
|
+
|
|
289
|
+
const spelled = hits.map((hit) => `\`${hit.acronym}\``).join(", ")
|
|
290
|
+
|
|
291
|
+
context.report({
|
|
292
|
+
node,
|
|
293
|
+
message:
|
|
294
|
+
`\`${node.name}\` title-cases ${hits.length > 1 ? "the acronyms" : "the acronym"} ${spelled} — an ` +
|
|
295
|
+
`acronym is capitalized as a whole camelCase component, so this reads \`${expected}\`.`,
|
|
296
|
+
})
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function inScope(node: AstNode): boolean {
|
|
300
|
+
return scope === "all" || isPublicSurface(node)
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Parameters are declarations too, but renaming one breaks nobody, so they are read only when the caller has asked
|
|
305
|
+
* for every declaration in the file.
|
|
306
|
+
*/
|
|
307
|
+
function parameters(node: AstNode): void {
|
|
308
|
+
if (scope !== "all") return
|
|
309
|
+
|
|
310
|
+
for (const parameter of node.params ?? []) {
|
|
311
|
+
check(parameter)
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/**
|
|
316
|
+
* A named declaration: `function`, `class`, `interface`, `type`, `enum`, `module`, and enum members.
|
|
317
|
+
*/
|
|
318
|
+
function declared(node: AstNode): void {
|
|
319
|
+
parameters(node)
|
|
320
|
+
|
|
321
|
+
if (inScope(node)) {
|
|
322
|
+
check(node.id)
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* A member of an interface, type literal, or class — public surface of the type that holds it.
|
|
328
|
+
*/
|
|
329
|
+
function member(node: AstNode): void {
|
|
330
|
+
if (node.computed || node.accessibility === "private") return
|
|
331
|
+
|
|
332
|
+
if (inScope(node)) {
|
|
333
|
+
check(node.key)
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
FunctionDeclaration: declared,
|
|
339
|
+
ClassDeclaration: declared,
|
|
340
|
+
TSInterfaceDeclaration: declared,
|
|
341
|
+
TSTypeAliasDeclaration: declared,
|
|
342
|
+
TSEnumDeclaration: declared,
|
|
343
|
+
TSModuleDeclaration: declared,
|
|
344
|
+
TSEnumMember: declared,
|
|
345
|
+
TSPropertySignature: member,
|
|
346
|
+
TSMethodSignature: member,
|
|
347
|
+
PropertyDefinition: member,
|
|
348
|
+
MethodDefinition: member,
|
|
349
|
+
VariableDeclaration(node) {
|
|
350
|
+
if (!inScope(node)) return
|
|
351
|
+
|
|
352
|
+
for (const declarator of node.declarations ?? []) {
|
|
353
|
+
check(declarator.id)
|
|
354
|
+
}
|
|
355
|
+
},
|
|
356
|
+
// The exported half of `export { poiBoard as PoiBoard }` and `export * as PoiBoard from "…"` is
|
|
357
|
+
// the name a consumer imports, whatever the local one is called.
|
|
358
|
+
ExportSpecifier(node) {
|
|
359
|
+
check(node.exported)
|
|
360
|
+
},
|
|
361
|
+
ExportAllDeclaration(node) {
|
|
362
|
+
check(node.exported)
|
|
363
|
+
},
|
|
364
|
+
FunctionExpression: parameters,
|
|
365
|
+
ArrowFunctionExpression: parameters,
|
|
366
|
+
}
|
|
367
|
+
},
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export default noTitleCaseAcronymRule
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
* @file The browser globals whose bare use is ambiguous, and the rule entry built from them. A list
|
|
6
|
+
* this long is reference data — it belongs beside the code that consumes it, not inside the file
|
|
7
|
+
* that assembles the platform-layering overrides.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Browser globals whose bare use is ambiguous (they collide with common identifiers). Browser- and node-runtime files
|
|
12
|
+
* warn on these, nudging toward an explicit `window.` access.
|
|
13
|
+
*/
|
|
14
|
+
export const BROWSER_GLOBALS = [
|
|
15
|
+
"addEventListener",
|
|
16
|
+
"blur",
|
|
17
|
+
"close",
|
|
18
|
+
"closed",
|
|
19
|
+
"confirm",
|
|
20
|
+
"defaultStatus",
|
|
21
|
+
"defaultstatus",
|
|
22
|
+
"event",
|
|
23
|
+
"external",
|
|
24
|
+
"find",
|
|
25
|
+
"focus",
|
|
26
|
+
"frameElement",
|
|
27
|
+
"frames",
|
|
28
|
+
"history",
|
|
29
|
+
"innerHeight",
|
|
30
|
+
"innerWidth",
|
|
31
|
+
"length",
|
|
32
|
+
"location",
|
|
33
|
+
"locationbar",
|
|
34
|
+
"menubar",
|
|
35
|
+
"moveBy",
|
|
36
|
+
"moveTo",
|
|
37
|
+
"name",
|
|
38
|
+
"onblur",
|
|
39
|
+
"onerror",
|
|
40
|
+
"onfocus",
|
|
41
|
+
"onload",
|
|
42
|
+
"onresize",
|
|
43
|
+
"onunload",
|
|
44
|
+
"open",
|
|
45
|
+
"opener",
|
|
46
|
+
"opera",
|
|
47
|
+
"outerHeight",
|
|
48
|
+
"outerWidth",
|
|
49
|
+
"pageXOffset",
|
|
50
|
+
"pageYOffset",
|
|
51
|
+
"parent",
|
|
52
|
+
"print",
|
|
53
|
+
"removeEventListener",
|
|
54
|
+
"resizeBy",
|
|
55
|
+
"resizeTo",
|
|
56
|
+
"screen",
|
|
57
|
+
"screenLeft",
|
|
58
|
+
"screenTop",
|
|
59
|
+
"screenX",
|
|
60
|
+
"screenY",
|
|
61
|
+
"scroll",
|
|
62
|
+
"scrollbars",
|
|
63
|
+
"scrollBy",
|
|
64
|
+
"scrollTo",
|
|
65
|
+
"scrollX",
|
|
66
|
+
"scrollY",
|
|
67
|
+
"self",
|
|
68
|
+
"status",
|
|
69
|
+
"statusbar",
|
|
70
|
+
"stop",
|
|
71
|
+
"toolbar",
|
|
72
|
+
"top",
|
|
73
|
+
] as const
|
|
74
|
+
|
|
75
|
+
export function restrictedBrowserGlobalsRule(): unknown {
|
|
76
|
+
return ["warn", ...BROWSER_GLOBALS.map((name) => ({ name, message: `Ambiguous: did you mean \`window.${name}\`?` }))]
|
|
77
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
* @file The `sister-software/console-padding` rule: a `console.*` call gets a blank line on each
|
|
6
|
+
* side, so the lines that produce output stand apart from the lines that compute it. Scanning a
|
|
7
|
+
* long procedural script for "what does this print" then works visually.
|
|
8
|
+
*
|
|
9
|
+
* A RUN of console calls is one group — no blank lines inside it, one before the first and one
|
|
10
|
+
* after the last, because consecutive calls are one block of output. Neither edge is required
|
|
11
|
+
* where the block boundary already provides the separation: nothing is needed before a call that
|
|
12
|
+
* opens a block, or after one that closes it.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { createPaddingHelpers } from "./padding-utils.js"
|
|
16
|
+
import type { AstNode, Rule } from "./plugin-types.js"
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* The object whose method calls this rule treats as output.
|
|
20
|
+
*/
|
|
21
|
+
const CONSOLE_OBJECT = "console"
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Is this statement a bare `console.<method>(…)` call?
|
|
25
|
+
*/
|
|
26
|
+
function isConsoleStatement(node: AstNode | undefined): boolean {
|
|
27
|
+
if (node?.type !== "ExpressionStatement") return false
|
|
28
|
+
const call = node.expression
|
|
29
|
+
|
|
30
|
+
if (call?.type !== "CallExpression") return false
|
|
31
|
+
const callee = call.callee
|
|
32
|
+
|
|
33
|
+
return (
|
|
34
|
+
callee?.type === "MemberExpression" && callee.object?.type === "Identifier" && callee.object.name === CONSOLE_OBJECT
|
|
35
|
+
)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export const consolePaddingRule: Rule = {
|
|
39
|
+
meta: {
|
|
40
|
+
name: "console-padding",
|
|
41
|
+
type: "layout",
|
|
42
|
+
fixable: "whitespace",
|
|
43
|
+
schema: [{ type: "object", additionalProperties: true }],
|
|
44
|
+
},
|
|
45
|
+
create(context) {
|
|
46
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode!()
|
|
47
|
+
const text = sourceCode.getText()
|
|
48
|
+
const { requirePadding } = createPaddingHelpers(context)
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
ExpressionStatement(node: AstNode) {
|
|
52
|
+
if (!isConsoleStatement(node)) return
|
|
53
|
+
const parent = node.parent
|
|
54
|
+
|
|
55
|
+
// Only statements in a statement list have neighbours to pad against.
|
|
56
|
+
if (!parent || !Array.isArray(parent.body)) return
|
|
57
|
+
const body = parent.body
|
|
58
|
+
const index = body.indexOf(node)
|
|
59
|
+
|
|
60
|
+
if (index === -1) return
|
|
61
|
+
const previous = body[index - 1]
|
|
62
|
+
const next = body[index + 1]
|
|
63
|
+
|
|
64
|
+
// Nothing before a call that opens the block, and nothing between calls in one run.
|
|
65
|
+
if (previous && !isConsoleStatement(previous)) {
|
|
66
|
+
requirePadding(previous, node, node, "Expected a blank line before this console call.")
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Nothing after a call that closes the block — the brace already separates it.
|
|
70
|
+
if (next && !isConsoleStatement(next)) {
|
|
71
|
+
requirePadding(node, next, node, "Expected a blank line after this console call.")
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
}
|
|
@@ -11,18 +11,28 @@
|
|
|
11
11
|
|
|
12
12
|
import type { Rule } from "./plugin-types.js"
|
|
13
13
|
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* Names in SCREAMING_SNAKE_CASE, the convention for a tuning knob.
|
|
16
|
+
*/
|
|
15
17
|
const SCREAMING_CASE = /^[A-Z][A-Z0-9_]*$/
|
|
16
18
|
|
|
17
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* Initializers that make the binding a function rather than a constant value.
|
|
21
|
+
*/
|
|
18
22
|
const FUNCTION_INITIALIZERS = new Set(["ArrowFunctionExpression", "FunctionExpression"])
|
|
19
23
|
|
|
20
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Which module-level constants the rule applies to.
|
|
26
|
+
*/
|
|
21
27
|
export type ConstantDocScope = "exported" | "screaming" | "exported-or-screaming"
|
|
22
28
|
|
|
23
|
-
/**
|
|
29
|
+
/**
|
|
30
|
+
* Options accepted by {@link requireConstantDocRule}.
|
|
31
|
+
*/
|
|
24
32
|
export interface ConstantDocOptions {
|
|
25
|
-
/**
|
|
33
|
+
/**
|
|
34
|
+
* Defaults to `"exported-or-screaming"`.
|
|
35
|
+
*/
|
|
26
36
|
scope?: ConstantDocScope
|
|
27
37
|
/**
|
|
28
38
|
* Export names a framework requires and gives meaning to, which a JSDoc block cannot improve on — Pastel's
|
package/src/headers-plugin.ts
CHANGED
|
@@ -9,7 +9,9 @@
|
|
|
9
9
|
|
|
10
10
|
import type { Comment, Plugin, Rule } from "./plugin-types.js"
|
|
11
11
|
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* Options accepted by the header rule.
|
|
14
|
+
*/
|
|
13
15
|
export interface HeaderRuleOptions {
|
|
14
16
|
copyrightHolder?: string
|
|
15
17
|
spdxLicenseIdentifier?: string
|
|
@@ -96,7 +98,9 @@ export const headerRule: Rule = {
|
|
|
96
98
|
},
|
|
97
99
|
}
|
|
98
100
|
|
|
99
|
-
/**
|
|
101
|
+
/**
|
|
102
|
+
* The Sister Software oxlint header plugin. Registers the `sister-software/require-file-header` rule.
|
|
103
|
+
*/
|
|
100
104
|
const headerPlugin: Plugin = {
|
|
101
105
|
meta: { name: "sister-software" },
|
|
102
106
|
rules: { "require-file-header": headerRule },
|