-
Notifications
You must be signed in to change notification settings - Fork 13k
Infer from usage by similarity to builtin types #33263
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
Conversation
Basically, drop "Context" from all names, because it just indicates that it's an implementation of the State monad.
1. Everything explodes! Out of stack space! 2. Results aren't used yet. 3. But call and construct use the new getSignatureFromCalls, so I expect some baseline changes after I get the infinite recursion fixed.
Type parameter inference is special-cased, just moved from its previous place with no improvement.
It's a smeary copy of the checker's type parameter, so I feel bad about duplicating that code. Not sure what the solution is, architecturally.
} | ||
|
||
interface Usage { | ||
isNumber?: boolean; | ||
isString?: boolean; | ||
isNumber: boolean | undefined; |
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.
cleanup: all Usage properties are now required and Usages are created with createEmptyUsage.
@@ -626,6 +640,9 @@ namespace ts.codefix { | |||
else if (otherOperandType.flags & TypeFlags.StringLike) { | |||
usage.isString = true; | |||
} | |||
else if (otherOperandType.flags & TypeFlags.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.
driveby fix (2a)
@@ -772,7 +790,7 @@ namespace ts.codefix { | |||
good = good.filter(i => !(checker.getObjectFlags(i) & ObjectFlags.Anonymous)); | |||
good.push(unifyAnonymousTypes(anons)); | |||
} | |||
return checker.getWidenedType(checker.getUnionType(good)); | |||
return checker.getWidenedType(checker.getUnionType(good.map(checker.getBaseTypeOfLiteralType), UnionReduction.Subtype)); |
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.
driveby fix (2b)
@@ -875,45 +885,183 @@ namespace ts.codefix { | |||
|
|||
types.push(checker.createAnonymousType(/*symbol*/ undefined!, members, callSignatures, constructSignatures, stringIndexInfo, /*numberIndexInfo*/ undefined)); // TODO: GH#18217 | |||
} | |||
return types; | |||
return types; // TODO: Should cache this since I HOPE it doesn't change |
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.
like normal checking, caching should be fine here. I'll do that in a followup PR.
This makes inferences a lot better.
void is explicitly inferred now, never used as a fallback.
@@ -490,6 +551,9 @@ namespace ts.codefix { | |||
} | |||
|
|||
switch (node.parent.kind) { | |||
case SyntaxKind.ExpressionStatement: | |||
addCandidateType(usage, checker.getVoidType()); |
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.
driveby fix (2c)
|
||
if (usage.properties && hasCalls(usage.properties.get("then" as __String))) { | ||
const paramType = getParameterTypeFromCalls(0, usage.properties.get("then" as __String)!.calls!, /*isRestParameter*/ false)!; // TODO: GH#18217 | ||
const types = paramType.getCallSignatures().map(sig => sig.getReturnType()); |
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 was wrong--for the expression numberPromise.then(n => n.toString())
, it would infer Promise<string>
not Promise<number>
for numberPromise
.
@andrewbranch @orta @weswigham I think this is ready for review now. Not sure who is most interested in this. |
@@ -524,6 +524,9 @@ namespace ts { | |||
}, | |||
getApparentType, | |||
getUnionType, | |||
isTypeAssignableTo: (source, target) => { |
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.
Even @internal
, some people are going to be ecstatic that this finally found its way into our API surface and will therefore finally be callable by people using the API (even if via cast).
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.
Uhhhh should I be worried about exposing too much? I feeling like assignability is a State Secret or something.
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'm really interested in the reason why it should be marked as @internal
. Don't get me wrong, I'm one of those people ecstatic that this found it's way to the API, now I can ditch my custom typescript version from my library which exposed this. I'm just super curious about the reasoning behind this decision :)
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.
Effectively this function now has to cross conceptual boundaries in TypeScript. Going from being useful internally only inside checker to being used inside the TSServices, which meant it needed to be exposed as a public API on the checker internally within our codebase,
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.
Ok, but why the @internal
annotation? It looks like the method is still kinda private?
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'm not 100% convinced that arbitrary uses of this function are safe and won't corrupt compiler state. Marking it @internal
allows me to use it inside Typescript where I know how it's being used. Using it outside is an any
cast away, but that way it's obvious that it's not technically supported.
@@ -680,7 +747,7 @@ namespace ts.codefix { | |||
} | |||
} | |||
|
|||
calculateUsageOfNode(parent, call.returnType); | |||
calculateUsageOfNode(parent, call.return_); |
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 is a return_
?
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.
return_: Usage
, which is the information we inferred about a signature's return type from calls.
The old name, returnType
, implies that it's a type already, which it's not; that comes from inferring a usage and unifying the resulting list of types.
Unifying is a bad name too, it should probably be reconcile or aggregate.
@@ -694,7 +761,7 @@ namespace ts.codefix { | |||
if (!usage.properties) { | |||
usage.properties = createUnderscoreEscapedMap<Usage>(); | |||
} | |||
const propertyUsage = usage.properties.get(name) || { }; | |||
const propertyUsage = usage.properties.get(name) || createEmptyUsage(); |
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, this feels nicer to me 👍
return unifyFromUsage(inferFromUsage(innerUsage)); | ||
/** | ||
* inference is limited to | ||
* 1. generic types with a single parameter |
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'm impressed we can do this 👍 - I assume it's use by the promise and array implementations
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.
Yep, plus I intend to expand this to other types later.
Tests were failing because I forgot to update a couple of baselines with the new inference code, which is unable to infer from arguments |
} | ||
if (propUsage.calls) { | ||
const sigs = checker.getSignaturesOfType(source, SignatureKind.Call); | ||
result = result && !!sigs.length && checker.isTypeAssignableTo(source, getFunctionFromCalls(propUsage.calls)); |
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.
Here’s an interesting test case:
function test(a) {
a.toString(2)
}
Expected result: a
is number
Actual result: a
is { toString: (arg0: number) => void; }
It happens because every builtin’s toString
call signature is assignable to the inferred structural one, and then the inference to builtins bails because of the limit of 3 union constituents. I briefly thought source
and target
might be backwards here—that you actually want to see if the usage is assignable to the builtin’s property—but that’s too restrictive; the parameter type of arg0: number
isn’t assignable to radix?: number
under strictFunctionTypes
because of the optionality of the latter, and the inferred return type void
is not assignable to string
. (Interestingly, if you swap target
and source
, turn off strictFunctionTypes
, and change the line to let x = a.toString(2)
, the inference to number
occurs.)
I was able to hack up a fix for this something like:
const sigs = checker.getSignaturesOfType(source, SignatureKind.Call);
const usageType = getFunctionFromCalls(propUsage.calls);
const maxSourceParams = sigs.reduce((max, sig) => Math.max(max, sig.parameters.length), 0);
const maxUsageParams = usageType.getCallSignatures().reduce((max, sig) => Math.max(max, sig.parameters.length), 0);
result = result && !!sigs.length && maxUsageParams <= maxSourceParams && checker.isTypeAssignableTo(source, usageType);
but admittedly it feels kind of bad that checking assignability doesn’t Just Work™. Maybe I’m missing something clever?
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.
Errr, could we get better results by checking subtype first, then assignability, like we do for overload resolution?
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 think the root problem is that we expect to infer signatures whose lengths match, and have that beat the signatures whose lengths don't, right? I think an rule in inferFromUsage would be better than trying to get subtype or assignability to do the right thing, since they have other concerns to worry about -- they are part of the Real World Checker that has to be correct, but I feel like this preference is part of the Dream World Inference we have going on here, where you choose the best candidate on what feels right a lot of the time.
This would require maintaining some additional information -- specifically a list of source signatures matched with usage-signatures, and then that matched list would be used to rank the built-in types instead of just using filter
. Basically, combineNamedTypes
, by analogy with combineTypes
. Maybe the decision could be even deferred to combineTypes
.
Let's take this PR as-is and I'll think about it for the future.
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.
Oh, I mean, I just suggested subtype before assignability because i thought the difference in rules would already do the right thing (tm).
I think this PR is ready for review now.
This PR:
It also infers type parameters for Array and Promise, using an algorithm that is disturbingly close to the real inference algorithm. On the whole, infer-from-usage is like a shadow checker with subtly different rules. I don't really like this, but it may be inherent in the way infer-from-usage is designed. Thoughts?
Notes:
a. In
+
inference,T + any
no longer infersT = string | number
.b. inference type "unification" (it is more like aggregation and selection) no longer infers literal types and now uses subtype reduction when forming unions.
c. Return type inference no longer defaults to
void
; being in an expression statement infersvoid
instead.I'm not sure (2a) is correct; #30093 tracks problems with
+
inference and has some examples I need to try.Follow up work:
concat
doesn't work because it has two overloads.None of these are particularly required but (2) should help a lot and not be too hard.