@blumintinc/eslint-plugin-blumint 1.20.179 → 1.20.181
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
|
@@ -1316,7 +1316,8 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
1316
1316
|
const reportWrittenKey = (written) => reportUseAssertSafe(written, context.sourceCode.getText(written));
|
|
1317
1317
|
/**
|
|
1318
1318
|
* Returns true when the identifier was initialized directly from an
|
|
1319
|
-
* assertSafe(...) call, e.g. `const safeKey = assertSafe(rawKey)
|
|
1319
|
+
* assertSafe(...) call, e.g. `const safeKey = assertSafe(rawKey)`, with any
|
|
1320
|
+
* wrappers that erase before the code runs read through.
|
|
1320
1321
|
* Only direct, single-step initializers count — transitive aliases
|
|
1321
1322
|
* (const b = a) are not followed so they continue to be flagged.
|
|
1322
1323
|
* findVariableInScope returns the nearest binding, so an inner variable
|
|
@@ -1328,11 +1329,20 @@ exports.enforceAssertSafeObjectKey = (0, createRule_1.createRule)({
|
|
|
1328
1329
|
if (!variable)
|
|
1329
1330
|
return false;
|
|
1330
1331
|
return variable.defs.some((def) => {
|
|
1331
|
-
//
|
|
1332
|
-
//
|
|
1333
|
-
//
|
|
1332
|
+
// The initializer is read through the same peel the index site uses, so
|
|
1333
|
+
// the two arms agree about one expression: `assertSafe(rawKey) as
|
|
1334
|
+
// TokenEncoded` binds the very key `assertSafe(rawKey)` does, the
|
|
1335
|
+
// assertion having erased before the code runs, and `assertSafe?.(...)`
|
|
1336
|
+
// is the same validated key again since the chain guards only a nullish
|
|
1337
|
+
// callee. Demanding a bare call instead rejected the shape this rule's
|
|
1338
|
+
// own remedy produces in typed code, where `Object.keys` widens to
|
|
1339
|
+
// `string` and the record is keyed by a branded type (#2152).
|
|
1340
|
+
//
|
|
1341
|
+
// That peel includes `await`. assertSafe is synchronous, so awaiting it
|
|
1342
|
+
// resolves to the very value it validated — the same reason the index
|
|
1343
|
+
// arm has always exempted `m[await assertSafe(k)]`.
|
|
1334
1344
|
const init = def.node.type === utils_1.AST_NODE_TYPES.VariableDeclarator && def.node.init
|
|
1335
|
-
?
|
|
1345
|
+
? unwrapWrittenKey(def.node.init)
|
|
1336
1346
|
: null;
|
|
1337
1347
|
return (!!init &&
|
|
1338
1348
|
init.type === utils_1.AST_NODE_TYPES.CallExpression &&
|
|
@@ -46,11 +46,42 @@ exports.enforceIdCapitalization = (0, createRule_1.createRule)({
|
|
|
46
46
|
// A single identifier token — no whitespace, no punctuation. Prose is a
|
|
47
47
|
// phrase, so anything matching this is a name rather than displayed text.
|
|
48
48
|
const IDENTIFIER_TOKEN = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
49
|
+
// Every value spelled as a string-literal TYPE in this file. A value
|
|
50
|
+
// literal with the same spelling is pinned by that type, so capitalizing
|
|
51
|
+
// the value on its own contradicts it (TS2322) while the type itself is
|
|
52
|
+
// code the rule must leave alone — there is no half of the pair that can
|
|
53
|
+
// be rewritten safely, so neither is. Membership is exact: a type
|
|
54
|
+
// `'id'` pins the literal `'id'`, not the prose `'Please enter your id.'`.
|
|
55
|
+
const literalTypeValues = new Set();
|
|
56
|
+
// A `TSLiteralType` can be written after the value it pins, so the verdict
|
|
57
|
+
// is only complete once the whole file has been walked. Candidates are
|
|
58
|
+
// therefore collected during traversal and reported at `Program:exit`.
|
|
59
|
+
const candidates = [];
|
|
60
|
+
/**
|
|
61
|
+
* A string literal that spells a TYPE is code, never displayed prose. It
|
|
62
|
+
* names a property (`Match['id']`), a discriminant, or a member of a
|
|
63
|
+
* literal union, so capitalizing it renames something the program refers
|
|
64
|
+
* to by that exact spelling and the fixed file stops compiling — an
|
|
65
|
+
* indexed-access key rewritten to `Match['ID']` is a TS2339 (#2153).
|
|
66
|
+
*
|
|
67
|
+
* The question is asked about the type position itself rather than by
|
|
68
|
+
* listing parent node types: every literal in a type is wrapped in a
|
|
69
|
+
* `TSLiteralType`, so one check covers indexed-access keys, mapped-type
|
|
70
|
+
* `as` clauses, tuple members, generic defaults and conditional-type
|
|
71
|
+
* branches at once. Enumerating parents instead leaves each spelling that
|
|
72
|
+
* is not on the list unguarded, which is how this defect shipped.
|
|
73
|
+
*/
|
|
74
|
+
function isTypePositionLiteral(node) {
|
|
75
|
+
return !!node.parent && node.parent.type === utils_1.AST_NODE_TYPES.TSLiteralType;
|
|
76
|
+
}
|
|
49
77
|
/**
|
|
50
78
|
* Check if a node is in a context that should be excluded from the rule
|
|
51
79
|
* (e.g., parameter names, property names, type definitions)
|
|
52
80
|
*/
|
|
53
81
|
function isExcludedContext(node) {
|
|
82
|
+
if (isTypePositionLiteral(node)) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
54
85
|
// Check if the node is a property of an object pattern (destructuring)
|
|
55
86
|
if (node.parent &&
|
|
56
87
|
(node.parent.type === utils_1.AST_NODE_TYPES.Property ||
|
|
@@ -240,30 +271,51 @@ exports.enforceIdCapitalization = (0, createRule_1.createRule)({
|
|
|
240
271
|
const fixedText = value.replace(idRegex, (_match, prefix, suffix) => {
|
|
241
272
|
return `${prefix}ID${suffix}`;
|
|
242
273
|
});
|
|
243
|
-
|
|
244
|
-
node,
|
|
245
|
-
messageId: 'enforceIdCapitalization',
|
|
246
|
-
fix: (fixer) => {
|
|
247
|
-
// JSX text carries no delimiters, so its own text is the content.
|
|
248
|
-
if (node.type === utils_1.AST_NODE_TYPES.JSXText) {
|
|
249
|
-
return fixer.replaceText(node, fixedText);
|
|
250
|
-
}
|
|
251
|
-
if (node.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
252
|
-
const replacement = fixStringLiteral(node, fixedText);
|
|
253
|
-
return replacement === null
|
|
254
|
-
? null
|
|
255
|
-
: fixer.replaceText(node, replacement);
|
|
256
|
-
}
|
|
257
|
-
// Any other node kind (a TemplateElement, say) is a fragment of a
|
|
258
|
-
// larger construct whose delimiters and `${}` expressions live
|
|
259
|
-
// outside this node; rebuilding it from the parsed value would
|
|
260
|
-
// destroy them, so report without a fix.
|
|
261
|
-
return null;
|
|
262
|
-
},
|
|
263
|
-
});
|
|
274
|
+
candidates.push({ node, fixedText });
|
|
264
275
|
}
|
|
265
276
|
}
|
|
277
|
+
/**
|
|
278
|
+
* A value literal whose exact spelling is also written as a type in this
|
|
279
|
+
* file is a token that type names, not prose: `const kind: 'id' = 'id'`
|
|
280
|
+
* only compiles while both halves agree, and the type half is off limits.
|
|
281
|
+
*/
|
|
282
|
+
function isPinnedByLiteralType(node) {
|
|
283
|
+
return (node.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
284
|
+
typeof node.value === 'string' &&
|
|
285
|
+
literalTypeValues.has(node.value));
|
|
286
|
+
}
|
|
287
|
+
function reportCandidate(node, fixedText) {
|
|
288
|
+
context.report({
|
|
289
|
+
node,
|
|
290
|
+
messageId: 'enforceIdCapitalization',
|
|
291
|
+
fix: (fixer) => {
|
|
292
|
+
// JSX text carries no delimiters, so its own text is the content.
|
|
293
|
+
if (node.type === utils_1.AST_NODE_TYPES.JSXText) {
|
|
294
|
+
return fixer.replaceText(node, fixedText);
|
|
295
|
+
}
|
|
296
|
+
if (node.type === utils_1.AST_NODE_TYPES.Literal) {
|
|
297
|
+
const replacement = fixStringLiteral(node, fixedText);
|
|
298
|
+
return replacement === null
|
|
299
|
+
? null
|
|
300
|
+
: fixer.replaceText(node, replacement);
|
|
301
|
+
}
|
|
302
|
+
// Any other node kind (a TemplateElement, say) is a fragment of a
|
|
303
|
+
// larger construct whose delimiters and `${}` expressions live
|
|
304
|
+
// outside this node; rebuilding it from the parsed value would
|
|
305
|
+
// destroy them, so report without a fix.
|
|
306
|
+
return null;
|
|
307
|
+
},
|
|
308
|
+
});
|
|
309
|
+
}
|
|
266
310
|
return {
|
|
311
|
+
// Record the spellings the file's types claim before any verdict is
|
|
312
|
+
// emitted; a value literal matching one of them cannot be rewritten.
|
|
313
|
+
TSLiteralType(node) {
|
|
314
|
+
if (node.literal.type === utils_1.AST_NODE_TYPES.Literal &&
|
|
315
|
+
typeof node.literal.value === 'string') {
|
|
316
|
+
literalTypeValues.add(node.literal.value);
|
|
317
|
+
}
|
|
318
|
+
},
|
|
267
319
|
// Check string literals
|
|
268
320
|
Literal(node) {
|
|
269
321
|
if (typeof node.value === 'string') {
|
|
@@ -274,6 +326,16 @@ exports.enforceIdCapitalization = (0, createRule_1.createRule)({
|
|
|
274
326
|
JSXText(node) {
|
|
275
327
|
checkForIdInString(node, node.value);
|
|
276
328
|
},
|
|
329
|
+
'Program:exit'() {
|
|
330
|
+
for (const { node, fixedText } of candidates) {
|
|
331
|
+
// JSX text is rendered, so no type can pin it — the check is scoped
|
|
332
|
+
// to literals, whose spelling a type can name.
|
|
333
|
+
if (isPinnedByLiteralType(node)) {
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
reportCandidate(node, fixedText);
|
|
337
|
+
}
|
|
338
|
+
},
|
|
277
339
|
// We don't need a separate handler for CallExpression since we already handle Literals
|
|
278
340
|
// The Literal handler will catch the string arguments in t("user.profile.id")
|
|
279
341
|
};
|
|
@@ -74,6 +74,70 @@ function isVoidishType(node) {
|
|
|
74
74
|
return false;
|
|
75
75
|
}
|
|
76
76
|
}
|
|
77
|
+
/**
|
|
78
|
+
* Type names whose values are awaited rather than read. `PromiseLike` counts
|
|
79
|
+
* because thenability — not the `Promise` constructor — is what makes a value
|
|
80
|
+
* an awaited one, and a `PromiseLike` member is as unconvertible as a `Promise`
|
|
81
|
+
* one.
|
|
82
|
+
*/
|
|
83
|
+
const THENABLE_TYPE_NAMES = new Set(['Promise', 'PromiseLike']);
|
|
84
|
+
/**
|
|
85
|
+
* `Promise` statics whose result is a promise whatever they are handed, so a
|
|
86
|
+
* `return Promise.all(...)` is a promise return with no annotation to read.
|
|
87
|
+
*/
|
|
88
|
+
const PROMISE_STATIC_PRODUCERS = new Set([
|
|
89
|
+
'resolve',
|
|
90
|
+
'reject',
|
|
91
|
+
'all',
|
|
92
|
+
'allSettled',
|
|
93
|
+
'race',
|
|
94
|
+
'any',
|
|
95
|
+
]);
|
|
96
|
+
/**
|
|
97
|
+
* Methods a promise answers, whose own result is another promise. `then` is the
|
|
98
|
+
* definition of thenable; `catch`/`finally` are sugar over it.
|
|
99
|
+
*/
|
|
100
|
+
const THENABLE_CHAIN_METHODS = new Set(['then', 'catch', 'finally']);
|
|
101
|
+
/**
|
|
102
|
+
* The final segment of a type name, so a qualified spelling
|
|
103
|
+
* (`globalThis.Promise<T>`, `bluebird.Promise<T>`) is recognized as the thenable
|
|
104
|
+
* it names rather than dismissed for not being a bare `Identifier`.
|
|
105
|
+
*/
|
|
106
|
+
function rightmostTypeName(typeName) {
|
|
107
|
+
if (typeName.type === utils_1.AST_NODE_TYPES.Identifier)
|
|
108
|
+
return typeName.name;
|
|
109
|
+
if (typeName.type === utils_1.AST_NODE_TYPES.TSQualifiedName) {
|
|
110
|
+
return typeName.right.name;
|
|
111
|
+
}
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Whether a *written* type annotation denotes a thenable.
|
|
116
|
+
*
|
|
117
|
+
* This is deliberately syntactic and deliberately not exhaustive: the rule
|
|
118
|
+
* requests no parser services, so an alias that happens to resolve to a promise
|
|
119
|
+
* is out of reach. Failing to spot one costs a report the rule would otherwise
|
|
120
|
+
* have made, which is the safe direction — the unsafe one is prescribing (and
|
|
121
|
+
* on `private` members, applying) a getter rewrite that breaks every caller.
|
|
122
|
+
*/
|
|
123
|
+
function isThenableTypeNode(node) {
|
|
124
|
+
if (!node)
|
|
125
|
+
return false;
|
|
126
|
+
switch (node.type) {
|
|
127
|
+
case utils_1.AST_NODE_TYPES.TSTypeReference: {
|
|
128
|
+
const name = rightmostTypeName(node.typeName);
|
|
129
|
+
return name !== null && THENABLE_TYPE_NAMES.has(name);
|
|
130
|
+
}
|
|
131
|
+
// A container that can hold a thenable still hands the caller one:
|
|
132
|
+
// `Promise<T> | undefined` is awaited at the call site exactly as `Promise<T>`
|
|
133
|
+
// is, and an intersection carries every constituent's contract.
|
|
134
|
+
case utils_1.AST_NODE_TYPES.TSUnionType:
|
|
135
|
+
case utils_1.AST_NODE_TYPES.TSIntersectionType:
|
|
136
|
+
return node.types.some(isThenableTypeNode);
|
|
137
|
+
default:
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
77
141
|
function isFunctionLikeNode(value) {
|
|
78
142
|
return (value.type === utils_1.AST_NODE_TYPES.FunctionExpression ||
|
|
79
143
|
value.type === utils_1.AST_NODE_TYPES.FunctionDeclaration ||
|
|
@@ -617,6 +681,213 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
|
|
|
617
681
|
}
|
|
618
682
|
return false;
|
|
619
683
|
}
|
|
684
|
+
/**
|
|
685
|
+
* The expressions the method itself returns. A `return` inside a nested
|
|
686
|
+
* function is that callback's result, not the method's, so those are skipped
|
|
687
|
+
* exactly as every other body walk here skips them.
|
|
688
|
+
*/
|
|
689
|
+
function collectReturnedExpressions(body) {
|
|
690
|
+
const returned = [];
|
|
691
|
+
const stack = [...body.body];
|
|
692
|
+
while (stack.length) {
|
|
693
|
+
const current = stack.pop();
|
|
694
|
+
if (isFunctionLikeNode(current)) {
|
|
695
|
+
continue;
|
|
696
|
+
}
|
|
697
|
+
if (current.type === utils_1.AST_NODE_TYPES.ReturnStatement &&
|
|
698
|
+
current.argument) {
|
|
699
|
+
returned.push(current.argument);
|
|
700
|
+
}
|
|
701
|
+
pushChildNodes(current, stack);
|
|
702
|
+
}
|
|
703
|
+
return returned;
|
|
704
|
+
}
|
|
705
|
+
/**
|
|
706
|
+
* Whether a function-valued node hands back a thenable: by its `async`
|
|
707
|
+
* keyword, by its own return annotation, or — failing both — by what its
|
|
708
|
+
* body demonstrably returns.
|
|
709
|
+
*
|
|
710
|
+
* `owner` fixes the class body that `this.<name>` resolves against. It stays
|
|
711
|
+
* the originally reported method through every recursion, because a sibling
|
|
712
|
+
* reached from that method's body lives in the same class body.
|
|
713
|
+
*/
|
|
714
|
+
function functionYieldsThenable(owner, fn, seen) {
|
|
715
|
+
if (!fn)
|
|
716
|
+
return false;
|
|
717
|
+
if (fn.async)
|
|
718
|
+
return true;
|
|
719
|
+
const returnType = fn.returnType?.typeAnnotation;
|
|
720
|
+
if (returnType) {
|
|
721
|
+
return isThenableTypeNode(returnType);
|
|
722
|
+
}
|
|
723
|
+
const body = fn.body;
|
|
724
|
+
if (!body)
|
|
725
|
+
return false;
|
|
726
|
+
// A concise arrow body IS the returned expression.
|
|
727
|
+
if (body.type !== utils_1.AST_NODE_TYPES.BlockStatement) {
|
|
728
|
+
return isThenableExpression(owner, body, seen);
|
|
729
|
+
}
|
|
730
|
+
return collectReturnedExpressions(body).some((expression) => isThenableExpression(owner, expression, seen));
|
|
731
|
+
}
|
|
732
|
+
/** Recurses into a sibling's body once, never revisiting a function. */
|
|
733
|
+
function siblingFunctionYieldsThenable(owner, fn, seen) {
|
|
734
|
+
// `a() { return this.b(); } b() { return this.a(); }` is legal and would
|
|
735
|
+
// otherwise recur forever; visiting each function at most once also bounds
|
|
736
|
+
// the work by the size of the class body.
|
|
737
|
+
if (!fn || seen.has(fn))
|
|
738
|
+
return false;
|
|
739
|
+
seen.add(fn);
|
|
740
|
+
return functionYieldsThenable(owner, fn, seen);
|
|
741
|
+
}
|
|
742
|
+
/**
|
|
743
|
+
* Whether `this.<name>` resolves, within the enclosing class body, to a
|
|
744
|
+
* member that yields a thenable.
|
|
745
|
+
*
|
|
746
|
+
* A promise-returning method is often written with no annotation of its own
|
|
747
|
+
* (`readEpoch() { return this.evaluate(); }`), so the sibling's declaration
|
|
748
|
+
* is the only syntactic evidence available without a type checker. The
|
|
749
|
+
* sibling's BODY is consulted when it carries no annotation either, so the
|
|
750
|
+
* exemption survives a sibling transform that strips one — `--fix` under the
|
|
751
|
+
* recommended config runs `no-explicit-return-type` over the same file, and
|
|
752
|
+
* an exemption that only an annotation can carry does not survive it.
|
|
753
|
+
*
|
|
754
|
+
* `viaCall` distinguishes `this.evaluate()` from `this.evaluate`: reading a
|
|
755
|
+
* method without calling it yields the function object, which is not a
|
|
756
|
+
* thenable however the method is annotated, while reading a getter or a
|
|
757
|
+
* field is what produces its declared type.
|
|
758
|
+
*/
|
|
759
|
+
function siblingYieldsThenable(owner, name, viaCall, seen) {
|
|
760
|
+
const classBody = owner.parent;
|
|
761
|
+
if (!classBody || classBody.type !== utils_1.AST_NODE_TYPES.ClassBody) {
|
|
762
|
+
return false;
|
|
763
|
+
}
|
|
764
|
+
return classBody.body.some((member) => {
|
|
765
|
+
if (member.type === utils_1.AST_NODE_TYPES.StaticBlock)
|
|
766
|
+
return false;
|
|
767
|
+
const memberName = memberNameOf(member.key, member.computed);
|
|
768
|
+
if (memberName !== name)
|
|
769
|
+
return false;
|
|
770
|
+
if (member.type === utils_1.AST_NODE_TYPES.MethodDefinition ||
|
|
771
|
+
member.type === utils_1.AST_NODE_TYPES.TSAbstractMethodDefinition) {
|
|
772
|
+
const readsAsValue = member.kind === 'get' ? !viaCall : viaCall;
|
|
773
|
+
return (readsAsValue &&
|
|
774
|
+
siblingFunctionYieldsThenable(owner, member.value, seen));
|
|
775
|
+
}
|
|
776
|
+
if (member.type === utils_1.AST_NODE_TYPES.PropertyDefinition ||
|
|
777
|
+
member.type === utils_1.AST_NODE_TYPES.TSAbstractPropertyDefinition) {
|
|
778
|
+
const annotation = member.typeAnnotation?.typeAnnotation;
|
|
779
|
+
const value = member.value;
|
|
780
|
+
const isFunctionValued = !!value &&
|
|
781
|
+
(value.type === utils_1.AST_NODE_TYPES.ArrowFunctionExpression ||
|
|
782
|
+
value.type === utils_1.AST_NODE_TYPES.FunctionExpression);
|
|
783
|
+
if (!viaCall) {
|
|
784
|
+
if (isThenableTypeNode(annotation))
|
|
785
|
+
return true;
|
|
786
|
+
// An un-annotated field initialized to a promise (`private pending =
|
|
787
|
+
// Promise.resolve(x)`) is thenable on its own evidence. A
|
|
788
|
+
// function-valued field is not: reading it yields the function.
|
|
789
|
+
return (!annotation &&
|
|
790
|
+
!isFunctionValued &&
|
|
791
|
+
!!value &&
|
|
792
|
+
isThenableExpression(owner, value, seen));
|
|
793
|
+
}
|
|
794
|
+
// A called field is function-valued, so its RETURN type is what
|
|
795
|
+
// reaches the caller.
|
|
796
|
+
if (annotation?.type === utils_1.AST_NODE_TYPES.TSFunctionType) {
|
|
797
|
+
return isThenableTypeNode(annotation.returnType?.typeAnnotation);
|
|
798
|
+
}
|
|
799
|
+
return (isFunctionValued &&
|
|
800
|
+
siblingFunctionYieldsThenable(owner, value, seen));
|
|
801
|
+
}
|
|
802
|
+
return false;
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
/** `Promise`, however it is qualified (`globalThis.Promise`, `bluebird.Promise`). */
|
|
806
|
+
function isPromiseNamespace(expression) {
|
|
807
|
+
if (expression.type === utils_1.AST_NODE_TYPES.Identifier) {
|
|
808
|
+
return expression.name === 'Promise';
|
|
809
|
+
}
|
|
810
|
+
if (expression.type === utils_1.AST_NODE_TYPES.MemberExpression) {
|
|
811
|
+
return (memberNameOf(expression.property, expression.computed) === 'Promise');
|
|
812
|
+
}
|
|
813
|
+
return false;
|
|
814
|
+
}
|
|
815
|
+
function isThenableCall(owner, call, seen) {
|
|
816
|
+
// An optional call is a `ChainExpression` WRAPPING the call, so the
|
|
817
|
+
// callee here is always the plain member expression; the chain wrapper is
|
|
818
|
+
// unwrapped one level up.
|
|
819
|
+
const callee = call.callee;
|
|
820
|
+
if (callee.type !== utils_1.AST_NODE_TYPES.MemberExpression)
|
|
821
|
+
return false;
|
|
822
|
+
const property = memberNameOf(callee.property, callee.computed);
|
|
823
|
+
if (property === null)
|
|
824
|
+
return false;
|
|
825
|
+
if (isPromiseNamespace(callee.object) &&
|
|
826
|
+
PROMISE_STATIC_PRODUCERS.has(property)) {
|
|
827
|
+
return true;
|
|
828
|
+
}
|
|
829
|
+
if (THENABLE_CHAIN_METHODS.has(property))
|
|
830
|
+
return true;
|
|
831
|
+
if (callee.object.type === utils_1.AST_NODE_TYPES.ThisExpression) {
|
|
832
|
+
return siblingYieldsThenable(owner, property, true, seen);
|
|
833
|
+
}
|
|
834
|
+
return false;
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Whether a returned expression is demonstrably a thenable. Recursion is
|
|
838
|
+
* confined to combinators that pass a value straight through (assertions,
|
|
839
|
+
* `?:`, `&&`/`||`/`??`, optional chains), so the answer always rests on one
|
|
840
|
+
* of the concrete producers above rather than on a guess about a name.
|
|
841
|
+
*/
|
|
842
|
+
function isThenableExpression(owner, expression, seen, depth = 0) {
|
|
843
|
+
if (depth > 4)
|
|
844
|
+
return false;
|
|
845
|
+
switch (expression.type) {
|
|
846
|
+
// `return await x` only parses inside an `async` method, so the method
|
|
847
|
+
// itself hands the caller a promise.
|
|
848
|
+
case utils_1.AST_NODE_TYPES.AwaitExpression:
|
|
849
|
+
return true;
|
|
850
|
+
case utils_1.AST_NODE_TYPES.TSAsExpression:
|
|
851
|
+
case utils_1.AST_NODE_TYPES.TSSatisfiesExpression:
|
|
852
|
+
return (isThenableTypeNode(expression.typeAnnotation) ||
|
|
853
|
+
isThenableExpression(owner, expression.expression, seen, depth + 1));
|
|
854
|
+
case utils_1.AST_NODE_TYPES.TSNonNullExpression:
|
|
855
|
+
case utils_1.AST_NODE_TYPES.ChainExpression:
|
|
856
|
+
return isThenableExpression(owner, expression.expression, seen, depth + 1);
|
|
857
|
+
case utils_1.AST_NODE_TYPES.ConditionalExpression:
|
|
858
|
+
return (isThenableExpression(owner, expression.consequent, seen, depth + 1) ||
|
|
859
|
+
isThenableExpression(owner, expression.alternate, seen, depth + 1));
|
|
860
|
+
case utils_1.AST_NODE_TYPES.LogicalExpression:
|
|
861
|
+
return (isThenableExpression(owner, expression.left, seen, depth + 1) ||
|
|
862
|
+
isThenableExpression(owner, expression.right, seen, depth + 1));
|
|
863
|
+
case utils_1.AST_NODE_TYPES.CallExpression:
|
|
864
|
+
return isThenableCall(owner, expression, seen);
|
|
865
|
+
case utils_1.AST_NODE_TYPES.MemberExpression:
|
|
866
|
+
// `return this.pending` where `pending: Promise<T>`.
|
|
867
|
+
return (expression.object.type === utils_1.AST_NODE_TYPES.ThisExpression &&
|
|
868
|
+
siblingYieldsThenable(owner, memberNameOf(expression.property, expression.computed) ?? '', false, seen));
|
|
869
|
+
default:
|
|
870
|
+
return false;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
/**
|
|
874
|
+
* Whether the method hands the caller a thenable.
|
|
875
|
+
*
|
|
876
|
+
* TypeScript does not require the `async` keyword to return a promise, so
|
|
877
|
+
* keying on the keyword alone classified `fetchToken(): Promise<string>` as
|
|
878
|
+
* synchronous and asked for a getter (#2154). A getter is never a legal
|
|
879
|
+
* remedy here: it turns a call that starts work into a property read, so
|
|
880
|
+
* `session.epoch` spawns the work on what reads as a field access and any
|
|
881
|
+
* reflective call site (`(session as any).readEpoch()`) throws outright.
|
|
882
|
+
*
|
|
883
|
+
* An explicit return annotation is the method's whole contract, so a
|
|
884
|
+
* non-thenable one settles the question without reading the body — which is
|
|
885
|
+
* what keeps an annotated `(): string` method reportable even when its body
|
|
886
|
+
* mentions promises.
|
|
887
|
+
*/
|
|
888
|
+
function returnsThenable(node) {
|
|
889
|
+
return functionYieldsThenable(node, node.value, new Set([node.value]));
|
|
890
|
+
}
|
|
620
891
|
/**
|
|
621
892
|
* Returns true when the method body contains a ThrowStatement that is
|
|
622
893
|
* directly in the method's own scope (not inside a nested function/arrow).
|
|
@@ -882,7 +1153,11 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
|
|
|
882
1153
|
node.key.type !== utils_1.AST_NODE_TYPES.PrivateIdentifier) {
|
|
883
1154
|
return;
|
|
884
1155
|
}
|
|
885
|
-
|
|
1156
|
+
// `ignoreAsync` means "ignore asynchronous methods", not "ignore
|
|
1157
|
+
// methods bearing the async keyword": TypeScript does not require the
|
|
1158
|
+
// keyword to return a promise, so `fetchToken(): Promise<string>` is
|
|
1159
|
+
// asynchronous with no keyword written at all (#2154).
|
|
1160
|
+
if (config.ignoreAsync && returnsThenable(node))
|
|
886
1161
|
return;
|
|
887
1162
|
if (config.ignoreAbstract &&
|
|
888
1163
|
node.type === utils_1.AST_NODE_TYPES.TSAbstractMethodDefinition) {
|
|
@@ -970,7 +1245,14 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
|
|
|
970
1245
|
const hasDuplicateSuggestedName = classBody?.type === utils_1.AST_NODE_TYPES.ClassBody
|
|
971
1246
|
? (suggestedNameCounts.get(classBody)?.get(scopeKey) ?? 0) > 1
|
|
972
1247
|
: false;
|
|
973
|
-
|
|
1248
|
+
// A thenable-returning member has no legal getter form at all: the
|
|
1249
|
+
// rewrite converts a call that starts work into a property read, and
|
|
1250
|
+
// a reflective call site (`(session as any).readEpoch()`) throws
|
|
1251
|
+
// afterwards. The eligibility gate already withholds the report for
|
|
1252
|
+
// these, so this is a second, independent lock — a future change
|
|
1253
|
+
// there must not silently re-enable a rewrite that cannot compile or
|
|
1254
|
+
// run. It subsumes the former `async`-keyword-only withhold.
|
|
1255
|
+
const isThenableReturning = returnsThenable(node);
|
|
974
1256
|
// A decorator cannot be applied to an ECMA private member under
|
|
975
1257
|
// `experimentalDecorators` (TS1206), so a decorated `#foo()` has no
|
|
976
1258
|
// legal getter form to convert to — the fix is impossible, not merely
|
|
@@ -1001,7 +1283,7 @@ exports.preferGetterOverParameterlessMethod = (0, createRule_1.createRule)({
|
|
|
1001
1283
|
},
|
|
1002
1284
|
fix: !isPrivate ||
|
|
1003
1285
|
sideEffectReason ||
|
|
1004
|
-
|
|
1286
|
+
isThenableReturning ||
|
|
1005
1287
|
!leftParen ||
|
|
1006
1288
|
!rightParen ||
|
|
1007
1289
|
hasCollision ||
|
package/package.json
CHANGED
package/release-manifest.json
CHANGED
|
@@ -1,4 +1,40 @@
|
|
|
1
1
|
[
|
|
2
|
+
{
|
|
3
|
+
"version": "1.20.181",
|
|
4
|
+
"date": "2026-08-27T06:29:43.247Z",
|
|
5
|
+
"rules": [
|
|
6
|
+
{
|
|
7
|
+
"name": "prefer-getter-over-parameterless-method",
|
|
8
|
+
"changeType": "fix",
|
|
9
|
+
"issues": [
|
|
10
|
+
2154
|
|
11
|
+
],
|
|
12
|
+
"summary": "decide \"synchronous\" from the returned type, not the async keyword (closes #2154)"
|
|
13
|
+
}
|
|
14
|
+
]
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"version": "1.20.180",
|
|
18
|
+
"date": "2026-08-27T01:52:25.340Z",
|
|
19
|
+
"rules": [
|
|
20
|
+
{
|
|
21
|
+
"name": "enforce-assert-safe-object-key",
|
|
22
|
+
"changeType": "fix",
|
|
23
|
+
"issues": [
|
|
24
|
+
2152
|
|
25
|
+
],
|
|
26
|
+
"summary": "read the validated binding through the shared peel (closes #2152)"
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"name": "enforce-id-capitalization",
|
|
30
|
+
"changeType": "fix",
|
|
31
|
+
"issues": [
|
|
32
|
+
2153
|
|
33
|
+
],
|
|
34
|
+
"summary": "leave string literals that spell a type alone (closes #2153)"
|
|
35
|
+
}
|
|
36
|
+
]
|
|
37
|
+
},
|
|
2
38
|
{
|
|
3
39
|
"version": "1.20.179",
|
|
4
40
|
"date": "2026-08-26T23:40:36.587Z",
|