From c48e34ef91fa9428a6e4d69196fe299671efad9a Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 2 Jul 2019 17:06:42 -0700 Subject: [PATCH] Did you forget to use await? for operators --- src/compiler/checker.ts | 106 +++++++++++++----- src/compiler/diagnosticMessages.json | 40 +++++++ ...existentPropertyAvailableOnPromisedType.ts | 3 - .../operationsAvailableOnPromisedType.ts | 25 +++++ 4 files changed, 146 insertions(+), 28 deletions(-) delete mode 100644 tests/cases/compiler/nonexistentPropertyAvailableOnPromisedType.ts create mode 100644 tests/cases/compiler/operationsAvailableOnPromisedType.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 610967cfc4f..f4bee2eef4f 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -932,6 +932,18 @@ namespace ts { addErrorOrSuggestion(isError, "message" in message ? createDiagnosticForNode(location, message, arg0, arg1, arg2, arg3) : createDiagnosticForNodeFromMessageChain(location, message)); } + function errorAndMaybeSuggestAwait( + location: Node | undefined, + maybeMissingAwait: boolean, + defaultMessage: DiagnosticMessage, + missingAwaitMessage: DiagnosticMessage, + arg0?: string | number | undefined, arg1?: string | number | undefined, arg2?: string | number | undefined, arg3?: string | number | undefined): Diagnostic { + if (maybeMissingAwait) { + return error(location, missingAwaitMessage, arg0, arg1, arg2, arg3); + } + return error(location, defaultMessage, arg0, arg1, arg2, arg3); + } + function createSymbol(flags: SymbolFlags, name: __String, checkFlags?: CheckFlags) { symbolCount++; const symbol = (new Symbol(flags | SymbolFlags.Transient, name)); @@ -23662,9 +23674,14 @@ namespace ts { } } - function checkArithmeticOperandType(operand: Node, type: Type, diagnostic: DiagnosticMessage): boolean { + function checkArithmeticOperandType(operand: Node, type: Type, diagnostic: DiagnosticMessage, missingAwaitDiagnostic: DiagnosticMessage): boolean { if (!isTypeAssignableTo(type, numberOrBigIntType)) { - error(operand, diagnostic); + const awaitedType = getAwaitedType(type); + errorAndMaybeSuggestAwait( + operand, + !!awaitedType && isTypeAssignableTo(awaitedType, numberOrBigIntType), + diagnostic, + missingAwaitDiagnostic); return false; } return true; @@ -23862,7 +23879,8 @@ namespace ts { case SyntaxKind.PlusPlusToken: case SyntaxKind.MinusMinusToken: const ok = checkArithmeticOperandType(node.operand, checkNonNullType(operandType, node.operand), - Diagnostics.An_arithmetic_operand_must_be_of_type_any_number_bigint_or_an_enum_type); + Diagnostics.An_arithmetic_operand_must_be_of_type_any_number_bigint_or_an_enum_type, + Diagnostics.An_arithmetic_operand_must_be_of_type_any_number_bigint_or_an_enum_type_Did_you_forget_to_use_await); if (ok) { // run check only if former checks succeeded to avoid reporting cascading errors checkReferenceExpression(node.operand, Diagnostics.The_operand_of_an_increment_or_decrement_operator_must_be_a_variable_or_a_property_access); @@ -23880,7 +23898,8 @@ namespace ts { const ok = checkArithmeticOperandType( node.operand, checkNonNullType(operandType, node.operand), - Diagnostics.An_arithmetic_operand_must_be_of_type_any_number_bigint_or_an_enum_type); + Diagnostics.An_arithmetic_operand_must_be_of_type_any_number_bigint_or_an_enum_type, + Diagnostics.An_arithmetic_operand_must_be_of_type_any_number_bigint_or_an_enum_type_Did_you_forget_to_use_await); if (ok) { // run check only if former checks succeeded to avoid reporting cascading errors checkReferenceExpression(node.operand, Diagnostics.The_operand_of_an_increment_or_decrement_operator_must_be_a_variable_or_a_property_access); @@ -24272,8 +24291,8 @@ namespace ts { } else { // otherwise just check each operand separately and report errors as normal - const leftOk = checkArithmeticOperandType(left, leftType, Diagnostics.The_left_hand_side_of_an_arithmetic_operation_must_be_of_type_any_number_bigint_or_an_enum_type); - const rightOk = checkArithmeticOperandType(right, rightType, Diagnostics.The_right_hand_side_of_an_arithmetic_operation_must_be_of_type_any_number_bigint_or_an_enum_type); + const leftOk = checkArithmeticOperandType(left, leftType, Diagnostics.The_left_hand_side_of_an_arithmetic_operation_must_be_of_type_any_number_bigint_or_an_enum_type, Diagnostics.The_left_hand_side_of_an_arithmetic_operation_must_be_of_type_any_number_bigint_or_an_enum_type_Did_you_forget_to_use_await); + const rightOk = checkArithmeticOperandType(right, rightType, Diagnostics.The_right_hand_side_of_an_arithmetic_operation_must_be_of_type_any_number_bigint_or_an_enum_type, Diagnostics.The_right_hand_side_of_an_arithmetic_operation_must_be_of_type_any_number_bigint_or_an_enum_type_Did_you_forget_to_use_await); let resultType: Type; // If both are any or unknown, allow operation; assume it will resolve to number if ((isTypeAssignableToKind(leftType, TypeFlags.AnyOrUnknown) && isTypeAssignableToKind(rightType, TypeFlags.AnyOrUnknown)) || @@ -24282,7 +24301,7 @@ namespace ts { ) { resultType = numberType; } - // At least one is assignable to bigint, so both should be only assignable to bigint + // At least one is assignable to bigint, so check that both are else if (isTypeAssignableToKind(leftType, TypeFlags.BigIntLike) && isTypeAssignableToKind(rightType, TypeFlags.BigIntLike)) { switch (operator) { case SyntaxKind.GreaterThanGreaterThanGreaterThanToken: @@ -24291,8 +24310,9 @@ namespace ts { } resultType = bigintType; } + // Exactly one of leftType/rightType is assignable to bigint else { - reportOperatorError(); + reportOperatorError((awaitedLeft, awaitedRight) => isTypeAssignableToKind(awaitedLeft, TypeFlags.BigIntLike) && isTypeAssignableToKind(awaitedRight, TypeFlags.BigIntLike)); resultType = errorType; } if (leftOk && rightOk) { @@ -24337,7 +24357,14 @@ namespace ts { } if (!resultType) { - reportOperatorError(); + // Types that have a reasonably good chance of being a valid operand type. + // If both types have an awaited type of one of these, we’ll assume the user + // might be missing an await without doing an exhaustive check that inserting + // await(s) will actually be a completely valid binary expression. + const closeEnoughKind = TypeFlags.NumberLike | TypeFlags.BigIntLike | TypeFlags.StringLike | TypeFlags.AnyOrUnknown; + reportOperatorError((awaitedLeft, awaitedRight) => + isTypeAssignableToKind(awaitedLeft, closeEnoughKind) && + isTypeAssignableToKind(awaitedRight, closeEnoughKind)); return anyType; } @@ -24352,21 +24379,18 @@ namespace ts { if (checkForDisallowedESSymbolOperand(operator)) { leftType = getBaseTypeOfLiteralType(checkNonNullType(leftType, left)); rightType = getBaseTypeOfLiteralType(checkNonNullType(rightType, right)); - if (!(isTypeComparableTo(leftType, rightType) || isTypeComparableTo(rightType, leftType) || - (isTypeAssignableTo(leftType, numberOrBigIntType) && isTypeAssignableTo(rightType, numberOrBigIntType)) - )) { - reportOperatorError(); - } + reportOperatorErrorUnless((left, right) => + isTypeComparableTo(left, right) || isTypeComparableTo(right, left) || ( + isTypeAssignableTo(left, numberOrBigIntType) && isTypeAssignableTo(right, numberOrBigIntType))); } return booleanType; case SyntaxKind.EqualsEqualsToken: case SyntaxKind.ExclamationEqualsToken: case SyntaxKind.EqualsEqualsEqualsToken: case SyntaxKind.ExclamationEqualsEqualsToken: - if (!isTypeEqualityComparableTo(leftType, rightType) && !isTypeEqualityComparableTo(rightType, leftType)) { - reportOperatorError(); - } + reportOperatorErrorUnless((left, right) => isTypeEqualityComparableTo(left, right) || isTypeEqualityComparableTo(right, left)); return booleanType; + case SyntaxKind.InstanceOfKeyword: return checkInstanceOfExpression(left, right, leftType, rightType); case SyntaxKind.InKeyword: @@ -24493,13 +24517,33 @@ namespace ts { } } - function reportOperatorError() { - const [leftStr, rightStr] = getTypeNamesForErrorDisplay(leftType, rightType); + /** + * Returns true if an error is reported + */ + function reportOperatorErrorUnless(typesAreCompatible: (left: Type, right: Type) => boolean): boolean { + if (!typesAreCompatible(leftType, rightType)) { + reportOperatorError(typesAreCompatible); + return true; + } + return false; + } + + function reportOperatorError(awaitedTypesAreCompatible?: (left: Type, right: Type) => boolean) { + let wouldWorkWithAwait = false; const errNode = errorNode || operatorToken; - if (!tryGiveBetterPrimaryError(errNode, leftStr, rightStr)) { - error( + const [leftStr, rightStr] = getTypeNamesForErrorDisplay(leftType, rightType); + if (awaitedTypesAreCompatible) { + const awaitedLeftType = getAwaitedType(leftType); + const awaitedRightType = getAwaitedType(rightType); + wouldWorkWithAwait = !!(awaitedLeftType && awaitedRightType) && awaitedTypesAreCompatible(awaitedLeftType, awaitedRightType); + } + + if (!tryGiveBetterPrimaryError(errNode, wouldWorkWithAwait, leftStr, rightStr)) { + errorAndMaybeSuggestAwait( errNode, + wouldWorkWithAwait, Diagnostics.Operator_0_cannot_be_applied_to_types_1_and_2, + Diagnostics.Operator_0_cannot_be_applied_to_types_1_and_2_Did_you_forget_to_use_await, tokenToString(operatorToken.kind), leftStr, rightStr, @@ -24507,15 +24551,27 @@ namespace ts { } } - function tryGiveBetterPrimaryError(errNode: Node, leftStr: string, rightStr: string) { + function tryGiveBetterPrimaryError(errNode: Node, maybeMissingAwait: boolean, leftStr: string, rightStr: string) { + let typeName: string | undefined; switch (operatorToken.kind) { case SyntaxKind.EqualsEqualsEqualsToken: case SyntaxKind.EqualsEqualsToken: - return error(errNode, Diagnostics.This_condition_will_always_return_0_since_the_types_1_and_2_have_no_overlap, "false", leftStr, rightStr); + typeName = "true"; + break; case SyntaxKind.ExclamationEqualsEqualsToken: case SyntaxKind.ExclamationEqualsToken: - return error(errNode, Diagnostics.This_condition_will_always_return_0_since_the_types_1_and_2_have_no_overlap, "true", leftStr, rightStr); - } + typeName = "false"; + } + + if (typeName) { + return errorAndMaybeSuggestAwait( + errNode, + maybeMissingAwait, + Diagnostics.This_condition_will_always_return_0_since_the_types_1_and_2_have_no_overlap, + Diagnostics.This_condition_will_always_return_0_since_the_types_1_and_2_have_no_overlap_Did_you_forget_to_use_await, + typeName, leftStr, rightStr); + } + return undefined; } } diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 3b426f2d0c6..446d4294272 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -2693,6 +2693,46 @@ "category": "Error", "code": 2772 }, + "Operator '{0}' cannot be applied to types '{1}' and '{2}'. Did you forget to use 'await'?": { + "category": "Error", + "code": 2773 + }, + "An arithmetic operand must be of type 'any', 'number', 'bigint' or an enum type. Did you forget to use 'await'?": { + "category": "Error", + "code": 2774 + }, + "The left-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type. Did you forget to use 'await'?": { + "category": "Error", + "code": 2775 + }, + "The right-hand side of an arithmetic operation must be of type 'any', 'number', 'bigint' or an enum type. Did you forget to use 'await'?": { + "category": "Error", + "code": 2777 + }, + "Type '{0}' must have a '[Symbol.iterator]()' method that returns an iterator. Did you forget to use 'await'?": { + "category": "Error", + "code": 2777 + }, + "Type '{0}' is not an array type or a string type. Did you forget to use 'await'?": { + "category": "Error", + "code": 2778 + }, + "Argument of type '{0}' is not assignable to parameter of type '{1}'. Did you forget to use 'await'?": { + "category": "Error", + "code": 2779 + }, + "Type '{0}' has no call signatures. Did you forget to use 'await'?": { + "category": "Error", + "code": 2780 + }, + "Type '{0}' has no construct signatures. Did you forget to use 'await'?": { + "category": "Error", + "code": 2781 + }, + "This condition will always return '{0}' since the types '{1}' and '{2}' have no overlap. Did you forget to use 'await'?": { + "category": "Error", + "code": 2782 + }, "Import declaration '{0}' is using private name '{1}'.": { "category": "Error", diff --git a/tests/cases/compiler/nonexistentPropertyAvailableOnPromisedType.ts b/tests/cases/compiler/nonexistentPropertyAvailableOnPromisedType.ts deleted file mode 100644 index 476f4ac5636..00000000000 --- a/tests/cases/compiler/nonexistentPropertyAvailableOnPromisedType.ts +++ /dev/null @@ -1,3 +0,0 @@ -function f(x: Promise) { - x.toLowerCase(); -} diff --git a/tests/cases/compiler/operationsAvailableOnPromisedType.ts b/tests/cases/compiler/operationsAvailableOnPromisedType.ts new file mode 100644 index 00000000000..24e3f891fe4 --- /dev/null +++ b/tests/cases/compiler/operationsAvailableOnPromisedType.ts @@ -0,0 +1,25 @@ +function fn( + a: number, + b: Promise, + c: Promise, + d: Promise<{ prop: string }>, + e: Promise<() => void>, + f: Promise<() => void> | (() => void), + g: Promise<{ new(): any }> +) { + // All errors + a | b; + b | a; + a + b; + a > b; + b++; + --b; + a === b; + for (const s of c) { + fn(b, b, c, d); + d.prop; + } + e(); + f(); + new g(); +}