-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Control flow based type analysis #8010
Changes from 35 commits
e67d15a
afa1714
7c45c7b
80c2e5e
33985b2
ed5002c
6d25a42
bf78470
4f936c4
7f02357
9e965d4
9de0a5d
560bc3f
0820249
5a5d89a
424074b
c6f4de3
e53f390
a38d863
3d0fa31
ce81ba5
354fd10
5179dd6
019f5bd
f13c92f
b03d087
7a32129
6fb9424
e45bac8
7dfcad6
92df029
32e6464
560e768
b1e9f43
4c250d0
7c7a1c0
df62fa0
586ac55
cd88f1e
472ab7c
b689c07
1ed9871
921efec
9aea708
10889a0
2595f04
b83dc88
538e22a
b5104cb
87f55fa
9defdde
d28a4fe
d735b7a
c8bf6d8
ea96dfd
33e359f
bab8ef4
e9a7d3d
a0101c0
729dfce
3045cf5
06928b6
8a0bc3b
d2b89be
ab4b039
e12b2a7
4fb31ac
f06d3f6
42e3fc4
0dee5ad
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1811,7 +1811,7 @@ namespace ts { | |
function parseEntityName(allowReservedWords: boolean, diagnosticMessage?: DiagnosticMessage): EntityName { | ||
let entity: EntityName = parseIdentifier(diagnosticMessage); | ||
while (parseOptional(SyntaxKind.DotToken)) { | ||
const node = <QualifiedName>createNode(SyntaxKind.QualifiedName, entity.pos); | ||
const node: QualifiedName = <QualifiedName>createNode(SyntaxKind.QualifiedName, entity.pos); // !!! | ||
node.left = entity; | ||
node.right = parseRightSideOfDot(allowReservedWords); | ||
entity = finishNode(node); | ||
|
@@ -3643,7 +3643,7 @@ namespace ts { | |
let elementName: EntityName = parseIdentifierName(); | ||
while (parseOptional(SyntaxKind.DotToken)) { | ||
scanJsxIdentifier(); | ||
const node = <QualifiedName>createNode(SyntaxKind.QualifiedName, elementName.pos); | ||
const node: QualifiedName = <QualifiedName>createNode(SyntaxKind.QualifiedName, elementName.pos); // !!! | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same comment as above:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same issue. |
||
node.left = elementName; | ||
node.right = parseIdentifierName(); | ||
elementName = finishNode(node); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1408,6 +1408,31 @@ namespace ts { | |
return !!node && (node.kind === SyntaxKind.ArrayBindingPattern || node.kind === SyntaxKind.ObjectBindingPattern); | ||
} | ||
|
||
// A node is an assignment target if it is on the left hand side of an '=' token, if it is parented by a property | ||
// assignment in an object literal that is an assignment target, or if it is parented by an array literal that is | ||
// an assignment target. Examples include 'a = xxx', '{ p: a } = xxx', '[{ p: a}] = xxx'. | ||
export function isAssignmentTarget(node: Node): boolean { | ||
while (node.parent.kind === SyntaxKind.ParenthesizedExpression) { | ||
node = node.parent; | ||
} | ||
while (true) { | ||
const parent = node.parent; | ||
if (parent.kind === SyntaxKind.ArrayLiteralExpression || parent.kind === SyntaxKind.SpreadElementExpression) { | ||
node = parent; | ||
continue; | ||
} | ||
if (parent.kind === SyntaxKind.PropertyAssignment || parent.kind === SyntaxKind.ShorthandPropertyAssignment) { | ||
node = parent.parent; | ||
continue; | ||
} | ||
return parent.kind === SyntaxKind.BinaryExpression && | ||
(<BinaryExpression>parent).operatorToken.kind === SyntaxKind.EqualsToken && | ||
(<BinaryExpression>parent).left === node || | ||
(parent.kind === SyntaxKind.ForInStatement || parent.kind === SyntaxKind.ForOfStatement) && | ||
(<ForInStatement | ForOfStatement>parent).initializer === node; | ||
} | ||
} | ||
|
||
export function isNodeDescendentOf(node: Node, ancestor: Node): boolean { | ||
while (node) { | ||
if (node === ancestor) return true; | ||
|
@@ -1504,7 +1529,7 @@ namespace ts { | |
} | ||
|
||
// True if the given identifier, string literal, or number literal is the name of a declaration node | ||
export function isDeclarationName(name: Node): name is Identifier | StringLiteral | LiteralExpression { | ||
export function isDeclarationName(name: Node): boolean { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why was the type-guardiness of this function removed? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably because this function can also return There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If that was the case, then wouldn't the name of the function be misleading? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not in my opinion, it checks whether a node is a declaration name, but not every identifier is a declaration name. So the type annotation was wrong, not the name. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Seems fair. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, @ivogabe is exactly right about why I removed the type predicate annotation. |
||
if (name.kind !== SyntaxKind.Identifier && name.kind !== SyntaxKind.StringLiteral && name.kind !== SyntaxKind.NumericLiteral) { | ||
return false; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,53 @@ | ||
//// [assignmentTypeNarrowing.ts] | ||
let x: string | number | boolean | RegExp; | ||
|
||
x = ""; | ||
x; // string | ||
|
||
[x] = [true]; | ||
x; // boolean | ||
|
||
[x = ""] = [1]; | ||
x; // string | number | ||
|
||
({x} = {x: true}); | ||
x; // boolean | ||
|
||
({y: x} = {y: 1}); | ||
x; // number | ||
|
||
({x = ""} = {x: true}); | ||
x; // string | boolean | ||
|
||
({y: x = /a/} = {y: 1}); | ||
x; // number | RegExp | ||
|
||
let a: string[]; | ||
|
||
for (x of a) { | ||
x; // string | ||
} | ||
|
||
|
||
//// [assignmentTypeNarrowing.js] | ||
var x; | ||
x = ""; | ||
x; // string | ||
x = [true][0]; | ||
x; // boolean | ||
_a = [1][0], x = _a === void 0 ? "" : _a; | ||
x; // string | number | ||
(_b = { x: true }, x = _b.x, _b); | ||
x; // boolean | ||
(_c = { y: 1 }, x = _c.y, _c); | ||
x; // number | ||
(_d = { x: true }, _e = _d.x, x = _e === void 0 ? "" : _e, _d); | ||
x; // string | boolean | ||
(_f = { y: 1 }, _g = _f.y, x = _g === void 0 ? /a/ : _g, _f); | ||
x; // number | RegExp | ||
var a; | ||
for (var _i = 0, a_1 = a; _i < a_1.length; _i++) { | ||
x = a_1[_i]; | ||
x; // string | ||
} | ||
var _a, _b, _c, _d, _e, _f, _g; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,64 @@ | ||
=== tests/cases/conformance/expressions/assignmentOperator/assignmentTypeNarrowing.ts === | ||
let x: string | number | boolean | RegExp; | ||
>x : Symbol(x, Decl(assignmentTypeNarrowing.ts, 0, 3)) | ||
>RegExp : Symbol(RegExp, Decl(lib.d.ts, --, --), Decl(lib.d.ts, --, --)) | ||
|
||
x = ""; | ||
>x : Symbol(x, Decl(assignmentTypeNarrowing.ts, 0, 3)) | ||
|
||
x; // string | ||
>x : Symbol(x, Decl(assignmentTypeNarrowing.ts, 0, 3)) | ||
|
||
[x] = [true]; | ||
>x : Symbol(x, Decl(assignmentTypeNarrowing.ts, 0, 3)) | ||
|
||
x; // boolean | ||
>x : Symbol(x, Decl(assignmentTypeNarrowing.ts, 0, 3)) | ||
|
||
[x = ""] = [1]; | ||
>x : Symbol(x, Decl(assignmentTypeNarrowing.ts, 0, 3)) | ||
|
||
x; // string | number | ||
>x : Symbol(x, Decl(assignmentTypeNarrowing.ts, 0, 3)) | ||
|
||
({x} = {x: true}); | ||
>x : Symbol(x, Decl(assignmentTypeNarrowing.ts, 11, 2)) | ||
>x : Symbol(x, Decl(assignmentTypeNarrowing.ts, 11, 8)) | ||
|
||
x; // boolean | ||
>x : Symbol(x, Decl(assignmentTypeNarrowing.ts, 0, 3)) | ||
|
||
({y: x} = {y: 1}); | ||
>y : Symbol(y, Decl(assignmentTypeNarrowing.ts, 14, 2)) | ||
>x : Symbol(x, Decl(assignmentTypeNarrowing.ts, 0, 3)) | ||
>y : Symbol(y, Decl(assignmentTypeNarrowing.ts, 14, 11)) | ||
|
||
x; // number | ||
>x : Symbol(x, Decl(assignmentTypeNarrowing.ts, 0, 3)) | ||
|
||
({x = ""} = {x: true}); | ||
>x : Symbol(x, Decl(assignmentTypeNarrowing.ts, 17, 2)) | ||
>x : Symbol(x, Decl(assignmentTypeNarrowing.ts, 17, 13)) | ||
|
||
x; // string | boolean | ||
>x : Symbol(x, Decl(assignmentTypeNarrowing.ts, 0, 3)) | ||
|
||
({y: x = /a/} = {y: 1}); | ||
>y : Symbol(y, Decl(assignmentTypeNarrowing.ts, 20, 2)) | ||
>x : Symbol(x, Decl(assignmentTypeNarrowing.ts, 0, 3)) | ||
>y : Symbol(y, Decl(assignmentTypeNarrowing.ts, 20, 17)) | ||
|
||
x; // number | RegExp | ||
>x : Symbol(x, Decl(assignmentTypeNarrowing.ts, 0, 3)) | ||
|
||
let a: string[]; | ||
>a : Symbol(a, Decl(assignmentTypeNarrowing.ts, 23, 3)) | ||
|
||
for (x of a) { | ||
>x : Symbol(x, Decl(assignmentTypeNarrowing.ts, 0, 3)) | ||
>a : Symbol(a, Decl(assignmentTypeNarrowing.ts, 23, 3)) | ||
|
||
x; // string | ||
>x : Symbol(x, Decl(assignmentTypeNarrowing.ts, 0, 3)) | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,98 @@ | ||
=== tests/cases/conformance/expressions/assignmentOperator/assignmentTypeNarrowing.ts === | ||
let x: string | number | boolean | RegExp; | ||
>x : string | number | boolean | RegExp | ||
>RegExp : RegExp | ||
|
||
x = ""; | ||
>x = "" : string | ||
>x : string | number | boolean | RegExp | ||
>"" : string | ||
|
||
x; // string | ||
>x : string | ||
|
||
[x] = [true]; | ||
>[x] = [true] : [boolean] | ||
>[x] : [string | number | boolean | RegExp] | ||
>x : string | number | boolean | RegExp | ||
>[true] : [boolean] | ||
>true : boolean | ||
|
||
x; // boolean | ||
>x : boolean | ||
|
||
[x = ""] = [1]; | ||
>[x = ""] = [1] : [number] | ||
>[x = ""] : [string] | ||
>x = "" : string | ||
>x : string | number | boolean | RegExp | ||
>"" : string | ||
>[1] : [number] | ||
>1 : number | ||
|
||
x; // string | number | ||
>x : string | number | ||
|
||
({x} = {x: true}); | ||
>({x} = {x: true}) : { x: boolean; } | ||
>{x} = {x: true} : { x: boolean; } | ||
>{x} : { x: string | number | boolean | RegExp; } | ||
>x : string | number | boolean | RegExp | ||
>{x: true} : { x: boolean; } | ||
>x : boolean | ||
>true : boolean | ||
|
||
x; // boolean | ||
>x : boolean | ||
|
||
({y: x} = {y: 1}); | ||
>({y: x} = {y: 1}) : { y: number; } | ||
>{y: x} = {y: 1} : { y: number; } | ||
>{y: x} : { y: string | number | boolean | RegExp; } | ||
>y : string | number | boolean | RegExp | ||
>x : string | number | boolean | RegExp | ||
>{y: 1} : { y: number; } | ||
>y : number | ||
>1 : number | ||
|
||
x; // number | ||
>x : number | ||
|
||
({x = ""} = {x: true}); | ||
>({x = ""} = {x: true}) : { x?: boolean; } | ||
>{x = ""} = {x: true} : { x?: boolean; } | ||
>{x = ""} : { x?: string | number | boolean | RegExp; } | ||
>x : string | number | boolean | RegExp | ||
>{x: true} : { x?: boolean; } | ||
>x : boolean | ||
>true : boolean | ||
|
||
x; // string | boolean | ||
>x : string | boolean | ||
|
||
({y: x = /a/} = {y: 1}); | ||
>({y: x = /a/} = {y: 1}) : { y?: number; } | ||
>{y: x = /a/} = {y: 1} : { y?: number; } | ||
>{y: x = /a/} : { y?: RegExp; } | ||
>y : RegExp | ||
>x = /a/ : RegExp | ||
>x : string | number | boolean | RegExp | ||
>/a/ : RegExp | ||
>{y: 1} : { y?: number; } | ||
>y : number | ||
>1 : number | ||
|
||
x; // number | RegExp | ||
>x : number | RegExp | ||
|
||
let a: string[]; | ||
>a : string[] | ||
|
||
for (x of a) { | ||
>x : string | number | boolean | RegExp | ||
>a : string[] | ||
|
||
x; // string | ||
>x : string | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the new type annotation and
// !!!
comment for?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since
Entity
is a type alias of a union, it will be narrowed after its assignment. This creates a circular dependency during the type analysis. Because of this,node
would be typed asany
if the type annotation was not given.@ahejlsberg Would it be possible to give the inference of the type of the initializer precedence over the narrowing after assignments?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The exact problem here is the following: We infer the type of
node
from the call tocreateNode
. To evaluate that call we need to know the type ofentity
so we can find the type of thepos
property. To find the type ofentity
we look at the preceding code paths. One leads from the top of the function. In that code path,entity
has typeIdentifier
, which is less than its full declared type (Identifier | QualifiedName
). Therefore we need to also analyze the second code path that comes from the bottom of the while statement (the loop around case). In that code path, we have the assignmententity = finishNode(node)
which requires us to know the type ofnode
. Boom! Circularity. Not exactly clear what rule we'd introduce to break that circularity.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would just assuming that
entity
has its original declared type be good enough?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
One other idea is that while doing the analysis, if you hit a type assertion, ignore the RHS, and just use the type associated with the type assertion.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes that's a good point. It's not a simple matter of using the declared type. But I think the overloaded
next
function wouldn't even accept a union type as an argument.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No, it wouldn't with how TS resolves signatures at present - at least not without another signature declaring it as taking a parameter of type
any
or a union of the other types. (Which, for some reason feels off to me - if every member of a union of argument types can be fulfilled by an overload and there's no better match, why can't the return be the union of those overloads' return values? Question for another time.)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
(Answer for another time ;)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@weswigham @JsonFreeman There are actually several issues being debated here:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks Anders, I think that's a great breakdown. I'll add that (if I'm not mistaken), this separation of resolution and checking is in place for bodies of function expressions. The idea here would be to generalize it for other expressions.
I agree that iterating to a fixed point may be necessary for loops in which the variable is reassigned in the loop body.