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

Reflect control flow effects of optional chains in equality checks #33821

Merged
merged 8 commits into from
Oct 6, 2019
Merged
Show file tree
Hide file tree
Changes from 5 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
28 changes: 28 additions & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18124,6 +18124,16 @@ namespace ts {
return false;
}

function optionalChainContainsReference(source: Node, target: Node) {
while (isOptionalChain(source)) {
source = source.expression;
if (isMatchingReference(source, target)) {
return true;
}
}
return false;
}

// Return true if target is a property access xxx.yyy, source is a property access xxx.zzz, the declared
// type of xxx is a union type, and yyy is a property that is possibly a discriminant. We consider a property
// a possible discriminant if its type differs in the constituents of containing union type, and if every
Expand Down Expand Up @@ -19350,6 +19360,14 @@ namespace ts {
if (isMatchingReference(reference, right)) {
return narrowTypeByEquality(type, operator, left, assumeTrue);
}
if (assumeTrue && (operator === SyntaxKind.EqualsEqualsEqualsToken || operator === SyntaxKind.ExclamationEqualsEqualsToken)) {
rbuckton marked this conversation as resolved.
Show resolved Hide resolved
if (optionalChainContainsReference(left, reference)) {
type = narrowTypeByOptionalChainContainment(type, operator, right);
}
else if (optionalChainContainsReference(right, reference)) {
type = narrowTypeByOptionalChainContainment(type, operator, left);
}
}
if (isMatchingReferenceDiscriminant(left, declaredType)) {
return narrowTypeByDiscriminant(type, <AccessExpression>left, t => narrowTypeByEquality(t, operator, right, assumeTrue));
}
Expand All @@ -19374,6 +19392,16 @@ namespace ts {
return type;
}

function narrowTypeByOptionalChainContainment(type: Type, operator: SyntaxKind, value: Expression): Type {
// We are in the true branch of obj?.foo === value or obj?.foo !== value. We remove undefined and null from
// the type of obj if (a) the operator is === and the type of value doesn't include undefined or (b) the
// operator is !== and the type of value is undefined.
const valueType = getTypeOfExpression(value);
return operator === SyntaxKind.EqualsEqualsEqualsToken && !(getTypeFacts(valueType) & TypeFacts.EQUndefined) ||
operator === SyntaxKind.ExclamationEqualsEqualsToken && valueType.flags & TypeFlags.Undefined ?
getTypeWithFacts(type, TypeFacts.NEUndefinedOrNull) : type;
}

function narrowTypeByEquality(type: Type, operator: SyntaxKind, value: Expression, assumeTrue: boolean): Type {
if (type.flags & TypeFlags.Any) {
return type;
Expand Down
95 changes: 89 additions & 6 deletions tests/baselines/reference/controlFlowOptionalChain.errors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(35,5): error TS2
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(39,1): error TS2722: Cannot invoke an object which is possibly 'undefined'.
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(52,5): error TS2532: Object is possibly 'undefined'.
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(57,1): error TS2532: Object is possibly 'undefined'.
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(62,5): error TS2532: Object is possibly 'undefined'.
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(68,5): error TS2532: Object is possibly 'undefined'.
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(72,1): error TS2532: Object is possibly 'undefined'.
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(83,5): error TS2532: Object is possibly 'undefined'.
Expand All @@ -20,9 +19,12 @@ tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(112,1): error TS
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(130,5): error TS2532: Object is possibly 'undefined'.
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(134,1): error TS2532: Object is possibly 'undefined'.
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(153,9): error TS2775: Assertions require every name in the call target to be declared with an explicit type annotation.
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(197,9): error TS2532: Object is possibly 'undefined'.
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(201,9): error TS2532: Object is possibly 'undefined'.
tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(205,9): error TS2532: Object is possibly 'undefined'.


==== tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts (22 errors) ====
==== tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts (24 errors) ====
// assignments in shortcutting chain
declare const o: undefined | {
[key: string]: any;
Expand Down Expand Up @@ -99,10 +101,8 @@ tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(153,9): error TS

declare const o3: { x: 1, y: string } | { x: 2, y: number } | undefined;
if (o3?.x === 1) {
o3; // TODO: should be `{ x: y, y: string }`
o3.x; // TODO: should not be an error.
~~
!!! error TS2532: Object is possibly 'undefined'.
o3;
o3.x;
o3?.x;
}
else {
Expand Down Expand Up @@ -227,4 +227,87 @@ tests/cases/conformance/controlFlow/controlFlowOptionalChain.ts(153,9): error TS
x;
}
}

type Thing = { foo: number, bar(): number };

function f10(o: Thing | undefined, value: number) {
if (o?.foo === value) {
o;
o.foo;
}
if (o?.["foo"] === value) {
o;
o["foo"];
}
if (o?.bar() === value) {
o;
o.bar;
}
}

function f11(o: Thing | null, value: number) {
if (o?.foo === value) {
o;
o.foo;
}
if (o?.["foo"] === value) {
o;
o["foo"];
}
if (o?.bar() === value) {
o;
o.bar;
}
}

function f12(o: Thing | undefined, value: number | undefined) {
if (o?.foo === value) {
o;
o.foo; // Error
~
!!! error TS2532: Object is possibly 'undefined'.
}
if (o?.["foo"] === value) {
o;
o["foo"]; // Error
~
!!! error TS2532: Object is possibly 'undefined'.
}
if (o?.bar() === value) {
o;
o.bar; // Error
~
!!! error TS2532: Object is possibly 'undefined'.
}
}

function f13(o: Thing | undefined) {
if (o?.foo !== undefined) {
o;
o.foo;
}
if (o?.["foo"] !== undefined) {
o;
o["foo"];
}
if (o?.bar() !== undefined) {
o;
o.bar;
}
}

function f14(o: Thing | null) {
if (o?.foo !== undefined) {
o;
o.foo;
}
if (o?.["foo"] !== undefined) {
o;
o["foo"];
}
if (o?.bar() !== undefined) {
o;
o.bar;
}
}

160 changes: 156 additions & 4 deletions tests/baselines/reference/controlFlowOptionalChain.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@ o2.f;

declare const o3: { x: 1, y: string } | { x: 2, y: number } | undefined;
if (o3?.x === 1) {
o3; // TODO: should be `{ x: y, y: string }`
o3.x; // TODO: should not be an error.
o3;
o3.x;
o3?.x;
}
else {
Expand Down Expand Up @@ -159,6 +159,83 @@ function f01(x: unknown) {
x;
}
}

type Thing = { foo: number, bar(): number };

function f10(o: Thing | undefined, value: number) {
if (o?.foo === value) {
o;
o.foo;
}
if (o?.["foo"] === value) {
o;
o["foo"];
}
if (o?.bar() === value) {
o;
o.bar;
}
}

function f11(o: Thing | null, value: number) {
if (o?.foo === value) {
o;
o.foo;
}
if (o?.["foo"] === value) {
o;
o["foo"];
}
if (o?.bar() === value) {
o;
o.bar;
}
}

function f12(o: Thing | undefined, value: number | undefined) {
if (o?.foo === value) {
o;
o.foo; // Error
}
if (o?.["foo"] === value) {
o;
o["foo"]; // Error
}
if (o?.bar() === value) {
o;
o.bar; // Error
}
}

function f13(o: Thing | undefined) {
if (o?.foo !== undefined) {
o;
o.foo;
}
if (o?.["foo"] !== undefined) {
o;
o["foo"];
}
if (o?.bar() !== undefined) {
o;
o.bar;
}
}

function f14(o: Thing | null) {
if (o?.foo !== undefined) {
o;
o.foo;
}
if (o?.["foo"] !== undefined) {
o;
o["foo"];
}
if (o?.bar() !== undefined) {
o;
o.bar;
}
}


//// [controlFlowOptionalChain.js]
Expand Down Expand Up @@ -206,8 +283,8 @@ o2;
(_k = o2) === null || _k === void 0 ? void 0 : _k.f;
o2.f;
if (((_l = o3) === null || _l === void 0 ? void 0 : _l.x) === 1) {
o3; // TODO: should be `{ x: y, y: string }`
o3.x; // TODO: should not be an error.
o3;
o3.x;
(_m = o3) === null || _m === void 0 ? void 0 : _m.x;
}
else {
Expand Down Expand Up @@ -286,3 +363,78 @@ function f01(x) {
x;
}
}
function f10(o, value) {
var _a, _b, _c;
if (((_a = o) === null || _a === void 0 ? void 0 : _a.foo) === value) {
o;
o.foo;
}
if (((_b = o) === null || _b === void 0 ? void 0 : _b["foo"]) === value) {
o;
o["foo"];
}
if (((_c = o) === null || _c === void 0 ? void 0 : _c.bar()) === value) {
o;
o.bar;
}
}
function f11(o, value) {
var _a, _b, _c;
if (((_a = o) === null || _a === void 0 ? void 0 : _a.foo) === value) {
o;
o.foo;
}
if (((_b = o) === null || _b === void 0 ? void 0 : _b["foo"]) === value) {
o;
o["foo"];
}
if (((_c = o) === null || _c === void 0 ? void 0 : _c.bar()) === value) {
o;
o.bar;
}
}
function f12(o, value) {
var _a, _b, _c;
if (((_a = o) === null || _a === void 0 ? void 0 : _a.foo) === value) {
o;
o.foo; // Error
}
if (((_b = o) === null || _b === void 0 ? void 0 : _b["foo"]) === value) {
o;
o["foo"]; // Error
}
if (((_c = o) === null || _c === void 0 ? void 0 : _c.bar()) === value) {
o;
o.bar; // Error
}
}
function f13(o) {
var _a, _b, _c;
if (((_a = o) === null || _a === void 0 ? void 0 : _a.foo) !== undefined) {
o;
o.foo;
}
if (((_b = o) === null || _b === void 0 ? void 0 : _b["foo"]) !== undefined) {
o;
o["foo"];
}
if (((_c = o) === null || _c === void 0 ? void 0 : _c.bar()) !== undefined) {
o;
o.bar;
}
}
function f14(o) {
var _a, _b, _c;
if (((_a = o) === null || _a === void 0 ? void 0 : _a.foo) !== undefined) {
o;
o.foo;
}
if (((_b = o) === null || _b === void 0 ? void 0 : _b["foo"]) !== undefined) {
o;
o["foo"];
}
if (((_c = o) === null || _c === void 0 ? void 0 : _c.bar()) !== undefined) {
o;
o.bar;
}
}
Loading