@sister.software/oxlint-config 9.3.0 → 10.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 +102 -10
- package/out/constant-doc-plugin.d.ts +26 -0
- package/out/constant-doc-plugin.d.ts.map +1 -0
- package/out/constant-doc-plugin.js +87 -0
- package/out/constant-doc-plugin.js.map +1 -0
- package/out/headers-plugin.js +1 -1
- package/out/headers-plugin.js.map +1 -1
- package/out/index.d.ts +70 -1
- package/out/index.d.ts.map +1 -1
- package/out/index.js +280 -6
- package/out/index.js.map +1 -1
- package/out/length-truthiness-plugin.d.ts +18 -0
- package/out/length-truthiness-plugin.d.ts.map +1 -0
- package/out/length-truthiness-plugin.js +113 -0
- package/out/length-truthiness-plugin.js.map +1 -0
- package/out/plugin-types.d.ts +23 -0
- package/out/plugin-types.d.ts.map +1 -1
- package/out/plugin.d.ts.map +1 -1
- package/out/plugin.js +6 -0
- package/out/plugin.js.map +1 -1
- package/out/threshold-plugin.d.ts +21 -0
- package/out/threshold-plugin.d.ts.map +1 -0
- package/out/threshold-plugin.js +72 -0
- package/out/threshold-plugin.js.map +1 -0
- package/package.json +2 -2
- package/src/constant-doc-plugin.ts +114 -0
- package/src/headers-plugin.ts +1 -1
- package/src/index.ts +341 -5
- package/src/length-truthiness-plugin.ts +132 -0
- package/src/plugin-types.ts +23 -0
- package/src/plugin.ts +6 -0
- package/src/threshold-plugin.ts +93 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
* @file A prefer-length-truthiness rule, authored as an oxlint JS plugin (ESLint v9-compatible API).
|
|
6
|
+
* It is the house counterpart to `unicorn/explicit-length-check`, which enforces the opposite
|
|
7
|
+
* convention and is therefore off: `if (items.length)` reads better here than
|
|
8
|
+
* `if (items.length > 0)`.
|
|
9
|
+
*
|
|
10
|
+
* The rule only fires where the value is ALREADY coerced to a boolean — a condition, a ternary
|
|
11
|
+
* test, or the operand of `!` — including through `&&`/`||` nested inside one. Outside those
|
|
12
|
+
* positions the comparison is the value itself, and rewriting `const hasItems = items.length > 0`
|
|
13
|
+
* would silently change its type from boolean to number.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { AstNode, Fixer, Rule, RuleContext } from "./plugin-types.js"
|
|
17
|
+
|
|
18
|
+
/** Comparisons meaning "non-empty", which become the bare length. */
|
|
19
|
+
const TRUTHY_FORMS = new Set(["> 0", "!== 0", "!= 0", ">= 1"])
|
|
20
|
+
|
|
21
|
+
/** Comparisons meaning "empty", which become a negated length. */
|
|
22
|
+
const FALSY_FORMS = new Set(["=== 0", "== 0", "< 1"])
|
|
23
|
+
|
|
24
|
+
/** Members whose length-ness the rule understands. */
|
|
25
|
+
const LENGTH_PROPERTIES = new Set(["length", "size"])
|
|
26
|
+
|
|
27
|
+
/** The comparison rendered as `<operator> <literal>`, or null when it is not a length comparison. */
|
|
28
|
+
function classify(node: AstNode): { member: AstNode; negate: boolean } | null {
|
|
29
|
+
if (node.type !== "BinaryExpression" || !node.operator || !node.left || !node.right) return null
|
|
30
|
+
|
|
31
|
+
// Accept both `x.length > 0` and the flipped `0 < x.length`.
|
|
32
|
+
const flipped: Record<string, string> = { "<": ">", ">": "<", "<=": ">=", ">=": "<=" }
|
|
33
|
+
let { left, right, operator } = { left: node.left, right: node.right, operator: node.operator }
|
|
34
|
+
|
|
35
|
+
if (left.type === "Literal" || left.type === "NumericLiteral") {
|
|
36
|
+
;[left, right] = [right, left]
|
|
37
|
+
operator = flipped[operator] ?? operator
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (left.type !== "MemberExpression" && left.type !== "StaticMemberExpression") return null
|
|
41
|
+
|
|
42
|
+
const property = (left as AstNode & { property?: AstNode }).property
|
|
43
|
+
|
|
44
|
+
if (!property || property.type !== "Identifier" || !LENGTH_PROPERTIES.has(property.name ?? "")) return null
|
|
45
|
+
|
|
46
|
+
if (right.type !== "Literal" && right.type !== "NumericLiteral") return null
|
|
47
|
+
|
|
48
|
+
if (typeof right.value !== "number") return null
|
|
49
|
+
|
|
50
|
+
const form = `${operator} ${right.value}`
|
|
51
|
+
|
|
52
|
+
if (TRUTHY_FORMS.has(form)) return { member: left, negate: false }
|
|
53
|
+
|
|
54
|
+
if (FALSY_FORMS.has(form)) return { member: left, negate: true }
|
|
55
|
+
|
|
56
|
+
return null
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const preferLengthTruthinessRule: Rule = {
|
|
60
|
+
meta: {
|
|
61
|
+
name: "prefer-length-truthiness",
|
|
62
|
+
type: "suggestion",
|
|
63
|
+
fixable: "code",
|
|
64
|
+
schema: [{ type: "object", additionalProperties: true }],
|
|
65
|
+
},
|
|
66
|
+
create(context: RuleContext) {
|
|
67
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode!()
|
|
68
|
+
const text = sourceCode.getText()
|
|
69
|
+
|
|
70
|
+
function report(node: AstNode) {
|
|
71
|
+
const hit = classify(node)
|
|
72
|
+
|
|
73
|
+
if (!hit) return
|
|
74
|
+
|
|
75
|
+
const member = text.slice(hit.member.range[0], hit.member.range[1])
|
|
76
|
+
const replacement = hit.negate ? `!${member}` : member
|
|
77
|
+
|
|
78
|
+
context.report({
|
|
79
|
+
node,
|
|
80
|
+
message: `Prefer \`${replacement}\` over an explicit length comparison — the house convention is truthiness.`,
|
|
81
|
+
fix(fixer: Fixer) {
|
|
82
|
+
return fixer.replaceTextRange(node.range, replacement)
|
|
83
|
+
},
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Walk into a boolean context: logical operands and `!` arguments stay boolean. */
|
|
88
|
+
function visitCondition(node: AstNode | null | undefined) {
|
|
89
|
+
if (!node) return
|
|
90
|
+
|
|
91
|
+
if (node.type === "LogicalExpression") {
|
|
92
|
+
visitCondition(node.left)
|
|
93
|
+
visitCondition(node.right)
|
|
94
|
+
|
|
95
|
+
return
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (node.type === "UnaryExpression" && node.operator === "!") {
|
|
99
|
+
visitCondition(node.argument)
|
|
100
|
+
|
|
101
|
+
return
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
report(node)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return {
|
|
108
|
+
IfStatement(node) {
|
|
109
|
+
visitCondition(node.test)
|
|
110
|
+
},
|
|
111
|
+
WhileStatement(node) {
|
|
112
|
+
visitCondition(node.test)
|
|
113
|
+
},
|
|
114
|
+
DoWhileStatement(node) {
|
|
115
|
+
visitCondition(node.test)
|
|
116
|
+
},
|
|
117
|
+
ForStatement(node) {
|
|
118
|
+
visitCondition(node.test)
|
|
119
|
+
},
|
|
120
|
+
ConditionalExpression(node) {
|
|
121
|
+
visitCondition(node.test)
|
|
122
|
+
},
|
|
123
|
+
UnaryExpression(node) {
|
|
124
|
+
if (node.operator === "!") {
|
|
125
|
+
visitCondition(node.argument)
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export default preferLengthTruthinessRule
|
package/src/plugin-types.ts
CHANGED
|
@@ -22,6 +22,29 @@ export interface AstNode {
|
|
|
22
22
|
/** `if`/`else` branches, for the require-braces rule. */
|
|
23
23
|
consequent?: AstNode
|
|
24
24
|
alternate?: AstNode | null
|
|
25
|
+
/** Binary/unary operator text, for the threshold rule. */
|
|
26
|
+
operator?: string
|
|
27
|
+
/** Binary-expression operands. */
|
|
28
|
+
left?: AstNode
|
|
29
|
+
right?: AstNode
|
|
30
|
+
/** Unary-expression operand. */
|
|
31
|
+
argument?: AstNode
|
|
32
|
+
/** A literal's value, and its verbatim source text (`raw` preserves a `0x` prefix). */
|
|
33
|
+
value?: unknown
|
|
34
|
+
raw?: string
|
|
35
|
+
/** `const` / `let` / `var`, for the constant-doc rule. */
|
|
36
|
+
kind?: string
|
|
37
|
+
/** Declarators of a variable declaration. */
|
|
38
|
+
declarations?: AstNode[]
|
|
39
|
+
/** A declarator's binding identifier and initializer. */
|
|
40
|
+
id?: AstNode
|
|
41
|
+
init?: AstNode | null
|
|
42
|
+
/** An identifier's name. */
|
|
43
|
+
name?: string
|
|
44
|
+
/** The condition of an `if`/`while`/`for`/ternary, for the length-truthiness rule. */
|
|
45
|
+
test?: AstNode | null
|
|
46
|
+
/** A member expression's accessed property. */
|
|
47
|
+
property?: AstNode
|
|
25
48
|
}
|
|
26
49
|
|
|
27
50
|
export interface SourceCode {
|
package/src/plugin.ts
CHANGED
|
@@ -7,10 +7,13 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { bracesRule } from "./braces-plugin.js"
|
|
10
|
+
import { requireConstantDocRule } from "./constant-doc-plugin.js"
|
|
10
11
|
import { headerRule } from "./headers-plugin.js"
|
|
12
|
+
import { preferLengthTruthinessRule } from "./length-truthiness-plugin.js"
|
|
11
13
|
import { paddingRule } from "./padding-plugin.js"
|
|
12
14
|
import type { Plugin } from "./plugin-types.js"
|
|
13
15
|
import { noProcessGlobalsRule } from "./process-globals-plugin.js"
|
|
16
|
+
import { noUnnamedThresholdRule } from "./threshold-plugin.js"
|
|
14
17
|
|
|
15
18
|
const sisterSoftwarePlugin: Plugin = {
|
|
16
19
|
meta: { name: "sister-software" },
|
|
@@ -19,6 +22,9 @@ const sisterSoftwarePlugin: Plugin = {
|
|
|
19
22
|
"padding-lines": paddingRule,
|
|
20
23
|
"require-braces": bracesRule,
|
|
21
24
|
"no-process-globals": noProcessGlobalsRule,
|
|
25
|
+
"no-unnamed-threshold": noUnnamedThresholdRule,
|
|
26
|
+
"require-constant-doc": requireConstantDocRule,
|
|
27
|
+
"prefer-length-truthiness": preferLengthTruthinessRule,
|
|
22
28
|
},
|
|
23
29
|
}
|
|
24
30
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @copyright Sister Software
|
|
3
|
+
* @license AGPL-3.0
|
|
4
|
+
* @author Teffen Ellis, et al.
|
|
5
|
+
* @file A no-unnamed-threshold rule, authored as an oxlint JS plugin (ESLint v9-compatible API). It
|
|
6
|
+
* flags a numeric literal used as a threshold — an operand of a comparison — and nothing else.
|
|
7
|
+
* Numbers inside array and object literals are left alone, because a bounding box or a codepoint
|
|
8
|
+
* table is data rather than a tuning knob. That distinction is why this exists instead of
|
|
9
|
+
* `no-magic-numbers`, which cannot express it.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { AstNode, Rule } from "./plugin-types.js"
|
|
13
|
+
|
|
14
|
+
/** Operators whose operands read as thresholds. */
|
|
15
|
+
const COMPARISON_OPERATORS = new Set(["<", ">", "<=", ">=", "===", "!==", "==", "!="])
|
|
16
|
+
|
|
17
|
+
/** Numbers so conventional that naming them costs more clarity than it buys. */
|
|
18
|
+
const DEFAULT_IGNORE = [-1, 0, 1, 2, 0.5, 10, 100, 1000]
|
|
19
|
+
|
|
20
|
+
/** Radix-prefixed literals: `0x1f`, `0b1010`, `0o777`. */
|
|
21
|
+
const RADIX_PREFIXED = /^0[xXbBoO]/
|
|
22
|
+
|
|
23
|
+
/** Options accepted by {@link noUnnamedThresholdRule}. */
|
|
24
|
+
export interface ThresholdOptions {
|
|
25
|
+
/** Values that may appear unnamed. Replaces the default list rather than extending it. */
|
|
26
|
+
ignore?: number[]
|
|
27
|
+
/** Exempt radix-prefixed literals, which already read as codepoints or bit masks. */
|
|
28
|
+
allowHex?: boolean
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const noUnnamedThresholdRule: Rule = {
|
|
32
|
+
meta: {
|
|
33
|
+
name: "no-unnamed-threshold",
|
|
34
|
+
type: "suggestion",
|
|
35
|
+
schema: [{ type: "object", additionalProperties: true }],
|
|
36
|
+
},
|
|
37
|
+
create(context) {
|
|
38
|
+
const options = (context.options[0] ?? {}) as ThresholdOptions
|
|
39
|
+
const ignore = new Set(options.ignore ?? DEFAULT_IGNORE)
|
|
40
|
+
const allowHex = options.allowHex ?? true
|
|
41
|
+
|
|
42
|
+
function check(operand: AstNode | undefined) {
|
|
43
|
+
if (!operand) return
|
|
44
|
+
|
|
45
|
+
let literal = operand
|
|
46
|
+
let sign = 1
|
|
47
|
+
|
|
48
|
+
// `x < -273.15` parses as a unary minus wrapping the literal.
|
|
49
|
+
if (operand.type === "UnaryExpression") {
|
|
50
|
+
if (operand.operator !== "-" && operand.operator !== "+") return
|
|
51
|
+
|
|
52
|
+
if (!operand.argument) return
|
|
53
|
+
|
|
54
|
+
sign = operand.operator === "-" ? -1 : 1
|
|
55
|
+
literal = operand.argument
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// oxlint's AST uses ESTree `Literal` in some positions and Babel-style `NumericLiteral` in
|
|
59
|
+
// others, so both are accepted.
|
|
60
|
+
if (literal.type !== "Literal" && literal.type !== "NumericLiteral") return
|
|
61
|
+
|
|
62
|
+
if (typeof literal.value !== "number") return
|
|
63
|
+
|
|
64
|
+
const raw = literal.raw ?? String(literal.value)
|
|
65
|
+
|
|
66
|
+
// `cp >= 0x3040` already reads as a codepoint boundary; a name adds nothing.
|
|
67
|
+
if (allowHex && RADIX_PREFIXED.test(raw)) return
|
|
68
|
+
|
|
69
|
+
if (ignore.has(sign * literal.value)) return
|
|
70
|
+
|
|
71
|
+
// `raw` is the literal's own text, so a negated value reads as `273.15` without this.
|
|
72
|
+
const shown = sign === -1 ? `-${raw}` : raw
|
|
73
|
+
|
|
74
|
+
context.report({
|
|
75
|
+
node: literal,
|
|
76
|
+
message:
|
|
77
|
+
`Unnamed threshold \`${shown}\` — extract it to a documented named constant so a reader can tell ` +
|
|
78
|
+
`what it means and where the value came from.`,
|
|
79
|
+
})
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
BinaryExpression(node) {
|
|
84
|
+
if (!node.operator || !COMPARISON_OPERATORS.has(node.operator)) return
|
|
85
|
+
|
|
86
|
+
check(node.left)
|
|
87
|
+
check(node.right)
|
|
88
|
+
},
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default noUnnamedThresholdRule
|