@blumintinc/eslint-plugin-blumint 1.20.151 → 1.20.152

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.151',
226
+ version: '1.20.152',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -123,20 +123,19 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
123
123
  ]);
124
124
  const sourceCode = context.getSourceCode();
125
125
  /**
126
- * Reports are buffered until the whole file has been walked so the fixer can
127
- * put every substituted constant into a single import instead of racing
128
- * per-violation insertions that overlap and get dropped.
126
+ * Reports are buffered until the whole file has been walked so each fix is
127
+ * planned against the file's complete import picture: what a substituted
128
+ * constant resolves to, and whether the fix has to bring an import with it,
129
+ * are questions about the whole file rather than about the text above the
130
+ * violation.
129
131
  */
130
132
  const pendingReports = [];
131
133
  /**
132
- * The queryKeys import rides on one violation's fix, which makes that
133
- * violation the file's import carrier. ESLint collects fixes before it
134
- * applies inline disable directives, so a suppressed carrier takes the
135
- * import down with it while the surviving substitutions still land
136
- * leaving the file referencing constants nothing imports (#1410).
137
- * Resolving suppression here keeps a suppressed violation out of the plan
138
- * entirely: it neither claims the carrier slot nor contributes a specifier
139
- * to the import, which would otherwise be imported and never used.
134
+ * A violation the author has waived with an inline disable directive is
135
+ * left out of the fix plan, so no constant is resolved for it and no import
136
+ * is written for it (#1410). This is the decision ESLint reaches anyway
137
+ * when it discards a suppressed message together with its fix, taken one
138
+ * step earlier where the rule can see it.
140
139
  */
141
140
  const isReportSuppressed = (0, disableDirectives_1.createSuppressionChecker)(context);
142
141
  /**
@@ -264,10 +263,13 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
264
263
  : queryKeysSpecifier;
265
264
  }
266
265
  /**
267
- * Make the substituted constants resolve: extend the file's queryKeys import
266
+ * Make a substituted constant resolve: extend the file's queryKeys import
268
267
  * when there is one to extend, otherwise add a fresh import statement.
268
+ *
269
+ * The caller asks for this only where the constant binds to nothing, so the
270
+ * emitted specifier is never a duplicate of one the file already has.
269
271
  */
