Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved checking of nullable operands in expressions #13483

Merged
merged 6 commits into from
Jan 17, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 25 additions & 34 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12332,16 +12332,18 @@ namespace ts {
}

function checkNonNullExpression(node: Expression | QualifiedName) {
const type = checkExpression(node);
if (strictNullChecks) {
const kind = getFalsyFlags(type) & TypeFlags.Nullable;
if (kind) {
error(node, kind & TypeFlags.Undefined ? kind & TypeFlags.Null ?
Diagnostics.Object_is_possibly_null_or_undefined :
Diagnostics.Object_is_possibly_undefined :
Diagnostics.Object_is_possibly_null);
}
return getNonNullableType(type);
return checkNonNullType(checkExpression(node), node);
}

function checkNonNullType(type: Type, errorNode: Node): Type {
const kind = (strictNullChecks ? getFalsyFlags(type) : type.flags) & TypeFlags.Nullable;
if (kind) {
error(errorNode, kind & TypeFlags.Undefined ? kind & TypeFlags.Null ?
Diagnostics.Object_is_possibly_null_or_undefined :
Diagnostics.Object_is_possibly_undefined :
Diagnostics.Object_is_possibly_null);
const t = getNonNullableType(type);
return t.flags & (TypeFlags.Nullable | TypeFlags.Never) ? unknownType : t;
}
return type;
}
Expand Down Expand Up @@ -14481,6 +14483,7 @@ namespace ts {
case SyntaxKind.PlusToken:
case SyntaxKind.MinusToken:
case SyntaxKind.TildeToken:
checkNonNullType(operandType, node.operand);
if (maybeTypeOfKind(operandType, TypeFlags.ESSymbol)) {
error(node.operand, Diagnostics.The_0_operator_cannot_be_applied_to_type_symbol, tokenToString(node.operator));
}
Expand All @@ -14492,7 +14495,7 @@ namespace ts {
booleanType;
case SyntaxKind.PlusPlusToken:
case SyntaxKind.MinusMinusToken:
const ok = checkArithmeticOperandType(node.operand, getNonNullableType(operandType),
const ok = checkArithmeticOperandType(node.operand, checkNonNullType(operandType, node.operand),
Diagnostics.An_arithmetic_operand_must_be_of_type_any_number_or_an_enum_type);
if (ok) {
// run check only if former checks succeeded to avoid reporting cascading errors
Expand All @@ -14508,7 +14511,7 @@ namespace ts {
if (operandType === silentNeverType) {
return silentNeverType;
}
const ok = checkArithmeticOperandType(node.operand, getNonNullableType(operandType),
const ok = checkArithmeticOperandType(node.operand, checkNonNullType(operandType, node.operand),
Diagnostics.An_arithmetic_operand_must_be_of_type_any_number_or_an_enum_type);
if (ok) {
// run check only if former checks succeeded to avoid reporting cascading errors
Expand Down Expand Up @@ -14583,7 +14586,6 @@ namespace ts {
}
// NOTE: do not raise error if right is unknown as related error was already reported
if (!(isTypeAny(rightType) ||
rightType.flags & TypeFlags.Nullable ||
getSignaturesOfType(rightType, SignatureKind.Call).length ||
getSignaturesOfType(rightType, SignatureKind.Construct).length ||
isTypeSubtypeOf(rightType, globalFunctionType))) {
Expand All @@ -14596,6 +14598,8 @@ namespace ts {
if (leftType === silentNeverType || rightType === silentNeverType) {
return silentNeverType;
}
leftType = checkNonNullType(leftType, left);
rightType = checkNonNullType(rightType, right);
// TypeScript 1.0 spec (April 2014): 4.15.5
// The in operator requires the left operand to be of type Any, the String primitive type, or the Number primitive type,
// and the right operand to be of type Any, an object type, or a type parameter type.
Expand Down Expand Up @@ -14880,17 +14884,9 @@ namespace ts {
if (leftType === silentNeverType || rightType === silentNeverType) {
return silentNeverType;
}
// TypeScript 1.0 spec (April 2014): 4.19.1
// These operators require their operands to be of type Any, the Number primitive type,
// or an enum type. Operands of an enum type are treated
// as having the primitive type Number. If one operand is the null or undefined value,
// it is treated as having the type of the other operand.
// The result is always of the Number primitive type.
if (leftType.flags & TypeFlags.Nullable) leftType = rightType;
if (rightType.flags & TypeFlags.Nullable) rightType = leftType;

leftType = getNonNullableType(leftType);
rightType = getNonNullableType(rightType);
leftType = checkNonNullType(leftType, left);
rightType = checkNonNullType(rightType, right);

let suggestedOperator: SyntaxKind;
// if a user tries to apply a bitwise operator to 2 boolean operands
Expand All @@ -14915,16 +14911,11 @@ namespace ts {
if (leftType === silentNeverType || rightType === silentNeverType) {
return silentNeverType;
}
// TypeScript 1.0 spec (April 2014): 4.19.2
// The binary + operator requires both operands to be of the Number primitive type or an enum type,
// or at least one of the operands to be of type Any or the String primitive type.

// If one operand is the null or undefined value, it is treated as having the type of the other operand.
if (leftType.flags & TypeFlags.Nullable) leftType = rightType;
if (rightType.flags & TypeFlags.Nullable) rightType = leftType;

leftType = getNonNullableType(leftType);
rightType = getNonNullableType(rightType);
if (!isTypeOfKind(leftType, TypeFlags.Any | TypeFlags.StringLike) && !isTypeOfKind(rightType, TypeFlags.Any | TypeFlags.StringLike)) {
leftType = checkNonNullType(leftType, left);
rightType = checkNonNullType(rightType, right);
}

let resultType: Type;
if (isTypeOfKind(leftType, TypeFlags.NumberLike) && isTypeOfKind(rightType, TypeFlags.NumberLike)) {
Expand Down Expand Up @@ -14963,8 +14954,8 @@ namespace ts {
case SyntaxKind.LessThanEqualsToken:
case SyntaxKind.GreaterThanEqualsToken:
if (checkForDisallowedESSymbolOperand(operator)) {
leftType = getBaseTypeOfLiteralType(leftType);
rightType = getBaseTypeOfLiteralType(rightType);
leftType = getBaseTypeOfLiteralType(checkNonNullType(leftType, left));
rightType = getBaseTypeOfLiteralType(checkNonNullType(rightType, right));
if (!isTypeComparableTo(leftType, rightType) && !isTypeComparableTo(rightType, leftType)) {
reportOperatorError();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndInvalidOperator.ts(11,10): error TS2365: Operator '+' cannot be applied to types 'boolean' and 'boolean'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndInvalidOperator.ts(12,10): error TS2365: Operator '+' cannot be applied to types 'Object' and 'Object'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndInvalidOperator.ts(13,10): error TS2365: Operator '+' cannot be applied to types 'void' and 'void'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndInvalidOperator.ts(14,10): error TS2365: Operator '+' cannot be applied to types 'boolean' and 'boolean'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndInvalidOperator.ts(15,10): error TS2365: Operator '+' cannot be applied to types 'Object' and 'Object'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndInvalidOperator.ts(16,10): error TS2365: Operator '+' cannot be applied to types 'void' and 'void'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndInvalidOperator.ts(19,10): error TS2365: Operator '+' cannot be applied to types 'Number' and 'Number'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndInvalidOperator.ts(20,10): error TS2365: Operator '+' cannot be applied to types 'true' and 'true'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndInvalidOperator.ts(21,10): error TS2365: Operator '+' cannot be applied to types '{ a: string; }' and '{ a: string; }'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndInvalidOperator.ts(22,11): error TS2365: Operator '+' cannot be applied to types 'void' and 'void'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndInvalidOperator.ts(23,11): error TS2365: Operator '+' cannot be applied to types '() => void' and '() => void'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndInvalidOperator.ts(11,10): error TS2531: Object is possibly 'null'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndInvalidOperator.ts(12,10): error TS2531: Object is possibly 'null'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndInvalidOperator.ts(13,10): error TS2531: Object is possibly 'null'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndInvalidOperator.ts(14,14): error TS2531: Object is possibly 'null'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndInvalidOperator.ts(15,14): error TS2531: Object is possibly 'null'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndInvalidOperator.ts(16,10): error TS2531: Object is possibly 'null'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndInvalidOperator.ts(19,10): error TS2531: Object is possibly 'null'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndInvalidOperator.ts(20,10): error TS2531: Object is possibly 'null'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndInvalidOperator.ts(21,10): error TS2531: Object is possibly 'null'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndInvalidOperator.ts(22,11): error TS2531: Object is possibly 'null'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndInvalidOperator.ts(23,11): error TS2531: Object is possibly 'null'.


==== tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndInvalidOperator.ts (11 errors) ====
Expand All @@ -23,37 +23,37 @@ tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOpe

// null + boolean/Object
var r1 = null + a;
~~~~~~~~
!!! error TS2365: Operator '+' cannot be applied to types 'boolean' and 'boolean'.
~~~~
!!! error TS2531: Object is possibly 'null'.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object is definitely null, and typically we don't think of objects as being operands to arithmetic operations (whereas we do when it comes to primitives). Would be nice if we could be more precise on the error message.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two orthogonal issues:

First, we could do extra work to omit the word possibly when the type is exactly null or undefined. However, it is very rare for someone to explicitly use null or undefined as an operand, so it's not clear that it really matters all that much. This is the type of error that only surfaces in a test.

Second, we could consider using the word value instead of object when reporting for expression operands--but it's really the same error that we use for obj.foo where obj may be null or undefined. The downside would be error message searchability, but since the error is pretty obvious that may not be an issue.

var r2 = null + b;
~~~~~~~~
!!! error TS2365: Operator '+' cannot be applied to types 'Object' and 'Object'.
~~~~
!!! error TS2531: Object is possibly 'null'.
var r3 = null + c;
~~~~~~~~
!!! error TS2365: Operator '+' cannot be applied to types 'void' and 'void'.
~~~~
!!! error TS2531: Object is possibly 'null'.
var r4 = a + null;
~~~~~~~~
!!! error TS2365: Operator '+' cannot be applied to types 'boolean' and 'boolean'.
~~~~
!!! error TS2531: Object is possibly 'null'.
var r5 = b + null;
~~~~~~~~
!!! error TS2365: Operator '+' cannot be applied to types 'Object' and 'Object'.
~~~~
!!! error TS2531: Object is possibly 'null'.
var r6 = null + c;
~~~~~~~~
!!! error TS2365: Operator '+' cannot be applied to types 'void' and 'void'.
~~~~
!!! error TS2531: Object is possibly 'null'.

// other cases
var r7 = null + d;
~~~~~~~~
!!! error TS2365: Operator '+' cannot be applied to types 'Number' and 'Number'.
~~~~
!!! error TS2531: Object is possibly 'null'.
var r8 = null + true;
~~~~~~~~~~~
!!! error TS2365: Operator '+' cannot be applied to types 'true' and 'true'.
~~~~
!!! error TS2531: Object is possibly 'null'.
var r9 = null + { a: '' };
~~~~~~~~~~~~~~~~
!!! error TS2365: Operator '+' cannot be applied to types '{ a: string; }' and '{ a: string; }'.
~~~~
!!! error TS2531: Object is possibly 'null'.
var r10 = null + foo();
~~~~~~~~~~~~
!!! error TS2365: Operator '+' cannot be applied to types 'void' and 'void'.
~~~~
!!! error TS2531: Object is possibly 'null'.
var r11 = null + (() => { });
~~~~~~~~~~~~~~~~~~
!!! error TS2365: Operator '+' cannot be applied to types '() => void' and '() => void'.
~~~~
!!! error TS2531: Object is possibly 'null'.
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndValidOperator.ts(15,10): error TS2531: Object is possibly 'null'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndValidOperator.ts(16,10): error TS2531: Object is possibly 'null'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndValidOperator.ts(17,10): error TS2531: Object is possibly 'null'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndValidOperator.ts(18,10): error TS2531: Object is possibly 'null'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndValidOperator.ts(19,10): error TS2531: Object is possibly 'null'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndValidOperator.ts(20,14): error TS2531: Object is possibly 'null'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndValidOperator.ts(21,14): error TS2531: Object is possibly 'null'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndValidOperator.ts(22,15): error TS2531: Object is possibly 'null'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndValidOperator.ts(23,17): error TS2531: Object is possibly 'null'.
tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndValidOperator.ts(24,20): error TS2531: Object is possibly 'null'.


==== tests/cases/conformance/expressions/binaryOperators/additionOperator/additionOperatorWithNullValueAndValidOperator.ts (10 errors) ====
// If one operand is the null or undefined value, it is treated as having the type of the other operand.

enum E { a, b, c }

var a: any;
var b: number;
var c: E;
var d: string;

// null + any
var r1: any = null + a;
var r2: any = a + null;

// null + number/enum
var r3 = null + b;
~~~~
!!! error TS2531: Object is possibly 'null'.
var r4 = null + 1;
~~~~
!!! error TS2531: Object is possibly 'null'.
var r5 = null + c;
~~~~
!!! error TS2531: Object is possibly 'null'.
var r6 = null + E.a;
~~~~
!!! error TS2531: Object is possibly 'null'.
var r7 = null + E['a'];
~~~~
!!! error TS2531: Object is possibly 'null'.
var r8 = b + null;
~~~~
!!! error TS2531: Object is possibly 'null'.
var r9 = 1 + null;
~~~~
!!! error TS2531: Object is possibly 'null'.
var r10 = c + null
~~~~
!!! error TS2531: Object is possibly 'null'.
var r11 = E.a + null;
~~~~
!!! error TS2531: Object is possibly 'null'.
var r12 = E['a'] + null;
~~~~
!!! error TS2531: Object is possibly 'null'.

// null + string
var r13 = null + d;
var r14 = null + '';
var r15 = d + null;
var r16 = '' + null;
Loading