@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 CHANGED
@@ -223,7 +223,7 @@ function noFrontendImportsFromFunctionsPatterns(pattern) {
223
223
  module.exports = {
224
224
  meta: {
225
225
  name: '@blumintinc/eslint-plugin-blumint',
226
- version: '1.20.124',
226
+ version: '1.20.126',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -508,15 +508,30 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
508
508
  return null;
509
509
  }
510
510
  /**
511
- * Generate auto-fix suggestion for string literals
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
- if (node.type === utils_1.AST_NODE_TYPES.Literal) {
28
- return String(node.value);
56
+ const target = unwrapAssertions(node);
57
+ if (target.type === utils_1.AST_NODE_TYPES.Literal) {
58
+ return String(target.value);
29
59
  }
30
- if (node.type === utils_1.AST_NODE_TYPES.Identifier) {
31
- return node.name;
60
+ if (target.type === utils_1.AST_NODE_TYPES.Identifier) {
61
+ return target.name;
32
62
  }
33
- if (node.type === utils_1.AST_NODE_TYPES.TemplateLiteral &&
34
- node.expressions.length === 0 &&
35
- node.quasis.length === 1) {
36
- return node.quasis[0].value.cooked ?? node.quasis[0].value.raw;
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(value) {
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 cannot be read
208
- // statically never clears one already read: the rule's only remedy is to
209
- // append a `pointerEvents` key, which would duplicate the existing one.
210
- if (isPointerEventsProperty(propertyName) &&
211
- propertyValue !== undefined) {
212
- pointerEventsValue = propertyValue;
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
- * Generate auto-fix suggestion for string literals
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.124",
3
+ "version": "1.20.126",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -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",