@blumintinc/eslint-plugin-blumint 1.20.92 → 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.92',
226
+ version: '1.20.94',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -1 +1,2 @@
1
- export declare const enforceDateTTime: import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleModule<"enforceDateTTime", [], import("@typescript-eslint/utils/dist/ts-eslint/Rule").RuleListener>;
1
+ import { TSESLint } from '@typescript-eslint/utils';
2
+ export declare const enforceDateTTime: TSESLint.RuleModule<"enforceDateTTime", [], TSESLint.RuleListener>;
@@ -27,6 +27,8 @@ 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");
31
+ const importRemoval_1 = require("../utils/importRemoval");
30
32
  exports.enforceDateTTime = (0, createRule_1.createRule)({
31
33
  name: 'enforce-date-ttime',
32
34
  meta: {
@@ -43,7 +45,8 @@ exports.enforceDateTTime = (0, createRule_1.createRule)({
43
45
  },
44
46
  defaultOptions: [],
45
47
  create(context) {
46
- const sourceCode = context.sourceCode ?? context.getSourceCode();
48
+ const sourceCode = context
49
+ .sourceCode ?? context.getSourceCode();
47
50
  const parserServices = sourceCode?.parserServices ?? context.parserServices;
48
51
  const checker = parserServices?.program?.getTypeChecker();
49
52
  if (!parserServices || !checker) {
@@ -90,7 +93,102 @@ exports.enforceDateTTime = (0, createRule_1.createRule)({
90
93
  }
91
94
  return false;
92
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
+ }
93
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
+ },
94
192
  TSTypeReference(node) {
95
193
  const typeNameNode = node.typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName
96
194
  ? node.typeName.right
@@ -114,39 +212,21 @@ exports.enforceDateTTime = (0, createRule_1.createRule)({
114
212
  const tTimeArg = typeArgs[tTimeIndex];
115
213
  const typeName = sourceCode.getText(node.typeName);
116
214
  if (!tTimeArg) {
117
- // TTime is omitted
118
- context.report({
119
- node,
120
- messageId: 'enforceDateTTime',
121
- data: { typeName },
122
- fix(fixer) {
123
- if (node.typeParameters) {
124
- // Already has type parameters, but not enough to cover TTime
125
- const lastParam = node.typeParameters.params[node.typeParameters.params.length - 1];
126
- // Check if we can just append ", Date"
127
- // We can only do this safely if all parameters between existing ones and TTime have defaults.
128
- // For simplicity and safety, we only fix if tTimeIndex is the next one or if it's already provided.
129
- if (tTimeIndex === node.typeParameters.params.length) {
130
- return fixer.insertTextAfter(lastParam, ', Date');
131
- }
132
- }
133
- else if (tTimeIndex === 0) {
134
- // No type parameters and TTime is the first one
135
- return fixer.insertTextAfter(node.typeName, '<Date>');
136
- }
137
- return null;
138
- },
215
+ sites.push({
216
+ kind: 'omitted',
217
+ reportNode: node,
218
+ reference: node,
219
+ typeName,
220
+ tTimeIndex,
139
221
  });
140
222
  }
141
223
  else if (!isExactDate(tTimeArg)) {
142
- // TTime is provided but is not exactly Date
143
- context.report({
144
- node: tTimeArg,
145
- messageId: 'enforceDateTTime',
146
- data: { typeName },
147
- fix(fixer) {
148
- return fixer.replaceText(tTimeArg, 'Date');
149
- },
224
+ sites.push({
225
+ kind: 'nonDate',
226
+ reportNode: tTimeArg,
227
+ reference: node,
228
+ typeName,
229
+ tTimeIndex,
150
230
  });
151
231
  }
152
232
  },
@@ -29,6 +29,8 @@ 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");
33
+ const importRemoval_1 = require("../utils/importRemoval");
32
34
  /**
33
35
  * Type string formatting flags chosen to keep comparisons stable and predictable.
34
36
  * - NoTruncation avoids `...` elisions that can hide important differences.
@@ -143,10 +145,15 @@ function findTypeAnnotationStart(typeAnnotation, sourceCode) {
143
145
  }
144
146
  return removalStart;
145
147
  }
146
- function removeTypeAnnotation(fixer, typeAnnotation, sourceCode) {
147
- const end = typeAnnotation.range[1];
148
- const removalStart = findTypeAnnotationStart(typeAnnotation, sourceCode);
149
- return fixer.removeRange([removalStart, end]);
148
+ /**
149
+ * The slice a fix deletes to drop `typeAnnotation`: the `:` and the whitespace
150
+ * separating it from the declared name go with the type.
151
+ */
152
+ function annotationRemovalRange(typeAnnotation, sourceCode) {
153
+ return [
154
+ findTypeAnnotationStart(typeAnnotation, sourceCode),
155
+ typeAnnotation.range[1],
156
+ ];
150
157
  }
151
158
  function typeText(type, checker) {
152
159
  return checker.typeToString(type, undefined, typeFormatFlags());
@@ -338,18 +345,79 @@ exports.noRedundantAnnotationAssertion = (0, createRule_1.createRule)({
338
345
  return {};
339
346
  }
340
347
  const checker = parserServices.program.getTypeChecker();
341
- function reportIfRedundant(annotation, assertion, reportNode, fixerTarget) {
348
+ /**
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.
356
+ */
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) {
342
366
  const matchingType = haveMatchingTypes(annotation.typeAnnotation, assertion, checker, parserServices);
343
367
  if (!matchingType)
344
368
  return;
345
- context.report({
346
- node: reportNode,
347
- messageId: 'redundantAnnotationAndAssertion',
348
- data: { type: matchingType },
349
- fix: (fixer) => removeTypeAnnotation(fixer, fixerTarget, sourceCode),
369
+ sites.push({
370
+ reportNode,
371
+ removal: annotationRemovalRange(fixerTarget, sourceCode),
372
+ matchingType,
350
373
  });
351
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
+ }
352
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
+ },
353
421
  VariableDeclarator(node) {
354
422
  if (node.id.type !== utils_1.AST_NODE_TYPES.Identifier ||
355
423
  !node.id.typeAnnotation ||
@@ -360,7 +428,7 @@ exports.noRedundantAnnotationAssertion = (0, createRule_1.createRule)({
360
428
  const assertionType = extractAssertionTypeNode(node.init);
361
429
  if (!assertionType)
362
430
  return;
363
- reportIfRedundant(node.id.typeAnnotation, assertionType, node.id, node.id.typeAnnotation);
431
+ collectIfRedundant(node.id.typeAnnotation, assertionType, node.id, node.id.typeAnnotation);
364
432
  },
365
433
  PropertyDefinition(node) {
366
434
  if (!node.typeAnnotation ||
@@ -371,7 +439,7 @@ exports.noRedundantAnnotationAssertion = (0, createRule_1.createRule)({
371
439
  const assertionType = extractAssertionTypeNode(node.value);
372
440
  if (!assertionType)
373
441
  return;
374
- reportIfRedundant(node.typeAnnotation, assertionType, node.key, node.typeAnnotation);
442
+ collectIfRedundant(node.typeAnnotation, assertionType, node.key, node.typeAnnotation);
375
443
  },
376
444
  FunctionDeclaration(node) {
377
445
  if (!node.returnType)
@@ -379,7 +447,7 @@ exports.noRedundantAnnotationAssertion = (0, createRule_1.createRule)({
379
447
  const assertionType = getReturnAssertion(node);
380
448
  if (!assertionType)
381
449
  return;
382
- reportIfRedundant(node.returnType, assertionType, node.id ?? node, node.returnType);
450
+ collectIfRedundant(node.returnType, assertionType, node.id ?? node, node.returnType);
383
451
  },
384
452
  FunctionExpression(node) {
385
453
  if (node.parent?.type === utils_1.AST_NODE_TYPES.MethodDefinition) {
@@ -390,7 +458,7 @@ exports.noRedundantAnnotationAssertion = (0, createRule_1.createRule)({
390
458
  const assertionType = getReturnAssertion(node);
391
459
  if (!assertionType)
392
460
  return;
393
- reportIfRedundant(node.returnType, assertionType, node, node.returnType);
461
+ collectIfRedundant(node.returnType, assertionType, node, node.returnType);
394
462
  },
395
463
  ArrowFunctionExpression(node) {
396
464
  if (!node.returnType)
@@ -398,7 +466,7 @@ exports.noRedundantAnnotationAssertion = (0, createRule_1.createRule)({
398
466
  const assertionType = getReturnAssertion(node);
399
467
  if (!assertionType)
400
468
  return;
401
- reportIfRedundant(node.returnType, assertionType, node, node.returnType);
469
+ collectIfRedundant(node.returnType, assertionType, node, node.returnType);
402
470
  },
403
471
  MethodDefinition(node) {
404
472
  if (!node.value.returnType)
@@ -406,7 +474,7 @@ exports.noRedundantAnnotationAssertion = (0, createRule_1.createRule)({
406
474
  const assertionType = getReturnAssertion(node);
407
475
  if (!assertionType)
408
476
  return;
409
- reportIfRedundant(node.value.returnType, assertionType, node.key, node.value.returnType);
477
+ collectIfRedundant(node.value.returnType, assertionType, node.key, node.value.returnType);
410
478
  },
411
479
  };
412
480
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.92",
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,48 @@
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
+ },
24
+ {
25
+ "version": "1.20.93",
26
+ "date": "2026-08-03T22:34:05.616Z",
27
+ "rules": [
28
+ {
29
+ "name": "enforce-date-ttime",
30
+ "changeType": "fix",
31
+ "issues": [
32
+ 1667
33
+ ],
34
+ "summary": "keep the alias it rewrites away (closes #1667)"
35
+ },
36
+ {
37
+ "name": "no-redundant-annotation-assertion",
38
+ "changeType": "fix",
39
+ "issues": [
40
+ 1666
41
+ ],
42
+ "summary": "keep the type its annotation named (closes #1666)"
43
+ }
44
+ ]
45
+ },
2
46
  {
3
47
  "version": "1.20.92",
4
48
  "date": "2026-08-03T19:30:56.799Z",