@blumintinc/eslint-plugin-blumint 1.20.94 → 1.20.95

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.94',
226
+ version: '1.20.95',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.noRedundantParamTypes = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const createRule_1 = require("../utils/createRule");
6
+ const disableDirectives_1 = require("../utils/disableDirectives");
6
7
  const importRemoval_1 = require("../utils/importRemoval");
7
8
  /**
8
9
  * The annotation a parameter carries, or `undefined` when it has none. A
@@ -68,57 +69,88 @@ exports.noRedundantParamTypes = (0, createRule_1.createRule)({
68
69
  create(context) {
69
70
  const sourceCode = context.getSourceCode();
70
71
  /**
71
- * Reports a parameter annotation, taking with it any import it was the only
72
+ * An annotation is stripped together with any import it was the only
72
73
  * consumer of. The two are one fix: applying either half alone leaves the
73
74
  * file worse than applying neither — a stripped annotation with its import
74
75
  * left behind fails `no-unused-vars`, and since this rule's own report is
75
76
  * resolved by the fix, nothing re-reports the debt.
76
77
  *
77
- * Orphanhood is judged against this one annotation's own removal and the
78
- * file as it stands, never against what the rest of the `--fix` run might
79
- * also delete. A sibling annotation naming the same type may be
80
- * `eslint-disable`d which a rule cannot see, since suppression is applied
81
- * to reports after they are emitted so an edit that assumes its sibling
82
- * will also go deletes an import the surviving annotation still references,
83
- * trading an unused import for a dangling type. Judging one edit at a time
84
- * is suppression-safe by construction: a suppressed report's fix never
85
- * applies, so it can never have been depended on.
78
+ * Reporting is therefore deferred to `Program:exit`: an import is unbound
79
+ * only once no reference to it survives the fix, and a file where two
80
+ * annotations name the same imported type strips both in a single pass.
81
+ * Judging each removal alone sees the sibling annotation still standing,
82
+ * concludes the binding is alive, and leaves the import stranded with no
83
+ * later pass to notice.
84
+ */
85
+ const sites = [];
86
+ /**
87
+ * Suppression is applied to reports after a rule emits them, so a suppressed
88
+ * site keeps its annotation while losing its fix. Counting its removal
89
+ * toward orphanhood would unbind an import the surviving text still spells,
90
+ * trading an unused import for a dangling type.
91
+ */
92
+ const isReportSuppressed = (0, disableDirectives_1.createSuppressionChecker)(context);
93
+ /**
94
+ * The strips that actually ship. A site is excluded when its report will be
95
+ * suppressed, or when its own removal orphans something the helper cannot
96
+ * rewrite — a local alias, an interface, a type parameter. Deleting a
97
+ * declaration is a materially riskier edit than dropping an import
98
+ * specifier, and the author is better placed to decide whether the type
99
+ * should go or be used elsewhere.
86
100
  *
87
- * The cost is that a type shared by several strippable annotations is not
88
- * unbound in a single pass; each pass removes the annotations it can see,
89
- * and only a pass that leaves the binding with no reference at all removes
90
- * the import.
101
+ * Screening individually before batching keeps one unfixable site from
102
+ * vetoing the rest: orphanhood grows monotonically with the removed set, so
103
+ * a site that cannot be planned alone can only ever poison the batch.
91
104
  */
92
- function reportParam(param, typeAnnotation) {
93
- const removal = annotationRemovalRange(typeAnnotation, sourceCode);
94
- const importRanges = (0, importRemoval_1.planOrphanedImportRemoval)(sourceCode, [removal]);
95
- context.report({
96
- node: param,
97
- messageId: 'redundantParamType',
98
- data: {
99
- paramText: sourceCode.getText(param).replace(/\s+/g, ' ').trim(),
100
- },
101
- // No plan means no binding can be unbound safely, so the annotation
102
- // stays too: the report without a fixer is the lesser damage.
103
- ...(importRanges
104
- ? {
105
- fix: (fixer) => [
106
- fixer.removeRange([removal[0], removal[1]]),
107
- ...importRanges.map((range) => fixer.removeRange([range[0], range[1]])),
108
- ],
109
- }
110
- : {}),
111
- });
105
+ function selectFixableSites() {
106
+ return sites.filter((site) => !isReportSuppressed(site.param) &&
107
+ (0, importRemoval_1.planOrphanedImportRemoval)(sourceCode, [site.removal]) !== null);
112
108
  }
113
109
  return {
110
+ 'Program:exit'() {
111
+ if (sites.length === 0)
112
+ return;
113
+ const fixable = selectFixableSites();
114
+ const removals = fixable.map((site) => site.removal);
115
+ // One plan over every surviving strip: an import referenced solely by
116
+ // annotations that all go in this pass is orphaned by their union, even
117
+ // though no single one of them orphans it.
118
+ const importRanges = removals.length > 0
119
+ ? (0, importRemoval_1.planOrphanedImportRemoval)(sourceCode, removals)
120
+ : null;
121
+ // The whole batch ships as one fix, so no strip can land without the
122
+ // others that the import's orphanhood was judged against. The rest
123
+ // report without a fixer; the carrier's pass already resolves them.
124
+ //
125
+ // No plan at all means no binding can be unbound safely, so every
126
+ // annotation stays: reports without a fixer are the lesser damage.
127
+ const carrier = importRanges ? fixable[0] : undefined;
128
+ for (const site of sites) {
129
+ context.report({
130
+ node: site.param,
131
+ messageId: 'redundantParamType',
132
+ data: { paramText: site.paramText },
133
+ fix: site === carrier && importRanges
134
+ ? (fixer) => [
135
+ ...removals.map((range) => fixer.removeRange([range[0], range[1]])),
136
+ ...importRanges.map((range) => fixer.removeRange([range[0], range[1]])),
137
+ ]
138
+ : null,
139
+ });
140
+ }
141
+ },
114
142
  ArrowFunctionExpression(node) {
115
143
  if (!hasRedundantTypeAnnotation(node))
116
144
  return;
117
145
  node.params.forEach((param) => {
118
146
  const typeAnnotation = annotationOf(param);
119
- if (typeAnnotation) {
120
- reportParam(param, typeAnnotation);
121
- }
147
+ if (!typeAnnotation)
148
+ return;
149
+ sites.push({
150
+ param,
151
+ removal: annotationRemovalRange(typeAnnotation, sourceCode),
152
+ paramText: sourceCode.getText(param).replace(/\s+/g, ' ').trim(),
153
+ });
122
154
  });
123
155
  },
124
156
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.94",
3
+ "version": "1.20.95",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,18 @@
1
1
  [
2
+ {
3
+ "version": "1.20.95",
4
+ "date": "2026-08-03T23:44:33.733Z",
5
+ "rules": [
6
+ {
7
+ "name": "no-redundant-param-types",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1670
11
+ ],
12
+ "summary": "unbind the import two annotations share (closes #1670)"
13
+ }
14
+ ]
15
+ },
2
16
  {
3
17
  "version": "1.20.94",
4
18
  "date": "2026-08-03T22:57:52.839Z",