@blumintinc/eslint-plugin-blumint 1.20.88 → 1.20.89

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.88',
226
+ version: '1.20.89',
227
227
  },
228
228
  parseOptions: {
229
229
  ecmaVersion: 2020,
@@ -577,7 +577,60 @@ function checkUnionType(ts, unionType, checker, visited) {
577
577
  function isIntersectionType(ts, flags) {
578
578
  return (flags & ts.TypeFlags.Intersection) !== 0;
579
579
  }
580
+ /**
581
+ * Returns true when `type` holds a JavaScript primitive at runtime — a string,
582
+ * number, boolean, bigint, symbol, enum member, or any literal/template/mapping
583
+ * flavour of those. A union qualifies when every constituent does.
584
+ *
585
+ * Deliberately narrower than `isPrimitiveType`, which also accepts
586
+ * `null`/`undefined`/`void`/`never`: those carry no runtime *value* to reason
587
+ * about, and inside an intersection they collapse the whole type to `never`
588
+ * anyway, so admitting them would prove nothing about the surviving members.
589
+ */
590
+ function isRuntimePrimitiveType(ts, type) {
591
+ const flags = type.flags ?? 0;
592
+ if ((flags &
593
+ (ts.TypeFlags.StringLike |
594
+ ts.TypeFlags.NumberLike |
595
+ ts.TypeFlags.BooleanLike |
596
+ ts.TypeFlags.BigIntLike |
597
+ ts.TypeFlags.ESSymbolLike |
598
+ ts.TypeFlags.EnumLike)) !==
599
+ 0) {
600
+ return true;
601
+ }
602
+ // `('a' | 'b') & {}` can surface the literal union as a single member.
603
+ if ((flags & ts.TypeFlags.Union) !== 0) {
604
+ const members = type.types;
605
+ return (members.length > 0 &&
606
+ members.every((member) => isRuntimePrimitiveType(ts, member)));
607
+ }
608
+ return false;
609
+ }
610
+ /**
611
+ * Detects the open-ended literal union idiom — a primitive intersected with an
612
+ * object type, e.g. `'alert' | 'button' | (string & {})`, of which React's own
613
+ * `AriaRole` is the most widespread instance. The `& {}` exists purely to defeat
614
+ * TypeScript's literal-union widening so editors keep offering autocomplete on
615
+ * the named members; it contributes no runtime object identity.
616
+ *
617
+ * A value of `string & X` is still assignable to `string`, so it is a primitive
618
+ * at runtime no matter what `X` is. That makes `compareDeeply('role')` provably
619
+ * inert: the consuming comparator only reaches deep equality behind a
620
+ * `typeof value === 'object'` guard, and `isEqual` on two primitives is `===`
621
+ * regardless. So the reduction keys on "the intersection has a primitive
622
+ * member", not on the object member being empty — branded primitives
623
+ * (`string & { brand: 'x' }`) reduce identically, and the widener is spelled
624
+ * several ways in the wild (`{}`, `Record<never, never>`, `Record<string, never>`)
625
+ * that no structural emptiness test or name allowlist covers uniformly.
626
+ */
627
+ function isPrimitiveBackedIntersection(ts, intersectionType) {
628
+ return intersectionType.types.some((member) => isRuntimePrimitiveType(ts, member));
629
+ }
580
630
  function checkIntersectionType(ts, intersectionType, checker, visited) {
631
+ if (isPrimitiveBackedIntersection(ts, intersectionType)) {
632
+ return false;
633
+ }
581
634
  return intersectionType.types.some((t) => isComplexTypeInternal(ts, t, checker, visited));
582
635
  }
583
636
  function isPrimitiveType(ts, flags) {
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.noExplicitReturnType = 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
  const defaultOptions = {
8
9
  allowRecursiveFunctions: true,
@@ -640,6 +641,80 @@ function declaresVoidResult(returnType) {
640
641
  return (typeArguments?.length === 1 &&
641
642
  typeArguments[0].type === utils_1.AST_NODE_TYPES.TSVoidKeyword);
642
643
  }
644
+ function containsRange(outer, inner) {
645
+ return inner[0] >= outer[0] && inner[1] <= outer[1];
646
+ }
647
+ /**
648
+ * Partitions annotations into the sets whose removals have to travel together.
649
+ *
650
+ * An import read from two annotations is unbound only once both are gone, so
651
+ * neither annotation may unbind it alone — and a fix may only count on the other
652
+ * removal happening if it performs that removal itself. Annotations that jointly
653
+ * keep an import alive are therefore merged into one batch, and every other
654
+ * annotation is a batch of one, judged against the file as it stands.
655
+ *
656
+ * `candidates` must already exclude suppressed reports: a suppressed report's
657
+ * fix never runs, so its annotation — and the reference it holds — outlives the
658
+ * pass.
659
+ */
660
+ function batchAnnotations(source, candidates) {
661
+ const parents = candidates.map((_, index) => index);
662
+ const rootOf = (index) => {
663
+ let root = index;
664
+ while (parents[root] !== root) {
665
+ root = parents[root];
666
+ }
667
+ return root;
668
+ };
669
+ const ownerOf = (reference) => candidates.findIndex((candidate) => containsRange(candidate.returnType.range, reference));
670
+ for (const binding of (0, importRemoval_1.importBindingReferences)(source)) {
671
+ // A single reference cannot be shared, so the one-annotation judgement
672
+ // already covers it.
673
+ if (binding.references.length < 2)
674
+ continue;
675
+ const owners = new Set();
676
+ const escapes = binding.references.some((reference) => {
677
+ const owner = ownerOf(reference);
678
+ if (owner === -1)
679
+ return true;
680
+ owners.add(owner);
681
+ return false;
682
+ });
683
+ // A reference outside every strippable annotation survives whatever this
684
+ // pass deletes, so no batch can unbind the import.
685
+ if (escapes || owners.size < 2)
686
+ continue;
687
+ const [target, ...rest] = [...owners].map(rootOf);
688
+ for (const root of rest) {
689
+ if (root !== target) {
690
+ parents[root] = target;
691
+ }
692
+ }
693
+ }
694
+ const batches = new Map();
695
+ candidates.forEach((candidate, index) => {
696
+ const root = rootOf(index);
697
+ const batch = batches.get(root);
698
+ if (batch) {
699
+ batch.push(candidate);
700
+ }
701
+ else {
702
+ batches.set(root, [candidate]);
703
+ }
704
+ });
705
+ return [...batches.values()];
706
+ }
707
+ /**
708
+ * The ranges a single fix deletes for `batch`: the annotations themselves plus
709
+ * any import they were the last consumers of. `null` when a binding the removal
710
+ * orphans cannot be unbound safely — the caller then drops the fix rather than
711
+ * emitting the half of it that leaves a binding behind.
712
+ */
713
+ function planRemoval(source, batch) {
714
+ const annotations = batch.map((entry) => entry.returnType.range);
715
+ const imports = (0, importRemoval_1.planOrphanedImportRemoval)(source, annotations);
716
+ return imports ? [...annotations, ...imports] : null;
717
+ }
643
718
  exports.noExplicitReturnType = (0, createRule_1.createRule)({
644
719
  name: 'no-explicit-return-type',
645
720
  meta: {
@@ -677,6 +752,14 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
677
752
  const filename = context.getFilename();
678
753
  const sourceCode = context.getSourceCode();
679
754
  const visitorKeys = sourceCode.visitorKeys;
755
+ const pending = [];
756
+ /**
757
+ * Whether ESLint will discard a report, resolved the way ESLint resolves
758
+ * it. A fix that deletes several annotations at once is counting on all of
759
+ * them being reportable; a suppressed one keeps its type reference, so it
760
+ * must be left out of every batch.
761
+ */
762
+ const isReportSuppressed = (0, disableDirectives_1.createSuppressionChecker)(context);
680
763
  // Built at most once per file, and only when a direct self-reference has
681
764
  // already been ruled out.
682
765
  let returnReferenceGraph;
@@ -727,46 +810,75 @@ exports.noExplicitReturnType = (0, createRule_1.createRule)({
727
810
  declaresVoidResult(returnType));
728
811
  }
729
812
  /**
730
- * Reports an annotation, taking with it any import it was the only
731
- * consumer of. The two are one fix: applying either half alone leaves the
732
- * file worse than applying neither.
813
+ * Holds an annotation for `flushReports`, which decides once the file has
814
+ * been walked which removals may travel together.
815
+ */
816
+ function reportAnnotation(node, returnType, strippable) {
817
+ pending.push({ node, returnType, strippable });
818
+ }
819
+ /**
820
+ * Emits every held report, each carrying the import cleanup its own
821
+ * removal makes necessary. An annotation and the import it unbinds are one
822
+ * fix: applying either half alone leaves the file worse than applying
823
+ * neither.
733
824
  *
734
- * Orphanhood is judged against this annotation's own removal and the file
735
- * as it stands, never against what the rest of the `--fix` run might also
736
- * delete. A sibling annotation naming the same type may be
737
- * `eslint-disable`d which a rule cannot see, since suppression is
738
- * applied to reports after they are emitted so an edit that assumes its
739
- * sibling will also go deletes an import the surviving annotation still
740
- * references, trading an unused import for a dangling type. Judging one
741
- * edit at a time is suppression-safe by construction: a suppressed
742
- * report's fix never applies, so it can never have been depended on.
825
+ * Orphanhood is judged against a single fix's own deletions, never against
826
+ * what the rest of the `--fix` run might also delete. That is what makes
827
+ * the cleanup suppression-safe, and it is why a type several annotations
828
+ * share cannot be unbound by any one of them: a sibling annotation may be
829
+ * `eslint-disable`d, and deleting "its" import strands a reference that
830
+ * outlives the pass a compile error in place of an unused import.
743
831
  *
744
- * The cost is that a type shared by several strippable annotations is not
745
- * unbound in a single pass; each pass removes the annotations it can see,
746
- * and only a pass that leaves the binding with no reference at all
747
- * removes the import.
832
+ * Waiting for the last such annotation does not work either. Once every
833
+ * annotation is stripped the rule has nothing left to report, so no later
834
+ * fix exists to carry the cleanup and the binding stays orphaned for good
835
+ * (issue #1654). The annotations that jointly hold an import alive are
836
+ * therefore removed by one fix, which owes two things: it deletes all of
837
+ * them itself rather than assuming sibling reports land, and it counts
838
+ * only annotations whose reports ESLint will not discard.
748
839
  */
749
- function reportAnnotation(node, returnType, strippable) {
750
- const importRanges = strippable
751
- ? (0, importRemoval_1.planOrphanedImportRemoval)(sourceCode, [returnType.range])
752
- : null;
753
- context.report({
754
- node: returnType,
755
- messageId: strippable
756
- ? 'noExplicitReturnTypeInferable'
757
- : 'noExplicitReturnTypeNonInferable',
758
- data: { functionKind: describeFunctionKind(node) },
759
- ...(importRanges
760
- ? {
761
- fix: (fixer) => [
762
- fixer.remove(returnType),
763
- ...importRanges.map((range) => fixer.removeRange([range[0], range[1]])),
764
- ],
765
- }
766
- : {}),
767
- });
840
+ function flushReports() {
841
+ const strippable = pending.filter((entry) => entry.strippable);
842
+ // Every annotation starts with the judgement it would get alone, so a
843
+ // batch can only ever add a cleanup, never withdraw one.
844
+ const plans = new Map();
845
+ for (const entry of strippable) {
846
+ const plan = planRemoval(sourceCode, [entry]);
847
+ if (plan) {
848
+ plans.set(entry, plan);
849
+ }
850
+ }
851
+ const batches = batchAnnotations(sourceCode, strippable.filter((entry) => !isReportSuppressed(entry.returnType)));
852
+ for (const batch of batches) {
853
+ if (batch.length < 2)
854
+ continue;
855
+ const plan = planRemoval(sourceCode, batch);
856
+ if (!plan)
857
+ continue;
858
+ for (const entry of batch) {
859
+ plans.set(entry, plan);
860
+ }
861
+ }
862
+ for (const entry of pending) {
863
+ const plan = plans.get(entry);
864
+ context.report({
865
+ node: entry.returnType,
866
+ messageId: entry.strippable
867
+ ? 'noExplicitReturnTypeInferable'
868
+ : 'noExplicitReturnTypeNonInferable',
869
+ data: { functionKind: describeFunctionKind(entry.node) },
870
+ ...(plan
871
+ ? {
872
+ fix: (fixer) => plan.map((range) => fixer.removeRange([range[0], range[1]])),
873
+ }
874
+ : {}),
875
+ });
876
+ }
768
877
  }
769
878
  return {
879
+ 'Program:exit'() {
880
+ flushReports();
881
+ },
770
882
  FunctionDeclaration(node) {
771
883
  const returnType = node.returnType;
772
884
  if (!returnType)
@@ -3,33 +3,30 @@ 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
- function isIdentifierWithTypeAnnotation(node) {
7
- return (node.type === utils_1.AST_NODE_TYPES.Identifier && node.typeAnnotation !== undefined);
8
- }
9
- function isRestElementWithTypeAnnotation(node) {
10
- return (node.type === utils_1.AST_NODE_TYPES.RestElement &&
11
- node.typeAnnotation !== undefined);
12
- }
13
- function isObjectPatternWithTypeAnnotation(node) {
14
- return (node.type === utils_1.AST_NODE_TYPES.ObjectPattern &&
15
- node.typeAnnotation !== undefined);
16
- }
17
- function isArrayPatternWithTypeAnnotation(node) {
18
- return (node.type === utils_1.AST_NODE_TYPES.ArrayPattern &&
19
- node.typeAnnotation !== undefined);
20
- }
21
- function isAssignmentPatternWithTypeAnnotation(node) {
22
- return (node.type === utils_1.AST_NODE_TYPES.AssignmentPattern &&
23
- node.left.type === utils_1.AST_NODE_TYPES.Identifier &&
24
- node.left.typeAnnotation !== undefined);
6
+ const importRemoval_1 = require("../utils/importRemoval");
7
+ /**
8
+ * The annotation a parameter carries, or `undefined` when it has none. A
9
+ * parameter with a default value holds its annotation on the pattern it assigns
10
+ * to, and only an identifier pattern is read there: a destructured parameter
11
+ * with a default keeps its annotation.
12
+ */
13
+ function annotationOf(param) {
14
+ if (param.type === utils_1.AST_NODE_TYPES.AssignmentPattern) {
15
+ return param.left.type === utils_1.AST_NODE_TYPES.Identifier
16
+ ? param.left.typeAnnotation
17
+ : undefined;
18
+ }
19
+ return param.typeAnnotation;
25
20
  }
26
- function removeTypeAnnotation(fixer, typeAnnotation, sourceCode) {
27
- const typeStart = typeAnnotation.range[0];
28
- const typeEnd = typeAnnotation.range[1];
29
- // Check if there's a question mark before the type annotation
21
+ /**
22
+ * The slice a fix deletes to drop `typeAnnotation`. An optional marker goes with
23
+ * it: contextual typing supplies a parameter's optionality along with its type,
24
+ * so a `?` left behind keeps half of the duplication the rule exists to remove.
25
+ */
26
+ function annotationRemovalRange(typeAnnotation, sourceCode) {
27
+ const [typeStart, typeEnd] = typeAnnotation.range;
30
28
  const hasQuestionMark = typeStart > 0 && sourceCode.getText().charAt(typeStart - 1) === '?';
31
- const startPos = hasQuestionMark ? typeStart - 1 : typeStart;
32
- return fixer.removeRange([startPos, typeEnd]);
29
+ return [hasQuestionMark ? typeStart - 1 : typeStart, typeEnd];
33
30
  }
34
31
  function hasRedundantTypeAnnotation(node) {
35
32
  const parent = node.parent;
@@ -69,67 +66,58 @@ exports.noRedundantParamTypes = (0, createRule_1.createRule)({
69
66
  },
70
67
  defaultOptions: [],
71
68
  create(context) {
69
+ const sourceCode = context.getSourceCode();
70
+ /**
71
+ * Reports a parameter annotation, taking with it any import it was the only
72
+ * consumer of. The two are one fix: applying either half alone leaves the
73
+ * file worse than applying neither — a stripped annotation with its import
74
+ * left behind fails `no-unused-vars`, and since this rule's own report is
75
+ * resolved by the fix, nothing re-reports the debt.
76
+ *
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.
86
+ *
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.
91
+ */
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
+ });
112
+ }
72
113
  return {
73
114
  ArrowFunctionExpression(node) {
74
115
  if (!hasRedundantTypeAnnotation(node))
75
116
  return;
76
- const params = node.params;
77
- const sourceCode = context.getSourceCode();
78
- params.forEach((param) => {
79
- const paramText = sourceCode
80
- .getText(param)
81
- .replace(/\s+/g, ' ')
82
- .trim();
83
- if (isIdentifierWithTypeAnnotation(param)) {
84
- context.report({
85
- node: param,
86
- messageId: 'redundantParamType',
87
- data: { paramText },
88
- fix(fixer) {
89
- return removeTypeAnnotation(fixer, param.typeAnnotation, sourceCode);
90
- },
91
- });
92
- }
93
- else if (isRestElementWithTypeAnnotation(param)) {
94
- context.report({
95
- node: param,
96
- messageId: 'redundantParamType',
97
- data: { paramText },
98
- fix(fixer) {
99
- return removeTypeAnnotation(fixer, param.typeAnnotation, sourceCode);
100
- },
101
- });
102
- }
103
- else if (isObjectPatternWithTypeAnnotation(param)) {
104
- context.report({
105
- node: param,
106
- messageId: 'redundantParamType',
107
- data: { paramText },
108
- fix(fixer) {
109
- return removeTypeAnnotation(fixer, param.typeAnnotation, sourceCode);
110
- },
111
- });
112
- }
113
- else if (isArrayPatternWithTypeAnnotation(param)) {
114
- context.report({
115
- node: param,
116
- messageId: 'redundantParamType',
117
- data: { paramText },
118
- fix(fixer) {
119
- return removeTypeAnnotation(fixer, param.typeAnnotation, sourceCode);
120
- },
121
- });
122
- }
123
- else if (isAssignmentPatternWithTypeAnnotation(param)) {
124
- const { left } = param;
125
- context.report({
126
- node: param,
127
- messageId: 'redundantParamType',
128
- data: { paramText },
129
- fix(fixer) {
130
- return removeTypeAnnotation(fixer, left.typeAnnotation, sourceCode);
131
- },
132
- });
117
+ node.params.forEach((param) => {
118
+ const typeAnnotation = annotationOf(param);
119
+ if (typeAnnotation) {
120
+ reportParam(param, typeAnnotation);
133
121
  }
134
122
  });
135
123
  },
@@ -136,14 +136,25 @@ exports.noTypeAssertionReturns = (0, createRule_1.createRule)({
136
136
  function isInsideJSXAttributeOrObjectProperty(node) {
137
137
  let current = node;
138
138
  while (current?.parent) {
139
- // Direct JSX attribute
140
- if (current.parent.type === utils_1.AST_NODE_TYPES.JSXAttribute) {
139
+ // Direct JSX attribute, in either spelling. A named attribute
140
+ // (`title={x as T}`) and a spread (`{...(x as T)}`) feed the receiving
141
+ // component's props identically, so they earn the same carve-out: what
142
+ // the enclosing function returns is a JSXElement, never the asserted
143
+ // value, and the object is re-checked against the component's prop
144
+ // types at the JSX call site. This is the argument already accepted for
145
+ // call and new arguments above.
146
+ if (current.parent.type === utils_1.AST_NODE_TYPES.JSXAttribute ||
147
+ current.parent.type === utils_1.AST_NODE_TYPES.JSXSpreadAttribute) {
141
148
  return true;
142
149
  }
143
150
  // Object property (which could be JSX props)
144
151
  if (current.parent.type === utils_1.AST_NODE_TYPES.Property) {
145
152
  return true;
146
153
  }
154
+ // A SpreadElement is deliberately absent: `return { ...(x as T) }`
155
+ // splices the asserted value's own members into the returned value, so
156
+ // the cast is exactly the unvalidated data reaching callers that this
157
+ // rule exists to catch. Unlike a JSX spread, nothing re-checks it.
147
158
  current = current.parent;
148
159
  }
149
160
  return false;
@@ -34,6 +34,22 @@ export declare function removeImportBindingFixes(source: ImportRemovalSource, fi
34
34
  * its own rather than a specifier and is left alone.
35
35
  */
36
36
  export declare function importBindingSpecifierOf(variable: TSESLint.Scope.Variable): ImportBindingSpecifier | undefined;
37
+ /** An import binding together with the ranges that reference it. */
38
+ export type ImportBindingUse = {
39
+ specifier: ImportBindingSpecifier;
40
+ references: TextRange[];
41
+ };
42
+ /**
43
+ * Every import binding the file declares, paired with the positions that read
44
+ * it.
45
+ *
46
+ * A caller deleting several regions at once needs this to decide *which* of its
47
+ * own deletions a binding depends on before asking
48
+ * {@link planOrphanedImportRemoval} to unbind it: a binding read from two
49
+ * regions is unbound only if both go, and only a caller that deletes both in one
50
+ * fix may claim that.
51
+ */
52
+ export declare function importBindingReferences(source: ImportRemovalSource): ImportBindingUse[];
37
53
  /**
38
54
  * The extra ranges a fix must delete so that removing `removed` leaves nothing
39
55
  * bound to nothing, or `null` when some binding would be orphaned yet cannot be
@@ -45,13 +61,20 @@ export declare function importBindingSpecifierOf(variable: TSESLint.Scope.Variab
45
61
  * file that lints clean into one that fails `no-unused-vars`, and since the
46
62
  * original report is resolved by the fix, nothing re-reports the debt.
47
63
  *
48
- * `removed` is one fix's own deletion, judged against the file as it stands.
49
- * Widening it to "everything this `--fix` run will delete" looks like it would
50
- * catch a binding two edits share, but it is unsound: a rule cannot see
51
- * `eslint-disable` comments, because suppression is applied to reports after the
52
- * rule emits them. A suppressed sibling edit never happens, so the widened view
53
- * deletes an import that a surviving reference still needs trading an unused
54
- * import for a dangling type reference, a lint warning for a compile error.
55
- * There is deliberately no way to ask this helper that question.
64
+ * `removed` is **one fix's own deletion**, judged against the file as it stands.
65
+ * Several ranges may be passed only when a single fix deletes all of them, since
66
+ * ESLint applies a fix whole or not at all. Passing ranges that belong to other
67
+ * reports is unsound in two ways: a sibling report may be suppressed (ESLint
68
+ * applies `eslint-disable` after the rule emits its reports, so the sibling's fix
69
+ * never runs), and a sibling fix that overlaps another rule's fix is dropped for
70
+ * the pass. Either way the surviving reference outlives the import this helper
71
+ * was told to unbind trading an unused import for a dangling type reference, a
72
+ * lint warning for a compile error.
73
+ *
74
+ * A caller batching several of its own edits therefore owes two things: the
75
+ * edits must ship as one fix, and the reports they came from must be checked
76
+ * with `createSuppressionChecker` so that a suppressed one is never counted on.
77
+ * {@link importBindingReferences} exposes the reference sets such a caller needs
78
+ * to work out which edits belong in the same batch.
56
79
  */
57
80
  export declare function planOrphanedImportRemoval(source: ImportRemovalSource, removed: readonly TextRange[]): TextRange[] | null;
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.planOrphanedImportRemoval = exports.importBindingSpecifierOf = exports.removeImportBindingFixes = exports.planImportBindingRemoval = void 0;
3
+ exports.planOrphanedImportRemoval = exports.importBindingReferences = exports.importBindingSpecifierOf = exports.removeImportBindingFixes = exports.planImportBindingRemoval = void 0;
4
4
  const utils_1 = require("@typescript-eslint/utils");
5
5
  const disableDirectives_1 = require("./disableDirectives");
6
6
  function contains(outer, inner) {
@@ -221,6 +221,32 @@ function allVariables(scopeManager) {
221
221
  }
222
222
  return variables;
223
223
  }
224
+ /**
225
+ * Every import binding the file declares, paired with the positions that read
226
+ * it.
227
+ *
228
+ * A caller deleting several regions at once needs this to decide *which* of its
229
+ * own deletions a binding depends on before asking
230
+ * {@link planOrphanedImportRemoval} to unbind it: a binding read from two
231
+ * regions is unbound only if both go, and only a caller that deletes both in one
232
+ * fix may claim that.
233
+ */
234
+ function importBindingReferences(source) {
235
+ const uses = [];
236
+ for (const variable of allVariables(source.scopeManager)) {
237
+ if (!isImportBindingVariable(variable))
238
+ continue;
239
+ const specifier = importBindingSpecifierOf(variable);
240
+ if (!specifier)
241
+ continue;
242
+ uses.push({
243
+ specifier,
244
+ references: variable.references.map((reference) => reference.identifier.range),
245
+ });
246
+ }
247
+ return uses;
248
+ }
249
+ exports.importBindingReferences = importBindingReferences;
224
250
  function isAstNode(value) {
225
251
  return (typeof value === 'object' &&
226
252
  value !== null &&
@@ -329,14 +355,21 @@ function exportedBindingKeys(ast) {
329
355
  * file that lints clean into one that fails `no-unused-vars`, and since the
330
356
  * original report is resolved by the fix, nothing re-reports the debt.
331
357
  *
332
- * `removed` is one fix's own deletion, judged against the file as it stands.
333
- * Widening it to "everything this `--fix` run will delete" looks like it would
334
- * catch a binding two edits share, but it is unsound: a rule cannot see
335
- * `eslint-disable` comments, because suppression is applied to reports after the
336
- * rule emits them. A suppressed sibling edit never happens, so the widened view
337
- * deletes an import that a surviving reference still needs trading an unused
338
- * import for a dangling type reference, a lint warning for a compile error.
339
- * There is deliberately no way to ask this helper that question.
358
+ * `removed` is **one fix's own deletion**, judged against the file as it stands.
359
+ * Several ranges may be passed only when a single fix deletes all of them, since
360
+ * ESLint applies a fix whole or not at all. Passing ranges that belong to other
361
+ * reports is unsound in two ways: a sibling report may be suppressed (ESLint
362
+ * applies `eslint-disable` after the rule emits its reports, so the sibling's fix
363
+ * never runs), and a sibling fix that overlaps another rule's fix is dropped for
364
+ * the pass. Either way the surviving reference outlives the import this helper
365
+ * was told to unbind trading an unused import for a dangling type reference, a
366
+ * lint warning for a compile error.
367
+ *
368
+ * A caller batching several of its own edits therefore owes two things: the
369
+ * edits must ship as one fix, and the reports they came from must be checked
370
+ * with `createSuppressionChecker` so that a suppressed one is never counted on.
371
+ * {@link importBindingReferences} exposes the reference sets such a caller needs
372
+ * to work out which edits belong in the same batch.
340
373
  */
341
374
  function planOrphanedImportRemoval(source, removed) {
342
375
  const orphaned = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blumintinc/eslint-plugin-blumint",
3
- "version": "1.20.88",
3
+ "version": "1.20.89",
4
4
  "description": "Custom eslint rules for use within BluMint",
5
5
  "author": {
6
6
  "name": "Brodie McGuire",
@@ -1,4 +1,42 @@
1
1
  [
2
+ {
3
+ "version": "1.20.89",
4
+ "date": "2026-08-03T12:31:01.823Z",
5
+ "rules": [
6
+ {
7
+ "name": "memo-compare-deeply-complex-props",
8
+ "changeType": "fix",
9
+ "issues": [
10
+ 1656
11
+ ],
12
+ "summary": "treat a primitive-backed intersection as primitive, not complex (closes #1656)"
13
+ },
14
+ {
15
+ "name": "no-explicit-return-type",
16
+ "changeType": "fix",
17
+ "issues": [
18
+ 1654
19
+ ],
20
+ "summary": "unbind an import when the annotations jointly keeping it alive are all stripped (closes #1654)"
21
+ },
22
+ {
23
+ "name": "no-redundant-param-types",
24
+ "changeType": "fix",
25
+ "issues": [
26
+ 1653
27
+ ],
28
+ "summary": "remove the type imports a stripped parameter annotation was the sole consumer of (closes #1653)"
29
+ },
30
+ {
31
+ "name": "no-type-assertion-returns",
32
+ "changeType": "fix",
33
+ "issues": [
34
+ 1655
35
+ ],
36
+ "summary": "exempt type assertions in JSX spread attributes (closes #1655)"
37
+ }
38
+ ]
39
+ },
2
40
  {
3
41
  "version": "1.20.88",
4
42
  "date": "2026-08-03T09:05:50.265Z",