@blumintinc/eslint-plugin-blumint 1.20.66 → 1.20.68
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/lib/index.js +1 -1
- package/lib/rules/enforce-microdiff.js +5 -0
- package/lib/rules/memo-nested-react-components.js +6 -2
- package/lib/rules/no-compositing-layer-props.js +4 -0
- package/lib/rules/no-jsx-in-hooks.js +5 -2
- package/lib/rules/optimize-object-boolean-conditions.d.ts +2 -1
- package/lib/rules/optimize-object-boolean-conditions.js +310 -35
- package/lib/rules/prefer-getter-over-parameterless-method.js +1 -1
- package/lib/rules/prefer-sx-prop-over-system-props.d.ts +1 -0
- package/lib/rules/prefer-sx-prop-over-system-props.js +499 -82
- package/lib/rules/prefer-union-from-const-array.d.ts +6 -1
- package/lib/rules/prefer-union-from-const-array.js +114 -9
- package/lib/utils/ruleTester.d.ts +28 -0
- package/lib/utils/ruleTester.js +49 -1
- package/package.json +1 -1
- package/release-manifest.json +85 -0
|
@@ -3,6 +3,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.preferUnionFromConstArray = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const createRule_1 = require("../utils/createRule");
|
|
6
|
+
/**
|
|
7
|
+
* Matches Prettier's own default. The autofix authors a whole statement that a
|
|
8
|
+
* formatter owns, so a line it emits past this width is rewritten on the next
|
|
9
|
+
* `prettier --write` — and fails `prettier --check` in the meantime.
|
|
10
|
+
*/
|
|
11
|
+
const DEFAULT_PRINT_WIDTH = 80;
|
|
6
12
|
/**
|
|
7
13
|
* Mechanically derive an UPPER_SNAKE constant base from a PascalCase/camelCase
|
|
8
14
|
* type name by inserting `_` at every lowercase/digit-to-uppercase boundary and
|
|
@@ -43,6 +49,53 @@ function isNameInScope(scope, name) {
|
|
|
43
49
|
}
|
|
44
50
|
return false;
|
|
45
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* The file's own nesting step, taken as the most common indentation increase
|
|
54
|
+
* between consecutive lines. Reading it from the source keeps emitted code in
|
|
55
|
+
* the author's units instead of assuming a two-space, space-indented file.
|
|
56
|
+
*/
|
|
57
|
+
function indentUnitOf(sourceCode) {
|
|
58
|
+
const text = sourceCode.getText();
|
|
59
|
+
const blockComments = sourceCode
|
|
60
|
+
.getAllComments()
|
|
61
|
+
.filter((comment) => comment.type === utils_1.AST_TOKEN_TYPES.Block)
|
|
62
|
+
.map((comment) => comment.range);
|
|
63
|
+
// A block comment's interior lines carry whatever alignment the comment uses
|
|
64
|
+
// — the `*` one column in from its own indentation, or, for commented-out
|
|
65
|
+
// code, the original code's depths. Neither is a nesting step of the file, and
|
|
66
|
+
// counting them makes a JSDoc-heavy file look 1-space indented. Keying on the
|
|
67
|
+
// comment's range rather than a leading `*` also covers a body without them.
|
|
68
|
+
const continuesBlockComment = (offset) => blockComments.some(([start, end]) => start < offset && offset < end);
|
|
69
|
+
const frequencies = new Map();
|
|
70
|
+
let previous = '';
|
|
71
|
+
let offset = 0;
|
|
72
|
+
for (const line of text.split('\n')) {
|
|
73
|
+
const lineStart = offset;
|
|
74
|
+
offset += line.length + 1;
|
|
75
|
+
if (line.trim() === '') {
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (continuesBlockComment(lineStart)) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
const match = /^[ \t]*/.exec(line);
|
|
82
|
+
const indent = match ? match[0] : '';
|
|
83
|
+
if (indent.length > previous.length && indent.startsWith(previous)) {
|
|
84
|
+
const delta = indent.slice(previous.length);
|
|
85
|
+
frequencies.set(delta, (frequencies.get(delta) ?? 0) + 1);
|
|
86
|
+
}
|
|
87
|
+
previous = indent;
|
|
88
|
+
}
|
|
89
|
+
let unit = ' ';
|
|
90
|
+
let best = 0;
|
|
91
|
+
for (const [delta, count] of frequencies) {
|
|
92
|
+
if (count > best) {
|
|
93
|
+
unit = delta;
|
|
94
|
+
best = count;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return unit;
|
|
98
|
+
}
|
|
46
99
|
exports.preferUnionFromConstArray = (0, createRule_1.createRule)({
|
|
47
100
|
name: 'prefer-union-from-const-array',
|
|
48
101
|
meta: {
|
|
@@ -52,14 +105,37 @@ exports.preferUnionFromConstArray = (0, createRule_1.createRule)({
|
|
|
52
105
|
recommended: 'error',
|
|
53
106
|
},
|
|
54
107
|
fixable: 'code',
|
|
55
|
-
schema: [
|
|
108
|
+
schema: [
|
|
109
|
+
{
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: {
|
|
112
|
+
printWidth: {
|
|
113
|
+
type: 'number',
|
|
114
|
+
minimum: 1,
|
|
115
|
+
},
|
|
116
|
+
},
|
|
117
|
+
additionalProperties: false,
|
|
118
|
+
},
|
|
119
|
+
],
|
|
56
120
|
messages: {
|
|
57
121
|
preferDerivedUnion: 'Derive this union from an `as const` array: `const {{constName}} = [...] as const; type {{typeName}} = (typeof {{constName}})[number];`. This tethers the type to importable runtime values and prevents silent drift. Consider also naming each literal as its own exported constant so call sites never repeat magic strings.',
|
|
58
122
|
},
|
|
59
123
|
},
|
|
60
|
-
defaultOptions: [],
|
|
61
|
-
create(context) {
|
|
124
|
+
defaultOptions: [{}],
|
|
125
|
+
create(context, [options]) {
|
|
62
126
|
const sourceCode = context.getSourceCode();
|
|
127
|
+
const printWidth = typeof options.printWidth === 'number' && options.printWidth > 0
|
|
128
|
+
? options.printWidth
|
|
129
|
+
: DEFAULT_PRINT_WIDTH;
|
|
130
|
+
// Derived once per file rather than per fix: every fix in a file shares the
|
|
131
|
+
// author's nesting step.
|
|
132
|
+
let indentUnit = null;
|
|
133
|
+
const fileIndentUnit = () => {
|
|
134
|
+
if (indentUnit === null) {
|
|
135
|
+
indentUnit = indentUnitOf(sourceCode);
|
|
136
|
+
}
|
|
137
|
+
return indentUnit;
|
|
138
|
+
};
|
|
63
139
|
return {
|
|
64
140
|
TSTypeAliasDeclaration(node) {
|
|
65
141
|
const { typeAnnotation } = node;
|
|
@@ -103,12 +179,10 @@ exports.preferUnionFromConstArray = (0, createRule_1.createRule)({
|
|
|
103
179
|
messageId: 'preferDerivedUnion',
|
|
104
180
|
data: { constName, typeName },
|
|
105
181
|
fix(fixer) {
|
|
106
|
-
const literals = members
|
|
107
|
-
.map((member) => {
|
|
182
|
+
const literals = members.map((member) => {
|
|
108
183
|
const literal = member.literal;
|
|
109
184
|
return literal.raw ?? sourceCode.getText(literal);
|
|
110
|
-
})
|
|
111
|
-
.join(', ');
|
|
185
|
+
});
|
|
112
186
|
const typeParams = node.typeParameters
|
|
113
187
|
? sourceCode.getText(node.typeParameters)
|
|
114
188
|
: '';
|
|
@@ -124,8 +198,39 @@ exports.preferUnionFromConstArray = (0, createRule_1.createRule)({
|
|
|
124
198
|
.slice(0, fixNode.loc.start.column)
|
|
125
199
|
.match(/^\s*/);
|
|
126
200
|
const indent = indentMatch ? indentMatch[0] : '';
|
|
127
|
-
|
|
128
|
-
|
|
201
|
+
// A line break the fixer does not add is one Prettier adds later,
|
|
202
|
+
// so the fix must decide the same way Prettier does: keep the
|
|
203
|
+
// statement on one line while it fits, break it open past the print
|
|
204
|
+
// width. Over-wrapping is not the safe direction here — unlike an
|
|
205
|
+
// object literal, an array Prettier considers short enough is
|
|
206
|
+
// collapsed back onto one line, so a blanket wrap would trade this
|
|
207
|
+
// defect for its mirror image on every short union.
|
|
208
|
+
//
|
|
209
|
+
// The emitted const starts where the replaced node starts, so its
|
|
210
|
+
// column is the exact prefix length; the derived alias is emitted on
|
|
211
|
+
// a fresh line at `indent`.
|
|
212
|
+
const constStartColumn = fixNode.loc.start.column;
|
|
213
|
+
const inlineConst = `${constKeyword} ${constName} = [${literals.join(', ')}] as const;`;
|
|
214
|
+
// A literal whose raw text spans lines (a string written with a
|
|
215
|
+
// backslash line continuation) cannot sit on one line at all, so
|
|
216
|
+
// there is no width to compare — such an array always breaks open.
|
|
217
|
+
// Its raw text is emitted verbatim, leaving the interior line break
|
|
218
|
+
// and everything after it exactly where the author put it: that
|
|
219
|
+
// whitespace is string DATA, not layout.
|
|
220
|
+
const hasMultilineLiteral = literals.some((literal) => /[\r\n]/.test(literal));
|
|
221
|
+
const constDecl = !hasMultilineLiteral &&
|
|
222
|
+
constStartColumn + inlineConst.length <= printWidth
|
|
223
|
+
? inlineConst
|
|
224
|
+
: `${constKeyword} ${constName} = [\n${literals
|
|
225
|
+
.map((literal) => `${indent}${fileIndentUnit()}${literal},`)
|
|
226
|
+
.join('\n')}\n${indent}] as const;`;
|
|
227
|
+
const inlineType = `${typeKeyword} ${typeName}${typeParams} = (typeof ${constName})[number];`;
|
|
228
|
+
// Prettier answers an over-long type alias by breaking after the
|
|
229
|
+
// `=`, the only break point the derived form has.
|
|
230
|
+
const typeDecl = typeParams.includes('\n') ||
|
|
231
|
+
indent.length + inlineType.length <= printWidth
|
|
232
|
+
? inlineType
|
|
233
|
+
: `${typeKeyword} ${typeName}${typeParams} =\n${indent}${fileIndentUnit()}(typeof ${constName})[number];`;
|
|
129
234
|
return fixer.replaceText(fixNode, `${constDecl}\n${indent}${typeDecl}`);
|
|
130
235
|
},
|
|
131
236
|
});
|
|
@@ -1,6 +1,34 @@
|
|
|
1
1
|
import { ESLintUtils } from '@typescript-eslint/utils';
|
|
2
|
+
import type { TSESLint } from '@typescript-eslint/utils';
|
|
2
3
|
import { RuleTester } from 'eslint';
|
|
3
4
|
export declare const ruleTesterTs: ESLintUtils.RuleTester;
|
|
4
5
|
export declare const ruleTesterJsx: ESLintUtils.RuleTester;
|
|
5
6
|
export declare const ruleTesterJson: RuleTester;
|
|
6
7
|
export declare const ruleTesterMarkdown: RuleTester;
|
|
8
|
+
type ParserOptionsCarrier = {
|
|
9
|
+
parserOptions?: TSESLint.ParserOptions;
|
|
10
|
+
};
|
|
11
|
+
/**
|
|
12
|
+
* Applies `parserOptions` to every case of a suite.
|
|
13
|
+
*
|
|
14
|
+
* The instances above are the only RuleTester instances the suite may use:
|
|
15
|
+
* corpus-harvesting probes collect cases by stubbing `run` on these exports, so
|
|
16
|
+
* a test file that constructs its own tester is invisible to every one of them
|
|
17
|
+
* and its rule silently drops out of the fixer, convergence, collision and
|
|
18
|
+
* crash sweeps. `src/tests/no-local-rule-tester.test.ts` enforces that.
|
|
19
|
+
*
|
|
20
|
+
* A file needing parser options the shared instances do not declare — a
|
|
21
|
+
* type-aware `project`, a non-default `ecmaVersion` — attaches them per case
|
|
22
|
+
* rather than forking a tester. The shared instances already declare ES module
|
|
23
|
+
* scope analysis, so a case restating `sourceType: 'module'` is redundant but
|
|
24
|
+
* harmless. RuleTester deep merges each case's config over the tester's,
|
|
25
|
+
* and options a case declares itself still win over the ones applied here, so a
|
|
26
|
+
* case keeps any override it already had. Carrying the options on the case also
|
|
27
|
+
* means the harvested corpus records the parser configuration a snippet was
|
|
28
|
+
* written for, which a tester-level setting never exposes to a probe.
|
|
29
|
+
*/
|
|
30
|
+
export declare const withParserOptions: <T extends string | ParserOptionsCarrier>(parserOptions: TSESLint.ParserOptions, cases: readonly T[]) => (Exclude<T, string> | {
|
|
31
|
+
code: string;
|
|
32
|
+
parserOptions: TSESLint.ParserOptions;
|
|
33
|
+
})[];
|
|
34
|
+
export {};
|
package/lib/utils/ruleTester.js
CHANGED
|
@@ -1,14 +1,33 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ruleTesterMarkdown = exports.ruleTesterJson = exports.ruleTesterJsx = exports.ruleTesterTs = void 0;
|
|
3
|
+
exports.withParserOptions = exports.ruleTesterMarkdown = exports.ruleTesterJson = exports.ruleTesterJsx = exports.ruleTesterTs = void 0;
|
|
4
4
|
const utils_1 = require("@typescript-eslint/utils");
|
|
5
5
|
const eslint_1 = require("eslint");
|
|
6
|
+
/**
|
|
7
|
+
* `sourceType: 'module'` matches how the linted codebase actually parses: every
|
|
8
|
+
* consumer source file is an ES module, so its top-level declarations live in a
|
|
9
|
+
* module scope nested inside the global one.
|
|
10
|
+
*
|
|
11
|
+
* `@typescript-eslint/parser` defaults to `'script'`, under which a top-level
|
|
12
|
+
* `import`/`const` still binds — the binding hangs off the *global* scope rather
|
|
13
|
+
* than a module scope. References therefore still resolve either way, but the
|
|
14
|
+
* scope shape differs: under `'script'` `globalScope.variables` carries the
|
|
15
|
+
* file's own declarations and there is no module scope at all. A rule that
|
|
16
|
+
* discriminates on `scope.type`, walks `globalScope.childScopes`, or reads
|
|
17
|
+
* `globalScope.variables` would otherwise be exercised against a shape no real
|
|
18
|
+
* file has. `src/tests/rule-tester-parse-mode.test.ts` pins this.
|
|
19
|
+
*/
|
|
20
|
+
const SHARED_PARSER_OPTIONS = {
|
|
21
|
+
sourceType: 'module',
|
|
22
|
+
};
|
|
6
23
|
exports.ruleTesterTs = new utils_1.ESLintUtils.RuleTester({
|
|
7
24
|
parser: '@typescript-eslint/parser',
|
|
25
|
+
parserOptions: { ...SHARED_PARSER_OPTIONS },
|
|
8
26
|
});
|
|
9
27
|
exports.ruleTesterJsx = new utils_1.ESLintUtils.RuleTester({
|
|
10
28
|
parser: '@typescript-eslint/parser',
|
|
11
29
|
parserOptions: {
|
|
30
|
+
...SHARED_PARSER_OPTIONS,
|
|
12
31
|
ecmaFeatures: {
|
|
13
32
|
jsx: true,
|
|
14
33
|
},
|
|
@@ -23,4 +42,33 @@ exports.ruleTesterJson = new eslint_1.RuleTester({
|
|
|
23
42
|
exports.ruleTesterMarkdown = new eslint_1.RuleTester({
|
|
24
43
|
parser: require.resolve('markdown-eslint-parser'),
|
|
25
44
|
});
|
|
45
|
+
/**
|
|
46
|
+
* Applies `parserOptions` to every case of a suite.
|
|
47
|
+
*
|
|
48
|
+
* The instances above are the only RuleTester instances the suite may use:
|
|
49
|
+
* corpus-harvesting probes collect cases by stubbing `run` on these exports, so
|
|
50
|
+
* a test file that constructs its own tester is invisible to every one of them
|
|
51
|
+
* and its rule silently drops out of the fixer, convergence, collision and
|
|
52
|
+
* crash sweeps. `src/tests/no-local-rule-tester.test.ts` enforces that.
|
|
53
|
+
*
|
|
54
|
+
* A file needing parser options the shared instances do not declare — a
|
|
55
|
+
* type-aware `project`, a non-default `ecmaVersion` — attaches them per case
|
|
56
|
+
* rather than forking a tester. The shared instances already declare ES module
|
|
57
|
+
* scope analysis, so a case restating `sourceType: 'module'` is redundant but
|
|
58
|
+
* harmless. RuleTester deep merges each case's config over the tester's,
|
|
59
|
+
* and options a case declares itself still win over the ones applied here, so a
|
|
60
|
+
* case keeps any override it already had. Carrying the options on the case also
|
|
61
|
+
* means the harvested corpus records the parser configuration a snippet was
|
|
62
|
+
* written for, which a tester-level setting never exposes to a probe.
|
|
63
|
+
*/
|
|
64
|
+
const withParserOptions = (parserOptions, cases) => cases.map((testCase) => typeof testCase === 'string'
|
|
65
|
+
? { code: testCase, parserOptions }
|
|
66
|
+
: {
|
|
67
|
+
...testCase,
|
|
68
|
+
parserOptions: {
|
|
69
|
+
...parserOptions,
|
|
70
|
+
...testCase.parserOptions,
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
exports.withParserOptions = withParserOptions;
|
|
26
74
|
//# sourceMappingURL=ruleTester.js.map
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,89 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.68",
|
|
4
|
+
"date": "2026-08-01T18:03:14.950Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "prefer-union-from-const-array",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1577
|
|
11
|
+
],
|
|
12
|
+
"summary": "exclude block comments by range when inferring the indent unit (closes #1577)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.20.67",
|
|
18
|
+
"date": "2026-08-01T17:17:30.817Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "enforce-microdiff",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
1571
|
|
25
|
+
],
|
|
26
|
+
"summary": "guard the zero-argument JSON.stringify comparison (closes #1571)"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "memo-nested-react-components",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
1567
|
|
33
|
+
],
|
|
34
|
+
"summary": "exempt lowercase-initial render-prop callbacks (closes #1567)"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "no-compositing-layer-props",
|
|
38
|
+
"changeType": "fix",
|
|
39
|
+
"issues": [
|
|
40
|
+
1570
|
|
41
|
+
],
|
|
42
|
+
"summary": "exempt mix-blend-mode's non-promoting values (closes #1570)"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "no-jsx-in-hooks",
|
|
46
|
+
"changeType": "fix",
|
|
47
|
+
"issues": [
|
|
48
|
+
1572
|
|
49
|
+
],
|
|
50
|
+
"summary": "guard the zero-argument .map() inside useMemo (closes #1572)"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"name": "optimize-object-boolean-conditions",
|
|
54
|
+
"changeType": "fix",
|
|
55
|
+
"issues": [
|
|
56
|
+
1569
|
|
57
|
+
],
|
|
58
|
+
"summary": "stop calling boolean-prefixed and primitive identifiers objects (closes #1569)"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"name": "prefer-getter-over-parameterless-method",
|
|
62
|
+
"changeType": "fix",
|
|
63
|
+
"issues": [
|
|
64
|
+
1568
|
|
65
|
+
],
|
|
66
|
+
"summary": "prescribe a remedy that clears preferGetterSideEffect (closes #1568)"
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"name": "prefer-sx-prop-over-system-props",
|
|
70
|
+
"changeType": "fix",
|
|
71
|
+
"issues": [
|
|
72
|
+
1565,
|
|
73
|
+
1574
|
|
74
|
+
],
|
|
75
|
+
"summary": "ignore block-comment lines when inferring the indent unit (closes #1574); wrap emitted sx object when it overflows the print width (closes #1565)"
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
"name": "prefer-union-from-const-array",
|
|
79
|
+
"changeType": "fix",
|
|
80
|
+
"issues": [
|
|
81
|
+
1566
|
|
82
|
+
],
|
|
83
|
+
"summary": "wrap the authored as-const array when it exceeds the print width (closes #1566)"
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
},
|
|
2
87
|
{
|
|
3
88
|
"version": "1.20.66",
|
|
4
89
|
"date": "2026-08-01T13:53:31.680Z",
|