@blumintinc/eslint-plugin-blumint 1.20.93 → 1.20.94

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.93',
226
+ version: '1.20.94',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -27,6 +27,7 @@ exports.enforceDateTTime = void 0;
27
27
  const utils_1 = require("@typescript-eslint/utils");
28
28
  const ts = __importStar(require("typescript"));
29
29
  const createRule_1 = require("../utils/createRule");
30
+ const disableDirectives_1 = require("../utils/disableDirectives");
30
31
  const importRemoval_1 = require("../utils/importRemoval");
31
32
  exports.enforceDateTTime = (0, createRule_1.createRule)({
32
33
  name: 'enforce-date-ttime',
@@ -92,7 +93,102 @@ exports.enforceDateTTime = (0, createRule_1.createRule)({
92
93
  }
93
94
  return false;
94
95
  }
96
+ /**
97
+ * Reporting is deferred to `Program:exit` because an import is unbound only
98
+ * once no reference to it survives the fix, and a file where two arguments
99
+ * name the same imported alias overwrites both in a single pass. Judging
100
+ * each rewrite alone sees the sibling argument still standing, concludes the
101
+ * binding is alive, and leaves the import stranded with no later pass to
102
+ * notice — this rule's own reports are resolved by the fix, so nothing
103
+ * re-reports the debt.
104
+ */
105
+ const sites = [];
106
+ /**
107
+ * Suppression is applied to reports after a rule emits them, so a suppressed
108
+ * site keeps its argument while losing its fix. Counting its range toward
109
+ * orphanhood would unbind an import the surviving text still spells, trading
110
+ * an unused import for a dangling type.
111
+ */
112
+ const isReportSuppressed = (0, disableDirectives_1.createSuppressionChecker)(context);
113
+ /**
114
+ * Supplies a TTime that was left out entirely. Declines whenever the
115
+ * argument cannot be appended positionally — a gap before TTime would need
116
+ * every intervening parameter spelled out, and this rule does not invent
117
+ * them.
118
+ */
119
+ function fixOmittedArgument(site, fixer) {
120
+ const { reference, tTimeIndex } = site;
121
+ if (reference.typeParameters) {
122
+ const { params } = reference.typeParameters;
123
+ if (tTimeIndex === params.length) {
124
+ return fixer.insertTextAfter(params[params.length - 1], ', Date');
125
+ }
126
+ return null;
127
+ }
128
+ if (tTimeIndex === 0) {
129
+ return fixer.insertTextAfter(reference.typeName, '<Date>');
130
+ }
131
+ return null;
132
+ }
133
+ /**
134
+ * The rewrites that actually ship. A site is excluded when its report will
135
+ * be suppressed, or when its own range holds the last reference to something
136
+ * the helper cannot rewrite — a locally declared alias, or the enclosing
137
+ * declaration's own type parameter. Deleting a declaration is a materially
138
+ * riskier edit than dropping an import specifier, and a type parameter left
139
+ * unread by its own body fails `no-unused-vars` and `noUnusedParameters`
140
+ * exactly as an orphaned alias does, on top of turning a pass-through
141
+ * generic into one whose argument no longer means anything.
142
+ *
143
+ * Screening individually before batching keeps one unfixable site from
144
+ * vetoing the rest: orphanhood grows monotonically with the overwritten set,
145
+ * so a site that cannot be planned alone can only ever poison the batch.
146
+ */
147
+ function selectRewritableSites() {
148
+ return sites.filter((site) => site.kind === 'nonDate' &&
149
+ !isReportSuppressed(site.reportNode) &&
150
+ (0, importRemoval_1.planOrphanedImportRemoval)(sourceCode, [site.reportNode.range]) !==
151
+ null);
152
+ }
95
153
  return {
154
+ 'Program:exit'() {
155
+ if (sites.length === 0)
156
+ return;
157
+ const rewrites = selectRewritableSites();
158
+ const ranges = rewrites.map((site) => site.reportNode.range);
159
+ // One plan over every surviving rewrite: an import referenced solely by
160
+ // arguments that all go in this pass is orphaned by their union, even
161
+ // though no single one of them orphans it.
162
+ const importRanges = ranges.length > 0
163
+ ? (0, importRemoval_1.planOrphanedImportRemoval)(sourceCode, ranges)
164
+ : null;
165
+ // The whole batch ships as one fix, so no rewrite can land without the
166
+ // others that the import's orphanhood was judged against. The rest
167
+ // report without a fixer; the carrier's pass already resolves them.
168
+ const carrier = importRanges ? rewrites[0] : undefined;
169
+ for (const site of sites) {
170
+ if (site.kind === 'omitted') {
171
+ context.report({
172
+ node: site.reportNode,
173
+ messageId: 'enforceDateTTime',
174
+ data: { typeName: site.typeName },
175
+ fix: (fixer) => fixOmittedArgument(site, fixer),
176
+ });
177
+ continue;
178
+ }
179
+ context.report({
180
+ node: site.reportNode,
181
+ messageId: 'enforceDateTTime',
182
+ data: { typeName: site.typeName },
183
+ fix: site === carrier && importRanges
184
+ ? (fixer) => [
185
+ ...rewrites.map((rewrite) => fixer.replaceText(rewrite.reportNode, 'Date')),
186
+ ...importRanges.map((range) => fixer.removeRange([range[0], range[1]])),
187
+ ]
188
+ : null,
189
+ });
190
+ }
191
+ },
96
192
  TSTypeReference(node) {
97
193
  const typeNameNode = node.typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName
98
194
  ? node.typeName.right
@@ -116,71 +212,21 @@ exports.enforceDateTTime = (0, createRule_1.createRule)({
116
212
  const tTimeArg = typeArgs[tTimeIndex];
117
213
  const typeName = sourceCode.getText(node.typeName);
118
214
  if (!tTimeArg) {
119
- // TTime is omitted
120
- context.report({
121
- node,
122
- messageId: 'enforceDateTTime',
123
- data: { typeName },
124
- fix(fixer) {
125
- if (node.typeParameters) {
126
- // Already has type parameters, but not enough to cover TTime
127
- const lastParam = node.typeParameters.params[node.typeParameters.params.length - 1];
128
- // Check if we can just append ", Date"
129
- // We can only do this safely if all parameters between existing ones and TTime have defaults.
130
- // For simplicity and safety, we only fix if tTimeIndex is the next one or if it's already provided.
131
- if (tTimeIndex === node.typeParameters.params.length) {
132
- return fixer.insertTextAfter(lastParam, ', Date');
133
- }
134
- }
135
- else if (tTimeIndex === 0) {
136
- // No type parameters and TTime is the first one
137
- return fixer.insertTextAfter(node.typeName, '<Date>');
138
- }
139
- return null;
140
- },
215
+ sites.push({
216
+ kind: 'omitted',
217
+ reportNode: node,
218
+ reference: node,
219
+ typeName,
220
+ tTimeIndex,
141
221
  });
142
222
  }
143
223
  else if (!isExactDate(tTimeArg)) {
144
- // TTime is provided but is not exactly Date. Overwriting the argument
145
- // deletes every name it mentions, so the rewrite and the unbinding of
146
- // whatever it was the last reference to are one fix: applying either
147
- // half alone leaves the file worse than applying neither, and since
148
- // this rule's own report is resolved by the fix, nothing re-reports
149
- // the debt an orphaned declaration becomes.
150
- //
151
- // Orphanhood is judged against this one argument's own range and the
152
- // file as it stands, never against what the rest of the `--fix` run
153
- // might also overwrite. A sibling argument naming the same alias may
154
- // be `eslint-disable`d — which a rule cannot see, since suppression is
155
- // applied to reports after they are emitted — so an edit assuming its
156
- // sibling will also go unbinds an import the survivor still
157
- // references, trading an unused import for a dangling type. Judging
158
- // one edit at a time is suppression-safe by construction.
159
- const importRanges = (0, importRemoval_1.planOrphanedImportRemoval)(sourceCode, [
160
- tTimeArg.range,
161
- ]);
162
- context.report({
163
- node: tTimeArg,
164
- messageId: 'enforceDateTTime',
165
- data: { typeName },
166
- // No plan means the argument holds the last reference to something
167
- // the helper cannot rewrite — a locally declared alias, or the
168
- // enclosing declaration's own type parameter — so the argument
169
- // stays as written: the report without a fixer is the lesser
170
- // damage. Deleting a declaration is a materially riskier edit than
171
- // dropping an import specifier, and a type parameter left unread by
172
- // its own body fails `no-unused-vars` and `noUnusedParameters`
173
- // exactly as an orphaned alias does, on top of turning a
174
- // pass-through generic into one whose argument no longer means
175
- // anything.
176
- ...(importRanges
177
- ? {
178
- fix: (fixer) => [
179
- fixer.replaceText(tTimeArg, 'Date'),
180
- ...importRanges.map((range) => fixer.removeRange([range[0], range[1]])),
181
- ],
182
- }
183
- : {}),
224
+ sites.push({
225
+ kind: 'nonDate',
226
+ reportNode: tTimeArg,
227
+ reference: node,
228
+ typeName,
229
+ tTimeIndex,
184
230
  });
185
231
  }
186
232
  },
@@ -29,6 +29,7 @@ const visitor_keys_1 = require("@typescript-eslint/visitor-keys");
29
29
  const ts = __importStar(require("typescript"));
30
30
  const ASTHelpers_1 = require("../utils/ASTHelpers");
31
31
  const createRule_1 = require("../utils/createRule");
32
+ const disableDirectives_1 = require("../utils/disableDirectives");
32
33
  const importRemoval_1 = require("../utils/importRemoval");
33
34
  /**
34
35
  * Type string formatting flags chosen to keep comparisons stable and predictable.
@@ -345,51 +346,78 @@ exports.noRedundantAnnotationAssertion = (0, createRule_1.createRule)({
345
346
  }
346
347
  const checker = parserServices.program.getTypeChecker();
347
348
  /**
348
- * Reports the redundant annotation, taking with it any import it was the
349
- * only consumer of. The two are one fix: applying either half alone leaves
350
- * the file worse than applying neither a stripped annotation with its
351
- * import left behind fails `no-unused-vars`, and since this rule's own
352
- * report is resolved by the fix, nothing re-reports the debt.
353
- *
354
- * Orphanhood is judged against this one annotation's own removal and the
355
- * file as it stands, never against what the rest of the `--fix` run might
356
- * also delete. A sibling annotation naming the same type may be
357
- * `eslint-disable`d — which a rule cannot see, since suppression is applied
358
- * to reports after they are emitted — so an edit that assumes its sibling
359
- * will also go deletes an import the surviving annotation still references,
360
- * trading an unused import for a dangling type. Judging one edit at a time
361
- * is suppression-safe by construction: a suppressed report's fix never
362
- * applies, so it can never have been depended on.
363
- *
364
- * A type the annotation names but the helper cannot rewrite — a local alias,
365
- * an interface, a type parameter — declines the fix outright. Deleting a
366
- * declaration is a materially riskier edit than dropping an import
367
- * specifier, and the author is better placed to decide whether the type
368
- * should go or be used elsewhere.
349
+ * Reporting is deferred to `Program:exit` because an import is unbound only
350
+ * once no reference to it survives the fix, and a file where two annotations
351
+ * name the same imported type strips both in a single pass. Judging each
352
+ * removal alone sees the sibling annotation still standing, concludes the
353
+ * binding is alive, and leaves the import stranded with no later pass to
354
+ * notice — this rule's own reports are resolved by the fix, so nothing
355
+ * re-reports the debt.
369
356
  */
370
- function reportIfRedundant(annotation, assertion, reportNode, fixerTarget) {
357
+ const sites = [];
358
+ /**
359
+ * Suppression is applied to reports after a rule emits them, so a suppressed
360
+ * site keeps its annotation while losing its fix. Counting its removal
361
+ * toward orphanhood would unbind an import the surviving text still spells,
362
+ * trading an unused import for a dangling type.
363
+ */
364
+ const isReportSuppressed = (0, disableDirectives_1.createSuppressionChecker)(context);
365
+ function collectIfRedundant(annotation, assertion, reportNode, fixerTarget) {
371
366
  const matchingType = haveMatchingTypes(annotation.typeAnnotation, assertion, checker, parserServices);
372
367
  if (!matchingType)
373
368
  return;
374
- const removal = annotationRemovalRange(fixerTarget, sourceCode);
375
- const importRanges = (0, importRemoval_1.planOrphanedImportRemoval)(sourceCode, [removal]);
376
- context.report({
377
- node: reportNode,
378
- messageId: 'redundantAnnotationAndAssertion',
379
- data: { type: matchingType },
380
- // No plan means no binding can be unbound safely, so the annotation
381
- // stays too: the report without a fixer is the lesser damage.
382
- ...(importRanges
383
- ? {
384
- fix: (fixer) => [
385
- fixer.removeRange([removal[0], removal[1]]),
386
- ...importRanges.map((range) => fixer.removeRange([range[0], range[1]])),
387
- ],
388
- }
389
- : {}),
369
+ sites.push({
370
+ reportNode,
371
+ removal: annotationRemovalRange(fixerTarget, sourceCode),
372
+ matchingType,
390
373
  });
391
374
  }
375
+ /**
376
+ * The sites whose fixes actually ship. A site is excluded when its report
377
+ * will be suppressed, or when its own removal orphans something the helper
378
+ * cannot rewrite — a local alias, an interface, a type parameter. Deleting a
379
+ * declaration is a materially riskier edit than dropping an import
380
+ * specifier, and the author is better placed to decide whether the type
381
+ * should go or be used elsewhere.
382
+ *
383
+ * Screening individually before batching keeps one unfixable site from
384
+ * vetoing the rest: orphanhood grows monotonically with the removed set, so
385
+ * a site that cannot be planned alone can only ever poison the batch.
386
+ */
387
+ function selectFixableSites() {
388
+ return sites.filter((site) => !isReportSuppressed(site.reportNode) &&
389
+ (0, importRemoval_1.planOrphanedImportRemoval)(sourceCode, [site.removal]) !== null);
390
+ }
392
391
  return {
392
+ 'Program:exit'() {
393
+ if (sites.length === 0)
394
+ return;
395
+ const fixable = selectFixableSites();
396
+ const removals = fixable.map((site) => site.removal);
397
+ // One plan over every surviving removal: an import referenced solely by
398
+ // annotations that all go in this pass is orphaned by their union, even
399
+ // though no single one of them orphans it.
400
+ const importRanges = removals.length > 0
401
+ ? (0, importRemoval_1.planOrphanedImportRemoval)(sourceCode, removals)
402
+ : null;
403
+ // The whole batch ships as one fix, so no removal can land without the
404
+ // others that the import's orphanhood was judged against. The rest
405
+ // report without a fixer; the carrier's pass already resolves them.
406
+ const carrier = importRanges ? fixable[0] : undefined;
407
+ for (const site of sites) {
408
+ context.report({
409
+ node: site.reportNode,
410
+ messageId: 'redundantAnnotationAndAssertion',
411
+ data: { type: site.matchingType },
412
+ fix: site === carrier && importRanges
413
+ ? (fixer) => [
414
+ ...removals.map((range) => fixer.removeRange([range[0], range[1]])),
415
+ ...importRanges.map((range) => fixer.removeRange([range[0], range[1]])),
416
+ ]
417
+ : null,
418
+ });
419
+ }
420
+ },
393
421
  VariableDeclarator(node) {
394
422
  if (node.id.type !== utils_1.AST_NODE_TYPES.Identifier ||
395
423
  !node.id.typeAnnotation ||
@@ -400,7 +428,7 @@ exports.noRedundantAnnotationAssertion = (0, createRule_1.createRule)({
400
428
  const assertionType = extractAssertionTypeNode(node.init);
401
429
  if (!assertionType)
402
430
  return;
403
- reportIfRedundant(node.id.typeAnnotation, assertionType, node.id, node.id.typeAnnotation);
431
+ collectIfRedundant(node.id.typeAnnotation, assertionType, node.id, node.id.typeAnnotation);
404
432
  },
405
433
  PropertyDefinition(node) {
406
434
  if (!node.typeAnnotation ||
@@ -411,7 +439,7 @@ exports.noRedundantAnnotationAssertion = (0, createRule_1.createRule)({
411
439
  const assertionType = extractAssertionTypeNode(node.value);
412
440
  if (!assertionType)
413
441
  return;
414
- reportIfRedundant(node.typeAnnotation, assertionType, node.key, node.typeAnnotation);
442
+ collectIfRedundant(node.typeAnnotation, assertionType, node.key, node.typeAnnotation);
415
443
  },
416
444
  FunctionDeclaration(node) {
417
445
  if (!node.returnType)
@@ -419,7 +447,7 @@ exports.noRedundantAnnotationAssertion = (0, createRule_1.createRule)({
419
447
  const assertionType = getReturnAssertion(node);
420
448
  if (!assertionType)
421
449
  return;
422
- reportIfRedundant(node.returnType, assertionType, node.id ?? node, node.returnType);
450
+ collectIfRedundant(node.returnType, assertionType, node.id ?? node, node.returnType);
423
451
  },
424
452
  FunctionExpression(node) {
425
453
  if (node.parent?.type === utils_1.AST_NODE_TYPES.MethodDefinition) {
@@ -430,7 +458,7 @@ exports.noRedundantAnnotationAssertion = (0, createRule_1.createRule)({
430
458
  const assertionType = getReturnAssertion(node);
431
459
  if (!assertionType)
432
460
  return;
433
- reportIfRedundant(node.returnType, assertionType, node, node.returnType);
461
+ collectIfRedundant(node.returnType, assertionType, node, node.returnType);
434
462
  },
435
463
  ArrowFunctionExpression(node) {
436
464
  if (!node.returnType)
@@ -438,7 +466,7 @@ exports.noRedundantAnnotationAssertion = (0, createRule_1.createRule)({
438
466
  const assertionType = getReturnAssertion(node);
439
467
  if (!assertionType)
440
468
  return;
441
- reportIfRedundant(node.returnType, assertionType, node, node.returnType);
469
+ collectIfRedundant(node.returnType, assertionType, node, node.returnType);
442
470
  },
443
471
  MethodDefinition(node) {
444
472
  if (!node.value.returnType)
@@ -446,7 +474,7 @@ exports.noRedundantAnnotationAssertion = (0, createRule_1.createRule)({
446
474
  const assertionType = getReturnAssertion(node);
447
475
  if (!assertionType)
448
476
  return;
449
- reportIfRedundant(node.value.returnType, assertionType, node.key, node.value.returnType);
477
+ collectIfRedundant(node.value.returnType, assertionType, node.key, node.value.returnType);
450
478
  },
451
479
  };
452
480
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.93",
3
+ "version": "1.20.94",
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.94",
4
+ "date": "2026-08-03T22:57:52.839Z",
5
+ "rules": [
6
+ {
7
+ "name": "enforce-date-ttime",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1669
11
+ ],
12
+ "summary": "unbind the import two arguments share (closes #1669)"
13
+ },
14
+ {
15
+ "name": "no-redundant-annotation-assertion",
16
+ "changeType": "fix",
17
+ "issues": [
18
+ 1668
19
+ ],
20
+ "summary": "unbind the import two annotations share (closes #1668)"
21
+ }
22
+ ]
23
+ },
2
24
  {
3
25
  "version": "1.20.93",
4
26
  "date": "2026-08-03T22:34:05.616Z",