270
- function buildImportFix(fixer, constants) {
272
+ function buildImportFix(fixer, constant) {
271
273
  const importDeclarations = importDeclarationsOf();
272
274
  const queryKeysDeclarations = queryKeysDeclarationsOf();
273
275
  const reusable = queryKeysDeclarations.find((declaration) => declaration.importKind !== 'type' &&
@@ -275,7 +277,7 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
275
277
  if (reusable) {
276
278
  const namedSpecifiers = reusable.specifiers.filter(isValueImportSpecifier);
277
279
  const lastSpecifier = namedSpecifiers[namedSpecifiers.length - 1];
278
- return fixer.insertTextAfter(lastSpecifier, constants.map((constant) => `, ${constant}`).join(''));
280
+ return fixer.insertTextAfter(lastSpecifier, `, ${constant}`);
279
281
  }
280
282
  // A namespace or type-only queryKeys import cannot take named value
281
283
  // specifiers, but its path is proof of how this file reaches the module.
@@ -283,7 +285,7 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
283
285
  if (source === null) {
284
286
  return null;
285
287
  }
286
- const importText = `import { ${constants.join(', ')} } from '${source}';\n`;
288
+ const importText = `import { ${constant} } from '${source}';\n`;
287
289
  const anchor = (0, importInsertion_1.importInsertionAnchor)(sourceCode);
288
290
  if (importDeclarations.length) {
289
291
  // The statement joins an import block, and the anchor is that block's
@@ -310,7 +312,6 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
310
312
  }
311
313
  function flushReports() {
312
314
  const resolutions = new Map();
313
- const missingConstants = [];
314
315
  const canImport = importSourceOf() !== null;
315
316
  // The location handed to `context.report` below is what ESLint matches a
316
317
  // directive against, so suppression is resolved from exactly that node.
@@ -329,20 +330,7 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
329
330
  continue;
330
331
  }
331
332
  resolutions.set(report, { name, state });
332
- if (state === 'missing' && !missingConstants.includes(name)) {
333
- missingConstants.push(name);
334
- }
335
333
  }
336
- // The first applied substitution carries the import for every other one:
337
- // its fix range then starts at the top of the file and ends before the
338
- // remaining literals, so no two fixes of this rule overlap in a pass.
339
- // Suppressed reports are skipped so the slot falls to a survivor.
340
- const importCarrier = pendingReports.find((report) => {
341
- const resolution = resolutions.get(report);
342
- return (!suppressed.has(report) &&
343
- resolution !== undefined &&
344
- resolution.state !== 'conflict');
345
- });
346
334
  // Suppressed violations are still reported: ESLint discards them, and
347
335
  // reporting keeps the user's directive "used" so that
348
336
  // `--report-unused-disable-directives` does not flag it.
@@ -363,8 +351,20 @@ exports.enforceQueryKeyTs = (0, createRule_1.createRule)({
363
351
  const fixes = [
364
352
  fixer.replaceText(substitution.keyNode, resolution.name),
365
353
  ];
366
- if (report === importCarrier && missingConstants.length > 0) {
367
- const importFix = buildImportFix(fixer, missingConstants);
354
+ // Every fix stands on its own: the one that writes a constant is
355
+ // the one that imports it. Concentrating the file's imports into a
356
+ // single carrier fix made every other fix depend on that one being
357
+ // applied, and ESLint drops a fix whose range overlaps a fix it has
358
+ // already taken from another rule — so the carrier lost that race
359
+ // while the literal-only fixes still landed, leaving the file
360
+ // naming an identifier nothing imports (#2012).
361
+ //
362
+ // Two such fixes both reach for the import declaration, so within a
363
+ // pass they overlap and ESLint applies one of them. That converges:
364
+ // the unapplied violation is re-reported on the next pass and its
365
+ // constant then binds to, or extends, the import that landed.
366
+ if (resolution.state === 'missing') {
367
+ const importFix = buildImportFix(fixer, resolution.name);
368
368
  if (!importFix) {
369
369
  return null;
370
370
  }
@@ -25,6 +25,41 @@ function isCloneDeepModule(source) {
25
25
  source.startsWith('@/') ||
26
26
  source.endsWith('util/cloneDeep'));
27
27
  }
28
+ function isConstAssertion(node) {
29
+ return (node.type === utils_1.AST_NODE_TYPES.TSAsExpression &&
30
+ node.typeAnnotation.type === utils_1.AST_NODE_TYPES.TSTypeReference &&
31
+ node.typeAnnotation.typeName.type === utils_1.AST_NODE_TYPES.Identifier &&
32
+ node.typeAnnotation.typeName.name === 'const');
33
+ }
34
+ /**
35
+ * A `const` assertion is legal only on a literal, so leaving one wrapped around
36
+ * the emitted `cloneDeep(...)` call yields TS1355 and turns a compiling file
37
+ * into a broken one (#2011). The fix is declined there rather than absorbing
38
+ * the assertion, which is the conservative reading of a `const` the author
39
+ * asked for on a value this rule replaces.
40
+ *
41
+ * The whole assertion chain is walked because each of its links still applies
42
+ * to the emitted call: `as Foo as const`, `satisfies Foo as const` and
43
+ * `! as const` are TS1355 just the same. The walk stops at the first parent
44
+ * that is not an assertion, which keeps `as const` on an ENCLOSING literal
45
+ * fixable — that assertion still has a literal to apply to.
46
+ *
47
+ * Only a `const` assertion is disqualifying: `as Foo` and `satisfies Foo` are
48
+ * legal on a call expression and keep their fix.
49
+ */
50
+ function isConstAsserted(node) {
51
+ let current = node.parent;
52
+ while (current &&
53
+ (current.type === utils_1.AST_NODE_TYPES.TSAsExpression ||
54
+ current.type === utils_1.AST_NODE_TYPES.TSSatisfiesExpression ||
55
+ current.type === utils_1.AST_NODE_TYPES.TSNonNullExpression)) {
56
+ if (isConstAssertion(current)) {
57
+ return true;
58
+ }
59
+ current = current.parent;
60
+ }
61
+ return false;
62
+ }
28
63
  exports.preferCloneDeep = (0, createRule_1.createRule)({
29
64
  name: 'prefer-clone-deep',
30
65
  meta: {
@@ -444,6 +479,9 @@ exports.preferCloneDeep = (0, createRule_1.createRule)({
444
479
  return null;
445
480
  }
446
481
  for (const target of targets) {
482
+ if (isConstAsserted(target)) {
483
+ return null;
484
+ }
447
485
  const call = buildCloneDeepCall(target);
448
486
  if (call === null) {
449
487
  return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.151",
3
+ "version": "1.20.152",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,26 @@
1
1
  [
2
+ {
3
+ "version": "1.20.152",
4
+ "date": "2026-08-14T10:28:08.765Z",
5
+ "rules": [
6
+ {
7
+ "name": "enforce-querykey-ts",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 2012
11
+ ],
12
+ "summary": "carry each key's import on its own fix (closes #2012)"
13
+ },
14
+ {
15
+ "name": "prefer-clone-deep",
16
+ "changeType": "fix",
17
+ "issues": [
18
+ 2011
19
+ ],
20
+ "summary": "decline the fix under a const assertion (closes #2011)"
21
+ }
22
+ ]
23
+ },
2
24
  {
3
25
  "version": "1.20.151",
4
26
  "date": "2026-08-14T02:27:13.012Z",