diff --git a/packages/eslint-plugin/src/rules/return-await.ts b/packages/eslint-plugin/src/rules/return-await.ts index dfdb75fa30d..d388d84c480 100644 --- a/packages/eslint-plugin/src/rules/return-await.ts +++ b/packages/eslint-plugin/src/rules/return-await.ts @@ -1,7 +1,9 @@ import { AST_NODE_TYPES, TSESLint, TSESTree } from '@typescript-eslint/utils'; import * as tsutils from 'tsutils'; +import { isBinaryExpression } from 'tsutils'; import * as ts from 'typescript'; import * as util from '../util'; +import { getOperatorPrecedence } from '../util/getOperatorPrecedence'; type FunctionNode = | TSESTree.FunctionDeclaration @@ -149,15 +151,28 @@ export default util.createRule({ function insertAwait( fixer: TSESLint.RuleFixer, node: TSESTree.Expression, + isHighPrecendence: boolean, ): TSESLint.RuleFix | TSESLint.RuleFix[] { - if (node.type !== AST_NODE_TYPES.TSAsExpression) { + if (isHighPrecendence) { return fixer.insertTextBefore(node, 'await '); + } else { + return [ + fixer.insertTextBefore(node, 'await ('), + fixer.insertTextAfter(node, ')'), + ]; } + } - return [ - fixer.insertTextBefore(node, 'await ('), - fixer.insertTextAfter(node, ')'), - ]; + function isHigherPrecedenceThanAwait(node: ts.Node): boolean { + const operator = isBinaryExpression(node) + ? node.operatorToken.kind + : ts.SyntaxKind.Unknown; + const nodePrecedence = getOperatorPrecedence(node.kind, operator); + const awaitPrecedence = getOperatorPrecedence( + ts.SyntaxKind.AwaitExpression, + ts.SyntaxKind.Unknown, + ); + return nodePrecedence > awaitPrecedence; } function test(node: TSESTree.Expression, expression: ts.Node): void { @@ -208,7 +223,8 @@ export default util.createRule({ context.report({ messageId: 'requiredPromiseAwait', node, - fix: fixer => insertAwait(fixer, node), + fix: fixer => + insertAwait(fixer, node, isHigherPrecedenceThanAwait(expression)), }); } @@ -247,7 +263,8 @@ export default util.createRule({ context.report({ messageId: 'requiredPromiseAwait', node, - fix: fixer => insertAwait(fixer, node), + fix: fixer => + insertAwait(fixer, node, isHigherPrecedenceThanAwait(expression)), }); } diff --git a/packages/eslint-plugin/src/util/getOperatorPrecedence.ts b/packages/eslint-plugin/src/util/getOperatorPrecedence.ts new file mode 100644 index 00000000000..b7a9d75fb15 --- /dev/null +++ b/packages/eslint-plugin/src/util/getOperatorPrecedence.ts @@ -0,0 +1,347 @@ +import { SyntaxKind } from 'typescript'; + +export enum OperatorPrecedence { + // Expression: + // AssignmentExpression + // Expression `,` AssignmentExpression + Comma, + + // NOTE: `Spread` is higher than `Comma` due to how it is parsed in |ElementList| + // SpreadElement: + // `...` AssignmentExpression + Spread, + + // AssignmentExpression: + // ConditionalExpression + // YieldExpression + // ArrowFunction + // AsyncArrowFunction + // LeftHandSideExpression `=` AssignmentExpression + // LeftHandSideExpression AssignmentOperator AssignmentExpression + // + // NOTE: AssignmentExpression is broken down into several precedences due to the requirements + // of the parenthesize rules. + + // AssignmentExpression: YieldExpression + // YieldExpression: + // `yield` + // `yield` AssignmentExpression + // `yield` `*` AssignmentExpression + Yield, + + // AssignmentExpression: LeftHandSideExpression `=` AssignmentExpression + // AssignmentExpression: LeftHandSideExpression AssignmentOperator AssignmentExpression + // AssignmentOperator: one of + // `*=` `/=` `%=` `+=` `-=` `<<=` `>>=` `>>>=` `&=` `^=` `|=` `**=` + Assignment, + + // NOTE: `Conditional` is considered higher than `Assignment` here, but in reality they have + // the same precedence. + // AssignmentExpression: ConditionalExpression + // ConditionalExpression: + // ShortCircuitExpression + // ShortCircuitExpression `?` AssignmentExpression `:` AssignmentExpression + // ShortCircuitExpression: + // LogicalORExpression + // CoalesceExpression + Conditional, + + // CoalesceExpression: + // CoalesceExpressionHead `??` BitwiseORExpression + // CoalesceExpressionHead: + // CoalesceExpression + // BitwiseORExpression + Coalesce = Conditional, // NOTE: This is wrong + + // LogicalORExpression: + // LogicalANDExpression + // LogicalORExpression `||` LogicalANDExpression + LogicalOR, + + // LogicalANDExpression: + // BitwiseORExpression + // LogicalANDExpression `&&` BitwiseORExpression + LogicalAND, + + // BitwiseORExpression: + // BitwiseXORExpression + // BitwiseORExpression `^` BitwiseXORExpression + BitwiseOR, + + // BitwiseXORExpression: + // BitwiseANDExpression + // BitwiseXORExpression `^` BitwiseANDExpression + BitwiseXOR, + + // BitwiseANDExpression: + // EqualityExpression + // BitwiseANDExpression `^` EqualityExpression + BitwiseAND, + + // EqualityExpression: + // RelationalExpression + // EqualityExpression `==` RelationalExpression + // EqualityExpression `!=` RelationalExpression + // EqualityExpression `===` RelationalExpression + // EqualityExpression `!==` RelationalExpression + Equality, + + // RelationalExpression: + // ShiftExpression + // RelationalExpression `<` ShiftExpression + // RelationalExpression `>` ShiftExpression + // RelationalExpression `<=` ShiftExpression + // RelationalExpression `>=` ShiftExpression + // RelationalExpression `instanceof` ShiftExpression + // RelationalExpression `in` ShiftExpression + // [+TypeScript] RelationalExpression `as` Type + Relational, + + // ShiftExpression: + // AdditiveExpression + // ShiftExpression `<<` AdditiveExpression + // ShiftExpression `>>` AdditiveExpression + // ShiftExpression `>>>` AdditiveExpression + Shift, + + // AdditiveExpression: + // MultiplicativeExpression + // AdditiveExpression `+` MultiplicativeExpression + // AdditiveExpression `-` MultiplicativeExpression + Additive, + + // MultiplicativeExpression: + // ExponentiationExpression + // MultiplicativeExpression MultiplicativeOperator ExponentiationExpression + // MultiplicativeOperator: one of `*`, `/`, `%` + Multiplicative, + + // ExponentiationExpression: + // UnaryExpression + // UpdateExpression `**` ExponentiationExpression + Exponentiation, + + // UnaryExpression: + // UpdateExpression + // `delete` UnaryExpression + // `void` UnaryExpression + // `typeof` UnaryExpression + // `+` UnaryExpression + // `-` UnaryExpression + // `~` UnaryExpression + // `!` UnaryExpression + // AwaitExpression + // UpdateExpression: // TODO: Do we need to investigate the precedence here? + // `++` UnaryExpression + // `--` UnaryExpression + Unary, + + // UpdateExpression: + // LeftHandSideExpression + // LeftHandSideExpression `++` + // LeftHandSideExpression `--` + Update, + + // LeftHandSideExpression: + // NewExpression + // CallExpression + // NewExpression: + // MemberExpression + // `new` NewExpression + LeftHandSide, + + // CallExpression: + // CoverCallExpressionAndAsyncArrowHead + // SuperCall + // ImportCall + // CallExpression Arguments + // CallExpression `[` Expression `]` + // CallExpression `.` IdentifierName + // CallExpression TemplateLiteral + // MemberExpression: + // PrimaryExpression + // MemberExpression `[` Expression `]` + // MemberExpression `.` IdentifierName + // MemberExpression TemplateLiteral + // SuperProperty + // MetaProperty + // `new` MemberExpression Arguments + Member, + + // TODO: JSXElement? + // PrimaryExpression: + // `this` + // IdentifierReference + // Literal + // ArrayLiteral + // ObjectLiteral + // FunctionExpression + // ClassExpression + // GeneratorExpression + // AsyncFunctionExpression + // AsyncGeneratorExpression + // RegularExpressionLiteral + // TemplateLiteral + // CoverParenthesizedExpressionAndArrowParameterList + Primary, + + Highest = Primary, + Lowest = Comma, + // -1 is lower than all other precedences. Returning it will cause binary expression + // parsing to stop. + Invalid = -1, +} + +export function getOperatorPrecedence( + nodeKind: SyntaxKind, + operatorKind: SyntaxKind, + hasArguments?: boolean, +): OperatorPrecedence { + switch (nodeKind) { + case SyntaxKind.CommaListExpression: + return OperatorPrecedence.Comma; + + case SyntaxKind.SpreadElement: + return OperatorPrecedence.Spread; + + case SyntaxKind.YieldExpression: + return OperatorPrecedence.Yield; + + case SyntaxKind.ConditionalExpression: + return OperatorPrecedence.Conditional; + + case SyntaxKind.BinaryExpression: + switch (operatorKind) { + case SyntaxKind.CommaToken: + return OperatorPrecedence.Comma; + + case SyntaxKind.EqualsToken: + case SyntaxKind.PlusEqualsToken: + case SyntaxKind.MinusEqualsToken: + case SyntaxKind.AsteriskAsteriskEqualsToken: + case SyntaxKind.AsteriskEqualsToken: + case SyntaxKind.SlashEqualsToken: + case SyntaxKind.PercentEqualsToken: + case SyntaxKind.LessThanLessThanEqualsToken: + case SyntaxKind.GreaterThanGreaterThanEqualsToken: + case SyntaxKind.GreaterThanGreaterThanGreaterThanEqualsToken: + case SyntaxKind.AmpersandEqualsToken: + case SyntaxKind.CaretEqualsToken: + case SyntaxKind.BarEqualsToken: + case SyntaxKind.BarBarEqualsToken: + case SyntaxKind.AmpersandAmpersandEqualsToken: + case SyntaxKind.QuestionQuestionEqualsToken: + return OperatorPrecedence.Assignment; + + default: + return getBinaryOperatorPrecedence(operatorKind); + } + + // TODO: Should prefix `++` and `--` be moved to the `Update` precedence? + case SyntaxKind.TypeAssertionExpression: + case SyntaxKind.NonNullExpression: + case SyntaxKind.PrefixUnaryExpression: + case SyntaxKind.TypeOfExpression: + case SyntaxKind.VoidExpression: + case SyntaxKind.DeleteExpression: + case SyntaxKind.AwaitExpression: + return OperatorPrecedence.Unary; + + case SyntaxKind.PostfixUnaryExpression: + return OperatorPrecedence.Update; + + case SyntaxKind.CallExpression: + return OperatorPrecedence.LeftHandSide; + + case SyntaxKind.NewExpression: + return hasArguments + ? OperatorPrecedence.Member + : OperatorPrecedence.LeftHandSide; + + case SyntaxKind.TaggedTemplateExpression: + case SyntaxKind.PropertyAccessExpression: + case SyntaxKind.ElementAccessExpression: + case SyntaxKind.MetaProperty: + return OperatorPrecedence.Member; + + case SyntaxKind.AsExpression: + return OperatorPrecedence.Relational; + + case SyntaxKind.ThisKeyword: + case SyntaxKind.SuperKeyword: + case SyntaxKind.Identifier: + case SyntaxKind.PrivateIdentifier: + case SyntaxKind.NullKeyword: + case SyntaxKind.TrueKeyword: + case SyntaxKind.FalseKeyword: + case SyntaxKind.NumericLiteral: + case SyntaxKind.BigIntLiteral: + case SyntaxKind.StringLiteral: + case SyntaxKind.ArrayLiteralExpression: + case SyntaxKind.ObjectLiteralExpression: + case SyntaxKind.FunctionExpression: + case SyntaxKind.ArrowFunction: + case SyntaxKind.ClassExpression: + case SyntaxKind.RegularExpressionLiteral: + case SyntaxKind.NoSubstitutionTemplateLiteral: + case SyntaxKind.TemplateExpression: + case SyntaxKind.ParenthesizedExpression: + case SyntaxKind.OmittedExpression: + case SyntaxKind.JsxElement: + case SyntaxKind.JsxSelfClosingElement: + case SyntaxKind.JsxFragment: + return OperatorPrecedence.Primary; + + default: + return OperatorPrecedence.Invalid; + } +} + +export function getBinaryOperatorPrecedence( + kind: SyntaxKind, +): OperatorPrecedence { + switch (kind) { + case SyntaxKind.QuestionQuestionToken: + return OperatorPrecedence.Coalesce; + case SyntaxKind.BarBarToken: + return OperatorPrecedence.LogicalOR; + case SyntaxKind.AmpersandAmpersandToken: + return OperatorPrecedence.LogicalAND; + case SyntaxKind.BarToken: + return OperatorPrecedence.BitwiseOR; + case SyntaxKind.CaretToken: + return OperatorPrecedence.BitwiseXOR; + case SyntaxKind.AmpersandToken: + return OperatorPrecedence.BitwiseAND; + case SyntaxKind.EqualsEqualsToken: + case SyntaxKind.ExclamationEqualsToken: + case SyntaxKind.EqualsEqualsEqualsToken: + case SyntaxKind.ExclamationEqualsEqualsToken: + return OperatorPrecedence.Equality; + case SyntaxKind.LessThanToken: + case SyntaxKind.GreaterThanToken: + case SyntaxKind.LessThanEqualsToken: + case SyntaxKind.GreaterThanEqualsToken: + case SyntaxKind.InstanceOfKeyword: + case SyntaxKind.InKeyword: + case SyntaxKind.AsKeyword: + return OperatorPrecedence.Relational; + case SyntaxKind.LessThanLessThanToken: + case SyntaxKind.GreaterThanGreaterThanToken: + case SyntaxKind.GreaterThanGreaterThanGreaterThanToken: + return OperatorPrecedence.Shift; + case SyntaxKind.PlusToken: + case SyntaxKind.MinusToken: + return OperatorPrecedence.Additive; + case SyntaxKind.AsteriskToken: + case SyntaxKind.SlashToken: + case SyntaxKind.PercentToken: + return OperatorPrecedence.Multiplicative; + case SyntaxKind.AsteriskAsteriskToken: + return OperatorPrecedence.Exponentiation; + } + + // -1 is lower than all other precedences. Returning it will cause binary expression + // parsing to stop. + return -1; +} diff --git a/packages/eslint-plugin/tests/rules/return-await.test.ts b/packages/eslint-plugin/tests/rules/return-await.test.ts index 1c0a9cdd2a1..43db87ef819 100644 --- a/packages/eslint-plugin/tests/rules/return-await.test.ts +++ b/packages/eslint-plugin/tests/rules/return-await.test.ts @@ -909,5 +909,143 @@ async function test(): Promise { }, ], }, + { + code: ` + async function bar() {} + async function foo() { + try { + return undefined || bar(); + } catch {} + } + `, + output: ` + async function bar() {} + async function foo() { + try { + return await (undefined || bar()); + } catch {} + } + `, + errors: [ + { + line: 5, + messageId: 'requiredPromiseAwait', + }, + ], + }, + { + code: ` + async function bar() {} + async function foo() { + try { + return bar() || undefined || bar(); + } catch {} + } + `, + output: ` + async function bar() {} + async function foo() { + try { + return await (bar() || undefined || bar()); + } catch {} + } + `, + errors: [ + { + line: 5, + messageId: 'requiredPromiseAwait', + }, + ], + }, + { + code: ` + async function bar() {} + async function func1() { + try { + return null ?? bar(); + } catch {} + } + async function func2() { + try { + return 1 && bar(); + } catch {} + } + const foo = { + bar: async function () {}, + }; + async function func3() { + try { + return foo.bar(); + } catch {} + } + `, + output: ` + async function bar() {} + async function func1() { + try { + return await (null ?? bar()); + } catch {} + } + async function func2() { + try { + return await (1 && bar()); + } catch {} + } + const foo = { + bar: async function () {}, + }; + async function func3() { + try { + return await foo.bar(); + } catch {} + } + `, + errors: [ + { + line: 5, + messageId: 'requiredPromiseAwait', + }, + { + line: 10, + messageId: 'requiredPromiseAwait', + }, + { + line: 18, + messageId: 'requiredPromiseAwait', + }, + ], + }, + { + code: ` + class X { + async bar() { + return; + } + async func2() { + try { + return this.bar(); + } catch {} + } + } + `, + output: ` + class X { + async bar() { + return; + } + async func2() { + try { + return await this.bar(); + } catch {} + } + } + `, + errors: [ + { + line: 8, + messageId: 'requiredPromiseAwait', + }, + ], + }, ], });