-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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 analysis for implicit any variables #11263
Conversation
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.
A couple of questions each about the change and the tests, plus a couple of ideas for the tests.
@@ -3050,6 +3051,11 @@ namespace ts { | |||
return undefined; | |||
} | |||
|
|||
function isAutoVariableInitializer(initializer: Expression) { | |||
const expr = initializer && skipParentheses(initializer); | |||
return !expr || expr.kind === SyntaxKind.NullKeyword || expr.kind === SyntaxKind.Identifier && getResolvedSymbol(<Identifier>expr) === undefinedSymbol; |
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.
Can this work for []
or {}
as well?
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, but they each need specialized strategies and I'll cover them in separate PRs.
@@ -8952,7 +8971,7 @@ namespace ts { | |||
if (isRightSideOfQualifiedNameOrPropertyAccess(location)) { | |||
location = location.parent; | |||
} | |||
if (isExpression(location) && !isAssignmentTarget(location)) { | |||
if (isPartOfExpression(location) && !isAssignmentTarget(location)) { |
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 does this change and why? I'm guessing that now we can get the type of a symbol given a child of the symbol's declaration, but I can't guess why.
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.
This is a bug that somehow got introduced during some refactoring for the new emitter. The old isExpression
was renamed to isPartOfExpression
and a new isExpression
was introduced that doesn't look at the context (i.e. doesn't access the parent
property).
@@ -14,7 +14,7 @@ for (var x in fn()) { } | |||
for (var x in /[a-z]/) { } | |||
for (var x in new Date()) { } | |||
|
|||
var c, d, e; | |||
var c: any, d: any, e: any; |
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.
why add any
here? Is it to preserve the intent of the tests — does control flow pick a union type for these variables now?
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 test just needed some values of type any
but they aren't actually meaningful once we start analyzing control flow.
@@ -1,10 +1,10 @@ | |||
// @lib: es5,es2015.promise | |||
// @noEmitHelpers: true | |||
// @target: ES5 | |||
declare var x, y, z, a, b, c; | |||
declare var x: any, y: any, z: any, a: any, b: any, c: any; |
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.
I thought that auto typing didn't apply to ambient contexts. Why did you have to add any
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.
Yeah, you're right about that. But having them doesn't hurt and is a better indication of intent (i.e. we expressly don't want CFA here).
@@ -1,6 +1,6 @@ | |||
//@target: ES5 | |||
module M { | |||
var Symbol; | |||
var Symbol: any; |
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.
I don't understand why auto typing would apply here either
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.
Same here.
|
||
declare let cond: boolean; | ||
|
||
function f1() { |
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.
It would be nice if these functions had names that showed what they were testing.
if (cond) { | ||
x = "hello"; | ||
} | ||
const y = x; |
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.
if you have if (cond) .. else
then y: string | number
, right? Might be nice to have a case for that.
@mhegazy Want to take a look? |
* Fix fialing tests after microsoft/TypeScript#11263 * Fix more failures
@ahejlsberg function f(x: number[]): void { ... }
let x = []; // x: any[]
x.push(3); // x: number[]
f(x); // x is ok according to control flow, so no --noImplicitAny error? |
You are correct. this is not flagged as an implicit any error. |
@mhegazy this is not safe, though. function f(x: number[]): void {
setImmediate(function () {
console.log(x.reduce((a, b) => a * b, 1));
});
}
let x = [];
x.push(3);
f(x);
x.push("I am not a number"); // ok, now x: (string|number)[] |
Typescript is already unsafe in this way. You can also write
|
Never mind, I was completely wrong. It doesn't work. |
@sandersn But I think many people like TS for being (for the most part) statically type-safe and use Thus, I would suggest that you limit what is allowed by this feature or (harder) only allow cases that are provably correct (e.g. determine that the dynamic type never changes to an incompatible type). I can at least imagine the following operations to be potentially unsafe: saving the value in an object, an array or a variable in an outer scope; assigning the value to a setter; passing the value to a function; capturing the value in a closure; calling a member of the value; new-ing the value. |
@jods4, the compiler today does not guard against side effects in multiple places. it is not specific to noImplicitAny checks. for instance: function f(x: { a: number }) {
setTimeout(() => {
x.a.toFixed();
}, 10);
}
var x: { kind: "number", a: number } | { kind: "string", a: string };
if (x.kind === "number") {
f(x);
}
x.a = "string"; // potentially unsafe, as x has already been captured. Arrays are just like objects with properties when it comes to narrowing. I do understand your concern, but really this issue is a general design limitation and not specific to this change. |
@mhegazy That said, having safety holes doesn't necessarily mean we should add new ones. I guess the question is: do benefits outweight the drawbacks? Your example is a lot more convoluted than the simple example I gave, which was just: let x = [];
x.push(3);
f(x);
x.push("NaN"); I guess what annoys me most here is that this is unsafe because I use My opinion:
To maximize both safety and value, I would suggest that under let x; // this is not an error yet.
x = 4; // so x: number at this point
console.log(x.toFixed()); // sure
x += 5; // and so on...
let y; // same thing
y = 4; // so y: number
y = "Four"; // still an error when --noImplicitAny. Can't use dynamic types without explicit declaration. |
This PR introduces control flow analysis for
let
andvar
variables that have no type annotation and either no initial value or an initial value ofnull
orundefined
.In the example above,
x
andy
implicitly have declared types ofany
but control flow analysis can determine their actual types at every reference. Therefore, no errors are reported even when the example is compiled with--noImplicitAny
.Control flow analysis is unable to determine the actual types of implicit
any
variables when they are referenced in nested functions. In such cases, the variables will appear to have typeany
in the nested functions and errors will be reported for the references if--noImplicitAny
is enabled.In time it is possible that control flow analysis will be able to more accurately determine types of references to outer variables from nested functions in some cases, but given that nested functions are first class objects that can be arbitrarily passed around and called, it is effectively impossible to analyze all such scenarios.