@blumintinc/eslint-plugin-blumint 1.20.65 → 1.20.67
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-firestore-doc-ref-generic.js +59 -7
- 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 +103 -9
- package/lib/utils/ruleTester.d.ts +26 -0
- package/lib/utils/ruleTester.js +28 -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,42 @@ 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(text) {
|
|
58
|
+
const frequencies = new Map();
|
|
59
|
+
let previous = '';
|
|
60
|
+
for (const line of text.split('\n')) {
|
|
61
|
+
if (line.trim() === '') {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
// A block comment's continuation lines align on the `*` one column in from
|
|
65
|
+
// the comment's own indentation. That is comment alignment, not a nesting
|
|
66
|
+
// step, and counting it makes any JSDoc-heavy file look 1-space indented.
|
|
67
|
+
if (line.trimStart().startsWith('*')) {
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
const match = /^[ \t]*/.exec(line);
|
|
71
|
+
const indent = match ? match[0] : '';
|
|
72
|
+
if (indent.length > previous.length && indent.startsWith(previous)) {
|
|
73
|
+
const delta = indent.slice(previous.length);
|
|
74
|
+
frequencies.set(delta, (frequencies.get(delta) ?? 0) + 1);
|
|
75
|
+
}
|
|
76
|
+
previous = indent;
|
|
77
|
+
}
|
|
78
|
+
let unit = ' ';
|
|
79
|
+
let best = 0;
|
|
80
|
+
for (const [delta, count] of frequencies) {
|
|
81
|
+
if (count > best) {
|
|
82
|
+
unit = delta;
|
|
83
|
+
best = count;
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return unit;
|
|
87
|
+
}
|
|
46
88
|
exports.preferUnionFromConstArray = (0, createRule_1.createRule)({
|
|
47
89
|
name: 'prefer-union-from-const-array',
|
|
48
90
|
meta: {
|
|
@@ -52,14 +94,37 @@ exports.preferUnionFromConstArray = (0, createRule_1.createRule)({
|
|
|
52
94
|
recommended: 'error',
|
|
53
95
|
},
|
|
54
96
|
fixable: 'code',
|
|
55
|
-
schema: [
|
|
97
|
+
schema: [
|
|
98
|
+
{
|
|
99
|
+
type: 'object',
|
|
100
|
+
properties: {
|
|
101
|
+
printWidth: {
|
|
102
|
+
type: 'number',
|
|
103
|
+
minimum: 1,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
additionalProperties: false,
|
|
107
|
+
},
|
|
108
|
+
],
|
|
56
109
|
messages: {
|
|
57
110
|
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
111
|
},
|
|
59
112
|
},
|
|
60
|
-
defaultOptions: [],
|
|
61
|
-
create(context) {
|
|
113
|
+
defaultOptions: [{}],
|
|
114
|
+
create(context, [options]) {
|
|
62
115
|
const sourceCode = context.getSourceCode();
|
|
116
|
+
const printWidth = typeof options.printWidth === 'number' && options.printWidth > 0
|
|
117
|
+
? options.printWidth
|
|
118
|
+
: DEFAULT_PRINT_WIDTH;
|
|
119
|
+
// Derived once per file rather than per fix: every fix in a file shares the
|
|
120
|
+
// author's nesting step.
|
|
121
|
+
let indentUnit = null;
|
|
122
|
+
const fileIndentUnit = () => {
|
|
123
|
+
if (indentUnit === null) {
|
|
124
|
+
indentUnit = indentUnitOf(sourceCode.getText());
|
|
125
|
+
}
|
|
126
|
+
return indentUnit;
|
|
127
|
+
};
|
|
63
128
|
return {
|
|
64
129
|
TSTypeAliasDeclaration(node) {
|
|
65
130
|
const { typeAnnotation } = node;
|
|
@@ -103,12 +168,10 @@ exports.preferUnionFromConstArray = (0, createRule_1.createRule)({
|
|
|
103
168
|
messageId: 'preferDerivedUnion',
|
|
104
169
|
data: { constName, typeName },
|
|
105
170
|
fix(fixer) {
|
|
106
|
-
const literals = members
|
|
107
|
-
.map((member) => {
|
|
171
|
+
const literals = members.map((member) => {
|
|
108
172
|
const literal = member.literal;
|
|
109
173
|
return literal.raw ?? sourceCode.getText(literal);
|
|
110
|
-
})
|
|
111
|
-
.join(', ');
|
|
174
|
+
});
|
|
112
175
|
const typeParams = node.typeParameters
|
|
113
176
|
? sourceCode.getText(node.typeParameters)
|
|
114
177
|
: '';
|
|
@@ -124,8 +187,39 @@ exports.preferUnionFromConstArray = (0, createRule_1.createRule)({
|
|
|
124
187
|
.slice(0, fixNode.loc.start.column)
|
|
125
188
|
.match(/^\s*/);
|
|
126
189
|
const indent = indentMatch ? indentMatch[0] : '';
|
|
127
|
-
|
|
128
|
-
|
|
190
|
+
// A line break the fixer does not add is one Prettier adds later,
|
|
191
|
+
// so the fix must decide the same way Prettier does: keep the
|
|
192
|
+
// statement on one line while it fits, break it open past the print
|
|
193
|
+
// width. Over-wrapping is not the safe direction here — unlike an
|
|
194
|
+
// object literal, an array Prettier considers short enough is
|
|
195
|
+
// collapsed back onto one line, so a blanket wrap would trade this
|
|
196
|
+
// defect for its mirror image on every short union.
|
|
197
|
+
//
|
|
198
|
+
// The emitted const starts where the replaced node starts, so its
|
|
199
|
+
// column is the exact prefix length; the derived alias is emitted on
|
|
200
|
+
// a fresh line at `indent`.
|
|
201
|
+
const constStartColumn = fixNode.loc.start.column;
|
|
202
|
+
const inlineConst = `${constKeyword} ${constName} = [${literals.join(', ')}] as const;`;
|
|
203
|
+
// A literal whose raw text spans lines (a string written with a
|
|
204
|
+
// backslash line continuation) cannot sit on one line at all, so
|
|
205
|
+
// there is no width to compare — such an array always breaks open.
|
|
206
|
+
// Its raw text is emitted verbatim, leaving the interior line break
|
|
207
|
+
// and everything after it exactly where the author put it: that
|
|
208
|
+
// whitespace is string DATA, not layout.
|
|
209
|
+
const hasMultilineLiteral = literals.some((literal) => /[\r\n]/.test(literal));
|
|
210
|
+
const constDecl = !hasMultilineLiteral &&
|
|
211
|
+
constStartColumn + inlineConst.length <= printWidth
|
|
212
|
+
? inlineConst
|
|
213
|
+
: `${constKeyword} ${constName} = [\n${literals
|
|
214
|
+
.map((literal) => `${indent}${fileIndentUnit()}${literal},`)
|
|
215
|
+
.join('\n')}\n${indent}] as const;`;
|
|
216
|
+
const inlineType = `${typeKeyword} ${typeName}${typeParams} = (typeof ${constName})[number];`;
|
|
217
|
+
// Prettier answers an over-long type alias by breaking after the
|
|
218
|
+
// `=`, the only break point the derived form has.
|
|
219
|
+
const typeDecl = typeParams.includes('\n') ||
|
|
220
|
+
indent.length + inlineType.length <= printWidth
|
|
221
|
+
? inlineType
|
|
222
|
+
: `${typeKeyword} ${typeName}${typeParams} =\n${indent}${fileIndentUnit()}(typeof ${constName})[number];`;
|
|
129
223
|
return fixer.replaceText(fixNode, `${constDecl}\n${indent}${typeDecl}`);
|
|
130
224
|
},
|
|
131
225
|
});
|
|
@@ -1,6 +1,32 @@
|
|
|
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 — ES module
|
|
21
|
+
* scope analysis, a type-aware `project` — attaches them per case rather than
|
|
22
|
+
* forking a tester. RuleTester deep merges each case's config over the tester's,
|
|
23
|
+
* and options a case declares itself still win over the ones applied here, so a
|
|
24
|
+
* case keeps any override it already had. Carrying the options on the case also
|
|
25
|
+
* means the harvested corpus records the parser configuration a snippet was
|
|
26
|
+
* written for, which a tester-level setting never exposes to a probe.
|
|
27
|
+
*/
|
|
28
|
+
export declare const withParserOptions: <T extends string | ParserOptionsCarrier>(parserOptions: TSESLint.ParserOptions, cases: readonly T[]) => (Exclude<T, string> | {
|
|
29
|
+
code: string;
|
|
30
|
+
parserOptions: TSESLint.ParserOptions;
|
|
31
|
+
})[];
|
|
32
|
+
export {};
|
package/lib/utils/ruleTester.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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
6
|
exports.ruleTesterTs = new utils_1.ESLintUtils.RuleTester({
|
|
@@ -23,4 +23,31 @@ exports.ruleTesterJson = new eslint_1.RuleTester({
|
|
|
23
23
|
exports.ruleTesterMarkdown = new eslint_1.RuleTester({
|
|
24
24
|
parser: require.resolve('markdown-eslint-parser'),
|
|
25
25
|
});
|
|
26
|
+
/**
|
|
27
|
+
* Applies `parserOptions` to every case of a suite.
|
|
28
|
+
*
|
|
29
|
+
* The instances above are the only RuleTester instances the suite may use:
|
|
30
|
+
* corpus-harvesting probes collect cases by stubbing `run` on these exports, so
|
|
31
|
+
* a test file that constructs its own tester is invisible to every one of them
|
|
32
|
+
* and its rule silently drops out of the fixer, convergence, collision and
|
|
33
|
+
* crash sweeps. `src/tests/no-local-rule-tester.test.ts` enforces that.
|
|
34
|
+
*
|
|
35
|
+
* A file needing parser options the shared instances do not declare — ES module
|
|
36
|
+
* scope analysis, a type-aware `project` — attaches them per case rather than
|
|
37
|
+
* forking a tester. RuleTester deep merges each case's config over the tester's,
|
|
38
|
+
* and options a case declares itself still win over the ones applied here, so a
|
|
39
|
+
* case keeps any override it already had. Carrying the options on the case also
|
|
40
|
+
* means the harvested corpus records the parser configuration a snippet was
|
|
41
|
+
* written for, which a tester-level setting never exposes to a probe.
|
|
42
|
+
*/
|
|
43
|
+
const withParserOptions = (parserOptions, cases) => cases.map((testCase) => typeof testCase === 'string'
|
|
44
|
+
? { code: testCase, parserOptions }
|
|
45
|
+
: {
|
|
46
|
+
...testCase,
|
|
47
|
+
parserOptions: {
|
|
48
|
+
...parserOptions,
|
|
49
|
+
...testCase.parserOptions,
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
exports.withParserOptions = withParserOptions;
|
|
26
53
|
//# sourceMappingURL=ruleTester.js.map
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,89 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.67",
|
|
4
|
+
"date": "2026-08-01T17:17:30.817Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "enforce-microdiff",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1571
|
|
11
|
+
],
|
|
12
|
+
"summary": "guard the zero-argument JSON.stringify comparison (closes #1571)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "memo-nested-react-components",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
1567
|
|
19
|
+
],
|
|
20
|
+
"summary": "exempt lowercase-initial render-prop callbacks (closes #1567)"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"name": "no-compositing-layer-props",
|
|
24
|
+
"changeType": "fix",
|
|
25
|
+
"issues": [
|
|
26
|
+
1570
|
|
27
|
+
],
|
|
28
|
+
"summary": "exempt mix-blend-mode's non-promoting values (closes #1570)"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"name": "no-jsx-in-hooks",
|
|
32
|
+
"changeType": "fix",
|
|
33
|
+
"issues": [
|
|
34
|
+
1572
|
|
35
|
+
],
|
|
36
|
+
"summary": "guard the zero-argument .map() inside useMemo (closes #1572)"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "optimize-object-boolean-conditions",
|
|
40
|
+
"changeType": "fix",
|
|
41
|
+
"issues": [
|
|
42
|
+
1569
|
|
43
|
+
],
|
|
44
|
+
"summary": "stop calling boolean-prefixed and primitive identifiers objects (closes #1569)"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"name": "prefer-getter-over-parameterless-method",
|
|
48
|
+
"changeType": "fix",
|
|
49
|
+
"issues": [
|
|
50
|
+
1568
|
|
51
|
+
],
|
|
52
|
+
"summary": "prescribe a remedy that clears preferGetterSideEffect (closes #1568)"
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
"name": "prefer-sx-prop-over-system-props",
|
|
56
|
+
"changeType": "fix",
|
|
57
|
+
"issues": [
|
|
58
|
+
1565,
|
|
59
|
+
1574
|
|
60
|
+
],
|
|
61
|
+
"summary": "ignore block-comment lines when inferring the indent unit (closes #1574); wrap emitted sx object when it overflows the print width (closes #1565)"
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
"name": "prefer-union-from-const-array",
|
|
65
|
+
"changeType": "fix",
|
|
66
|
+
"issues": [
|
|
67
|
+
1566
|
|
68
|
+
],
|
|
69
|
+
"summary": "wrap the authored as-const array when it exceeds the print width (closes #1566)"
|
|
70
|
+
}
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
"version": "1.20.66",
|
|
75
|
+
"date": "2026-08-01T13:53:31.680Z",
|
|
76
|
+
"rules": [
|
|
77
|
+
{
|
|
78
|
+
"name": "enforce-firestore-doc-ref-generic",
|
|
79
|
+
"changeType": "fix",
|
|
80
|
+
"issues": [
|
|
81
|
+
1564
|
|
82
|
+
],
|
|
83
|
+
"summary": "trace compat receivers through annotated lets and seeding callbacks (closes #1564)"
|
|
84
|
+
}
|
|
85
|
+
]
|
|
86
|
+
},
|
|
2
87
|
{
|
|
3
88
|
"version": "1.20.65",
|
|
4
89
|
"date": "2026-08-01T13:35:40.762Z",
|