@blumintinc/eslint-plugin-blumint 1.21.3 → 1.21.4
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
|
@@ -140,6 +140,32 @@ function attributePrintedName(intended, printed, type) {
|
|
|
140
140
|
}
|
|
141
141
|
recordAttribution(intended, printed, symbol);
|
|
142
142
|
}
|
|
143
|
+
/**
|
|
144
|
+
* The bare identifier a declaration's own type annotation is written as, or
|
|
145
|
+
* null when the annotation is anything else.
|
|
146
|
+
*
|
|
147
|
+
* Only an un-parameterised reference to a plain identifier qualifies. A
|
|
148
|
+
* qualified name (`Theme.Mode`) needs its namespace to resolve at the fix site,
|
|
149
|
+
* which this answer cannot establish; a parameterised one (`Alias<'x'>`) is not
|
|
150
|
+
* denoted by its head at all, and emitting the head alone would name a
|
|
151
|
+
* different type. Both keep the printed union rather than guess.
|
|
152
|
+
*/
|
|
153
|
+
function annotatedTypeReferenceName(declaration) {
|
|
154
|
+
const annotation = ts.isParameter(declaration) ||
|
|
155
|
+
ts.isVariableDeclaration(declaration) ||
|
|
156
|
+
ts.isPropertySignature(declaration) ||
|
|
157
|
+
ts.isPropertyDeclaration(declaration)
|
|
158
|
+
? declaration.type
|
|
159
|
+
: undefined;
|
|
160
|
+
if (!annotation ||
|
|
161
|
+
!ts.isTypeReferenceNode(annotation) ||
|
|
162
|
+
!ts.isIdentifier(annotation.typeName) ||
|
|
163
|
+
(annotation.typeArguments?.length ?? 0) > 0) {
|
|
164
|
+
return null;
|
|
165
|
+
}
|
|
166
|
+
const name = annotation.typeName.text;
|
|
167
|
+
return BARE_TYPE_NAME.test(name) ? name : null;
|
|
168
|
+
}
|
|
143
169
|
/**
|
|
144
170
|
* Function/constructor/conditional type notation must be parenthesized to
|
|
145
171
|
* appear as a `|` union member, or the emitted annotation does not parse
|
|
@@ -833,11 +859,75 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
833
859
|
return null;
|
|
834
860
|
}
|
|
835
861
|
}
|
|
862
|
+
/**
|
|
863
|
+
* The bare type-alias name written on the discriminant's own annotation,
|
|
864
|
+
* when that name is provably the union the checker printed the members of.
|
|
865
|
+
*
|
|
866
|
+
* `checker.typeToString()` keeps an alias only while the type still carries
|
|
867
|
+
* an `aliasSymbol`, and TypeScript drops that resolving an indexed access
|
|
868
|
+
* over a `typeof` query — `const M = ['a', 'b'] as const; type Mode =
|
|
869
|
+
* (typeof M)[number]`, which is exactly the shape the key-space companion
|
|
870
|
+
* rule named on this rule's docs page emits. The printed text there is the
|
|
871
|
+
* widened literal union, the one spelling this rule's exhaustiveness
|
|
872
|
+
* promise exists to avoid: a stale `Record<'a' | 'b', V>` keeps
|
|
873
|
+
* typechecking after the union grows and only the LOOKUP complains, as
|
|
874
|
+
* TS7053 — an implicit-any diagnostic that disappears entirely under
|
|
875
|
+
* `noImplicitAny: false` and leaves the new member yielding `undefined` at
|
|
876
|
+
* runtime (#2009).
|
|
877
|
+
*
|
|
878
|
+
* The name is read off SYNTAX because the type no longer carries it, so it
|
|
879
|
+
* ships only against evidence that it denotes this very union: a bare,
|
|
880
|
+
* un-parameterised type reference written on the declaration the
|
|
881
|
+
* discriminant resolves to, whose symbol — resolved where the `Record`
|
|
882
|
+
* lands, not where the annotation was written — declares exactly this type.
|
|
883
|
+
* Mere presence of the name is not enough. Naming the WRONG union in the
|
|
884
|
+
* emitted `Record` breaks the build where the widened union merely weakens
|
|
885
|
+
* it, so anything short of type identity declines and keeps the printed
|
|
886
|
+
* text. Flow narrowing declines through the same check: a discriminant
|
|
887
|
+
* narrowed above the dispatch has a type the alias does not declare, and
|
|
888
|
+
* the alias would demand keys the construct has no branches for.
|
|
889
|
+
*/
|
|
890
|
+
function annotationAliasTypeText(discriminant, discriminantType) {
|
|
891
|
+
const tsNode = esTreeNodeToTSNodeMap.get(unwrapLink(discriminant));
|
|
892
|
+
const tsFixSite = esTreeNodeToTSNodeMap.get(discriminant);
|
|
893
|
+
if (!tsNode || !tsFixSite) {
|
|
894
|
+
return null;
|
|
895
|
+
}
|
|
896
|
+
try {
|
|
897
|
+
const symbol = checker.getSymbolAtLocation(tsNode);
|
|
898
|
+
const names = new Set((symbol?.declarations ?? [])
|
|
899
|
+
.map(annotatedTypeReferenceName)
|
|
900
|
+
.filter((name) => name !== null));
|
|
901
|
+
// Two declarations annotated with different names (a merged symbol, an
|
|
902
|
+
// overload set) leave no single spelling that is right for both.
|
|
903
|
+
if (names.size !== 1) {
|
|
904
|
+
return null;
|
|
905
|
+
}
|
|
906
|
+
const [name] = names;
|
|
907
|
+
const inScope = checker
|
|
908
|
+
.getSymbolsInScope(tsFixSite, ts.SymbolFlags.Type | ts.SymbolFlags.Alias)
|
|
909
|
+
.find((candidate) => candidate.name === name);
|
|
910
|
+
if (!inScope) {
|
|
911
|
+
return null;
|
|
912
|
+
}
|
|
913
|
+
const declaredSymbol = inScope.flags & ts.SymbolFlags.Alias
|
|
914
|
+
? checker.getAliasedSymbol(inScope)
|
|
915
|
+
: inScope;
|
|
916
|
+
if (checker.getDeclaredTypeOfSymbol(declaredSymbol) !== discriminantType) {
|
|
917
|
+
return null;
|
|
918
|
+
}
|
|
919
|
+
return { text: name, symbol: declaredSymbol };
|
|
920
|
+
}
|
|
921
|
+
catch {
|
|
922
|
+
return null;
|
|
923
|
+
}
|
|
924
|
+
}
|
|
836
925
|
/**
|
|
837
926
|
* Key type for the emitted `Record`. A discriminant whose own type prints
|
|
838
927
|
* as a bare name already carries the union's identity, so that name is
|
|
839
|
-
* kept; otherwise the fix reaches for the tag's declaring type
|
|
840
|
-
*
|
|
928
|
+
* kept; otherwise the fix reaches for the tag's declaring type (#1926),
|
|
929
|
+
* then for the alias its annotation names where the checker resolved past
|
|
930
|
+
* one (#2009), before settling for the resolved literal union.
|
|
841
931
|
*/
|
|
842
932
|
function discriminantTypeText(type, discriminant) {
|
|
843
933
|
let printed;
|
|
@@ -865,10 +955,27 @@ exports.preferMapOverConditionalDispatch = (0, createRule_1.createRule)({
|
|
|
865
955
|
// already ships only when the name resolves, at the fix site, to exactly
|
|
866
956
|
// the object's declared type, and identity of the TYPE accepts a nearer
|
|
867
957
|
// alias that denotes the same type — which compiles, and reads the same.
|
|
868
|
-
const
|
|
869
|
-
|
|
958
|
+
const indexed = indexedAccessTypeText(discriminant, type);
|
|
959
|
+
if (indexed !== null) {
|
|
960
|
+
return { text: normalizeTypeQuotes(indexed, singleQuote), intended };
|
|
961
|
+
}
|
|
962
|
+
// Recovery runs only where the printer already produced usable text, so
|
|
963
|
+
// it can only change the SPELLING of a key type the rule was going to
|
|
964
|
+
// emit anyway — never whether the rule fires. A discriminant the printer
|
|
965
|
+
// could not print at all stays silent exactly as before.
|
|
966
|
+
if (printed !== null) {
|
|
967
|
+
const alias = annotationAliasTypeText(discriminant, type);
|
|
968
|
+
if (alias) {
|
|
969
|
+
// Unlike the printed bare name, this one was never checked against
|
|
970
|
+
// the fix site's scope by the printer, so it carries its symbol into
|
|
971
|
+
// the gate for the same shadow check (#2229).
|
|
972
|
+
recordAttribution(intended, alias.text, alias.symbol);
|
|
973
|
+
return { text: alias.text, intended };
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return printed === null
|
|
870
977
|
? null
|
|
871
|
-
: { text: normalizeTypeQuotes(
|
|
978
|
+
: { text: normalizeTypeQuotes(printed, singleQuote), intended };
|
|
872
979
|
}
|
|
873
980
|
/**
|
|
874
981
|
* Whether the binding a type name reaches at the fix site is the symbol the
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.21.4",
|
|
4
|
+
"date": "2026-09-04T11:47:38.427Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "prefer-map-over-conditional-dispatch",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2009
|
|
11
|
+
],
|
|
12
|
+
"summary": "keep the alias when the checker resolves past it (closes #2009)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
2
16
|
{
|
|
3
17
|
"version": "1.21.3",
|
|
4
18
|
"date": "2026-09-03T23:34:34.310Z",
|