@blumintinc/eslint-plugin-blumint 1.20.124 → 1.20.126
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-querykey-ts.js +17 -2
- package/lib/rules/ensure-pointer-events-none.js +71 -16
- package/lib/rules/global-const-style.js +15 -0
- package/lib/rules/prefer-global-router-state-key.js +17 -1
- package/package.json +1 -1
- package/release-manifest.json +52 -0
package/lib/index.js
CHANGED
|
@@ -508,15 +508,30 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
|
|
|
508
508
|
return null;
|
|
509
509
|
}
|
|
510
510
|
/**
|
|
511
|
-
*
|
|
511
|
+
* The `QUERY_KEY_*` constant a key value names, or null when it names none.
|
|
512
|
+
*
|
|
513
|
+
* A key that is empty, or built only from the characters normalization
|
|
514
|
+
* folds into separators and then strips, leaves nothing after the prefix:
|
|
515
|
+
* the bare `QUERY_KEY_` that emitted is a name `queryKeys.ts` neither
|
|
516
|
+
* exports nor plausibly would, so applying it traded a report for a file
|
|
517
|
+
* that no longer compiles. Declining here leaves the report standing with
|
|
518
|
+
* no fix, which is the honest outcome — the author has to choose a real
|
|
519
|
+
* key, and no rewrite can choose one for them.
|
|
520
|
+
*
|
|
521
|
+
* The test is on the derived text alone, so it answers the same way for
|
|
522
|
+
* every notation the same value can be written in; putting it in
|
|
523
|
+
* `staticKeyOf` instead would gate the fix on content at the point that
|
|
524
|
+
* exists to keep notation out of the gate (#1803, #1813).
|
|
512
525
|
*/
|
|
513
526
|
function generateAutoFix(keyValue) {
|
|
514
|
-
// Simple heuristic to suggest query key constant names
|
|
515
527
|
const normalizedKey = keyValue
|
|
516
528
|
.toUpperCase()
|
|
517
529
|
.replace(/[^A-Z0-9]/g, '_')
|
|
518
530
|
.replace(/_+/g, '_')
|
|
519
531
|
.replace(/^_|_$/g, '');
|
|
532
|
+
if (normalizedKey === '') {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
520
535
|
return `QUERY_KEY_${normalizedKey}`;
|
|
521
536
|
}
|
|
522
537
|
return {
|
|
@@ -9,6 +9,33 @@ const createRule_1 = require("../utils/createRule");
|
|
|
9
9
|
function hasPseudoElementSelector(selector) {
|
|
10
10
|
return /::?(before|after)\b/i.test(selector);
|
|
11
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* The four expression assertions state a type about the expression they wrap and
|
|
14
|
+
* contribute no value of their own, so `'none' as const`, `'none' satisfies
|
|
15
|
+
* string`, `('none')!` and `<const>'none'` all denote the same string as
|
|
16
|
+
* `'none'`. A read that classifies the value a property holds must look through
|
|
17
|
+
* every one of them alike, or the verdict turns on which type syntax an author
|
|
18
|
+
* reached for.
|
|
19
|
+
*/
|
|
20
|
+
const ASSERTION_TYPES = new Set([
|
|
21
|
+
utils_1.AST_NODE_TYPES.TSAsExpression,
|
|
22
|
+
utils_1.AST_NODE_TYPES.TSSatisfiesExpression,
|
|
23
|
+
utils_1.AST_NODE_TYPES.TSNonNullExpression,
|
|
24
|
+
utils_1.AST_NODE_TYPES.TSTypeAssertion,
|
|
25
|
+
]);
|
|
26
|
+
const isAssertion = (node) => ASSERTION_TYPES.has(node.type);
|
|
27
|
+
/**
|
|
28
|
+
* Peels every assertion off a node. Assertions nest — `'none' as const
|
|
29
|
+
* satisfies string` is a satisfies over an as — so a single unwrap would still
|
|
30
|
+
* hand back a wrapper.
|
|
31
|
+
*/
|
|
32
|
+
function unwrapAssertions(node) {
|
|
33
|
+
let target = node;
|
|
34
|
+
while (isAssertion(target)) {
|
|
35
|
+
target = target.expression;
|
|
36
|
+
}
|
|
37
|
+
return target;
|
|
38
|
+
}
|
|
12
39
|
/**
|
|
13
40
|
* Reads the static string a node denotes, so a property name or value carries
|
|
14
41
|
* the same meaning however it is spelled. A no-substitution template literal is
|
|
@@ -21,19 +48,22 @@ function hasPseudoElementSelector(selector) {
|
|
|
21
48
|
* literal with duplicate keys does not compile.
|
|
22
49
|
*
|
|
23
50
|
* An interpolated template stays opaque: its text is not known statically, so
|
|
24
|
-
* the rule keeps its conservative silence there.
|
|
51
|
+
* the rule keeps its conservative silence there. An assertion around an opaque
|
|
52
|
+
* value is equally opaque: unwrapping reaches the expression underneath, and
|
|
53
|
+
* that expression still decides whether anything can be read.
|
|
25
54
|
*/
|
|
26
55
|
function staticStringOf(node) {
|
|
27
|
-
|
|
28
|
-
|
|
56
|
+
const target = unwrapAssertions(node);
|
|
57
|
+
if (target.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
58
|
+
return String(target.value);
|
|
29
59
|
}
|
|
30
|
-
if (
|
|
31
|
-
return
|
|
60
|
+
if (target.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
61
|
+
return target.name;
|
|
32
62
|
}
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
return
|
|
63
|
+
if (target.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
|
|
64
|
+
target.expressions.length === 0 &&
|
|
65
|
+
target.quasis.length === 1) {
|
|
66
|
+
return target.quasis[0].value.cooked ?? target.quasis[0].value.raw;
|
|
37
67
|
}
|
|
38
68
|
return undefined;
|
|
39
69
|
}
|
|
@@ -110,7 +140,11 @@ function classifyOffsetComponents(raw) {
|
|
|
110
140
|
* classified; variables, member expressions, and calls are treated as unknown
|
|
111
141
|
* and never counted toward the hit-slop exemption.
|
|
112
142
|
*/
|
|
113
|
-
function classifyOffsetValue(
|
|
143
|
+
function classifyOffsetValue(node) {
|
|
144
|
+
// An assertion states a type, not a length. Reading the position through one
|
|
145
|
+
// while leaving the offsets opaque would strip an assertion-written hit-slop
|
|
146
|
+
// overlay of its carve-out and hand it the tap-target-shrinking autofix.
|
|
147
|
+
const value = unwrapAssertions(node);
|
|
114
148
|
if (value.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
115
149
|
if (typeof value.value === 'number') {
|
|
116
150
|
if (value.value === 0)
|
|
@@ -179,6 +213,13 @@ exports.ensurePointerEventsNone = (0, createRule_1.createRule)({
|
|
|
179
213
|
const absolutePositionedStyles = new Map();
|
|
180
214
|
// Track style objects that already have pointer-events defined
|
|
181
215
|
const stylesWithPointerEvents = new Map();
|
|
216
|
+
// Track style objects that declare a `pointerEvents` key whose value cannot
|
|
217
|
+
// be read statically (a member expression, a call, a ternary, an
|
|
218
|
+
// interpolated template). Such a value earns no exemption — it might be
|
|
219
|
+
// 'auto', so the report stands — but it does veto the fix: the rule's only
|
|
220
|
+
// remedy is to append a `pointerEvents` key, and an object literal with two
|
|
221
|
+
// identical keys does not compile (TS1117).
|
|
222
|
+
const stylesWithUnreadablePointerEvents = new Map();
|
|
182
223
|
// Track style objects that are hit-slop touch-target extensions: an
|
|
183
224
|
// absolute/fixed overlay whose inset offsets only extend beyond the origin
|
|
184
225
|
// box (>=1 negative, none positive). A browser attributes pointer events on
|
|
@@ -192,6 +233,7 @@ exports.ensurePointerEventsNone = (0, createRule_1.createRule)({
|
|
|
192
233
|
function processStyleObject(node) {
|
|
193
234
|
let hasAbsolutePosition = false;
|
|
194
235
|
let pointerEventsValue;
|
|
236
|
+
let hasUnreadablePointerEvents = false;
|
|
195
237
|
let hasNegativeOffset = false;
|
|
196
238
|
let hasPositiveOffset = false;
|
|
197
239
|
// Check each property in the style object
|
|
@@ -204,12 +246,17 @@ exports.ensurePointerEventsNone = (0, createRule_1.createRule)({
|
|
|
204
246
|
if (isAbsoluteOrFixedPosition(propertyName, propertyValue)) {
|
|
205
247
|
hasAbsolutePosition = true;
|
|
206
248
|
}
|
|
207
|
-
// Check if this is pointer-events property. A value that
|
|
208
|
-
//
|
|
209
|
-
//
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
249
|
+
// Check if this is pointer-events property. A value that can be read
|
|
250
|
+
// decides the exemption; one that cannot is recorded separately, because
|
|
251
|
+
// the key's presence vetoes the fix even where it cannot prove the
|
|
252
|
+
// overlay is inert. An unreadable value never clears one already read.
|
|
253
|
+
if (isPointerEventsProperty(propertyName)) {
|
|
254
|
+
if (propertyValue !== undefined) {
|
|
255
|
+
pointerEventsValue = propertyValue;
|
|
256
|
+
}
|
|
257
|
+
else {
|
|
258
|
+
hasUnreadablePointerEvents = true;
|
|
259
|
+
}
|
|
213
260
|
}
|
|
214
261
|
// Track inset offsets to detect hit-slop touch-target extensions
|
|
215
262
|
if (INSET_PROPERTIES.has(propertyName)) {
|
|
@@ -227,6 +274,7 @@ exports.ensurePointerEventsNone = (0, createRule_1.createRule)({
|
|
|
227
274
|
if (pointerEventsValue !== undefined) {
|
|
228
275
|
stylesWithPointerEvents.set(node, pointerEventsValue);
|
|
229
276
|
}
|
|
277
|
+
stylesWithUnreadablePointerEvents.set(node, hasUnreadablePointerEvents);
|
|
230
278
|
// A hit-slop extension only enlarges the tappable area: it is
|
|
231
279
|
// absolute/fixed and its inset offsets extend outward (>=1 negative, none
|
|
232
280
|
// positive). Such overlays cannot occlude the control they belong to.
|
|
@@ -257,6 +305,13 @@ exports.ensurePointerEventsNone = (0, createRule_1.createRule)({
|
|
|
257
305
|
selector: formatSelector(selector),
|
|
258
306
|
},
|
|
259
307
|
fix(fixer) {
|
|
308
|
+
// The object already declares `pointerEvents`, but in a spelling
|
|
309
|
+
// whose value cannot be read. Appending the key is the rule's only
|
|
310
|
+
// remedy, and here it would emit a duplicate key that does not
|
|
311
|
+
// compile. A report with no fix is the correct outcome: the reader
|
|
312
|
+
// decides what the opaque value resolves to.
|
|
313
|
+
if (stylesWithUnreadablePointerEvents.get(node))
|
|
314
|
+
return null;
|
|
260
315
|
// Find the last property in the object
|
|
261
316
|
const sourceCode = context.sourceCode;
|
|
262
317
|
const properties = node.properties;
|
|
@@ -421,6 +421,21 @@ exports.default = (0, createRule_1.createRule)({
|
|
|
421
421
|
if (!declaredVariable) {
|
|
422
422
|
return null;
|
|
423
423
|
}
|
|
424
|
+
// The conversion degenerates on some names: one built only from
|
|
425
|
+
// underscores derives the empty string, and a leading
|
|
426
|
+
// underscore in front of a digit derives a name that starts
|
|
427
|
+
// with that digit. Applying either trades a naming report for a
|
|
428
|
+
// file that no longer parses — `const = {…}` — and the rename
|
|
429
|
+
// rewrites every reference, so the damage spreads to each use
|
|
430
|
+
// site. Declining leaves the report standing with no fix, which
|
|
431
|
+
// is the honest outcome: the author has to choose a real name,
|
|
432
|
+
// and no mechanical rewrite can choose one for them. The test is
|
|
433
|
+
// the rule's own acceptance predicate, so a derivation that
|
|
434
|
+
// would only relocate the same report (`_$` to `$`) is declined
|
|
435
|
+
// on the same terms.
|
|
436
|
+
if (!isUpperSnakeCase(newName)) {
|
|
437
|
+
return null;
|
|
438
|
+
}
|
|
424
439
|
// An exported binding's name is a cross-file contract: every
|
|
425
440
|
// importer spells it out in a file this single-file fixer
|
|
426
441
|
// cannot reach, so renaming the declaration breaks them all
|
|
@@ -336,7 +336,20 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
|
|
|
336
336
|
return null;
|
|
337
337
|
}
|
|
338
338
|
/**
|
|
339
|
-
*
|
|
339
|
+
* The `QUERY_KEY_*` constant a key value names, or null when it names none.
|
|
340
|
+
*
|
|
341
|
+
* A key that is empty, or built only from the characters normalization
|
|
342
|
+
* folds into separators and then strips, leaves nothing after the prefix:
|
|
343
|
+
* the bare `QUERY_KEY_` that emitted is a name `queryKeys.ts` neither
|
|
344
|
+
* exports nor plausibly would, so applying it traded a report for a file
|
|
345
|
+
* that no longer compiles. Declining here leaves the report standing with
|
|
346
|
+
* no fix, which is the honest outcome — the author has to choose a real
|
|
347
|
+
* key, and no rewrite can choose one for them.
|
|
348
|
+
*
|
|
349
|
+
* The test is on the derived text alone, so it answers the same way for
|
|
350
|
+
* every notation the same value can be written in; putting it in
|
|
351
|
+
* `staticKeyOf` instead would gate the fix on content at the point that
|
|
352
|
+
* exists to keep notation out of the gate (#1804, #1811).
|
|
340
353
|
*/
|
|
341
354
|
function generateAutoFix(keyValue) {
|
|
342
355
|
const normalizedKey = keyValue
|
|
@@ -344,6 +357,9 @@ exports.preferGlobalRouterStateKey = (0, createRule_1.createRule)({
|
|
|
344
357
|
.replace(/[^A-Z0-9]/g, '_')
|
|
345
358
|
.replace(/_+/g, '_')
|
|
346
359
|
.replace(/^_|_$/g, '');
|
|
360
|
+
if (normalizedKey === '') {
|
|
361
|
+
return null;
|
|
362
|
+
}
|
|
347
363
|
return `QUERY_KEY_${normalizedKey}`;
|
|
348
364
|
}
|
|
349
365
|
/**
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,56 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.126",
|
|
4
|
+
"date": "2026-08-06T22:42:22.372Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "ensure-pointer-events-none",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
1814
|
|
11
|
+
],
|
|
12
|
+
"summary": "read every selector, name, value and offset through expression assertions (closes #1814)"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "global-const-style",
|
|
16
|
+
"changeType": "fix",
|
|
17
|
+
"issues": [
|
|
18
|
+
1816
|
|
19
|
+
],
|
|
20
|
+
"summary": "decline the rename when the derived name is not an identifier (closes #1816)"
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
"version": "1.20.125",
|
|
26
|
+
"date": "2026-08-06T17:33:56.677Z",
|
|
27
|
+
"rules": [
|
|
28
|
+
{
|
|
29
|
+
"name": "enforce-querykey-ts",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
1813
|
|
33
|
+
],
|
|
34
|
+
"summary": "decline the fix when a key normalizes to nothing (closes #1813)"
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"name": "ensure-pointer-events-none",
|
|
38
|
+
"changeType": "fix",
|
|
39
|
+
"issues": [
|
|
40
|
+
1810
|
|
41
|
+
],
|
|
42
|
+
"summary": "decline the fix when an existing pointerEvents value is unreadable (closes #1810)"
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"name": "prefer-global-router-state-key",
|
|
46
|
+
"changeType": "fix",
|
|
47
|
+
"issues": [
|
|
48
|
+
1811
|
|
49
|
+
],
|
|
50
|
+
"summary": "decline the fix when a key normalizes to nothing (closes #1811)"
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
},
|
|
2
54
|
{
|
|
3
55
|
"version": "1.20.124",
|
|
4
56
|
"date": "2026-08-06T15:15:14.462Z",
|