-
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
Avoid excess property check for inference blocked source #52393
Avoid excess property check for inference blocked source #52393
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 flag set by a completions call should have no effect on errors. The checker state must be getting corrupted earlier; this is just silencing the problem when we need to fix it at the source instead. Unfortunately, I really don’t know where to start with this 😕
This makes sense conceptually - especially this part:
However, I debugged this quite a bit and this is roughly what happens:
We need to resolve the signature to obtain potential completions from the contextual type and resolving the signature has some logic related to reporting errors etc. I don't see how this could be caused by the "corrupted checker state". I actually started this investigation by looking for such a corrupted state but later on realized that it's perhaps not the reason behind this. Every step that I listed here is required for obtaining completions and, to the best of my understanding, this doesn't rely on any corrupted state - the problem is that we obtain Alternatively, I could try to suppress I might be missing some nuance here - but so far (and I've reexamined the situation again after your comment) I stand by the initial change :) |
Oh, so is the error issued during the call made from completions.ts? @DanielRosenwasser have we figured out a general approach for these services-calls-messing-up-unified-checker issues? |
I feel like pretty much any checker API call besides |
The only thing I know of is that there are certain check-modes that are used to make sure that we don't contextually type certain arguments during inference, and there's something like that for signature help (which clearly isn't fixing the error-reporting issue). @weswigham and @ahejlsberg might have a better idea on this one. |
@DanielRosenwasser yeah, I would say we need something similar here, but we’re having so many of this brand of problem that I wonder if we should come up with a similar, but more general, mechanism and apply it across most/all of the API entrypoints, similar to how we wrap most of the entrypoints that take a Node with
@Andarist can you explain your thoughts behind this? If we detect we’re coming from a checker API call, we definitely want to avoid reporting (or doing the work to compute) errors. But is the assignability failure you fixed in your proposed change affecting the contextual type in an observable way? |
I'm not sure what I had exactly in mind right now. I've re-examined the situation once again and I don't think this was any good idea 😅
We still need to check for assignability etc - so we want to "error" within OTOH, the blocked inference... can impact other arguments and we might "accidentally" report an error even though, at a given moment, there is no error. I was just able to do this with this code: type Constraint = { type: "foo" } | { type: "bar"; extra?: string };
declare function test<T extends Constraint>(
a: T,
b: Constraint extends T ? never : 1
): void;
test({ type: "bar" }, 1); When trying to add
This one is much harder to reproduce. I was only able to do it once and now I can't repro it anymore. I'm sure that I didn't actually change the source code anyhow - I was just editing the first argument of this function and somehow ended up with this situation. I was also able to end up in the opposite situation. I ended up with this not displaying any error - even though the error should be there: test({ type: "bar", extra: 1 }, 1); // extra has an incompatible type now, so the error should be reported here
I wasn't able to observe an unwanted change anyhow. I think though there is a slight potential for a behavior change here in some convoluted situations with multiple overloads. If I understand correctly - the contextual type here is still sourced from a single overload so if we'd craft a situation in which the first overload would report this error but the second one wouldn't... then we would end up with the contextual type from the second one whereas after my change we could successfully match the first overload and thus we would end up with contextual type from the first overload. That would be an improvement though because the scenario without the inference blocked would actually select the first overload and not the second one in this scenario. |
At least one of us is not quite understanding the other and I’m not sure which of us it is 😅. The editor displays red squigglies by calling It seems like the trouble happens when this completions request happens before errors are requested, and something about the That’s my understanding of it, anyway, without debugging it. I could be wrong. Does that fit what you’ve seen? |
This looks like an important bit to focus on here. I'm not saying that it should work like that - but I will go into what currently happens:
Now, there might be some missing step here - something I didn't observe or something that should be called but isn't. Perhaps the said cache should be cleared before going into a fresh "type check". However, according to the quote from your comment, it is expected that we might have some diagnostics produced by the completions request and that we might end up deduping the gathered errors etc. So the sole fact that the diagnostics were grabbed from the cache isn't the problem. Perhaps this particular diagnostic should be somehow cleared. That would however be, IMHO, harder to achieve - it's easier to prevent than it is to ignore something after the fact. To ignore you need to preserve somewhere some contextual information as metadata of the said error - to decide later on if it should be ignored or not. And if it should be ignored - what's even the point of adding it to the cache in the first place? On top of that, I still think that this particular error should not be added to the cache at all. It's a result of the So I recognize that "suppressing" this error might not be the whole story, but I still think that it should be suppressed (of course, I'm always happy to be proven wrong here, I'm not an expert on this codebase and my findings might be somehow off). The additional question is if the mechanism used to suppress it is a good one and if it's applied at the correct level etc - that is something that you have to judge for yourself. |
No no, I was only trying to state what I think is happening, not what should happen. I think we’re on the same page about what’s happening. What I’m proposing we do is essentially clear errors at the end of non-diagnostics API calls like those used in completions, or equivalently make |
This sort of thing would have prevented the error #52373 from being produced, so I feel like we should do something like that (in this case, it was a lazyDiagnostic, which should also probably be silenced). Although, that's effectively just what it was when we had two type checkers. That being said, it's also tricky because without the diagnostic, the actual underlying bug would have never been found (but, if nobody observes a bug, did it exist?). |
There is already a flag telling the compiler bits that this is a completion request - but that's in the I quickly checked if this could be used to fix this case if I would pass it along. The slight problem is that It fixed the case at hand while regressing on one of the tests ( diff with the context flag mapped to a context modediff --git a/src/compiler/checker.ts b/src/compiler/checker.ts
index 2f943279c9..ae1e76f1d1 100644
--- a/src/compiler/checker.ts
+++ b/src/compiler/checker.ts
@@ -20358,7 +20358,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
isSimpleTypeRelatedTo(source, target, relation, reportErrors ? reportError : undefined)) return Ternary.True;
if (source.flags & TypeFlags.StructuredOrInstantiable || target.flags & TypeFlags.StructuredOrInstantiable) {
- const isPerformingExcessPropertyChecks = !isFromInferenceBlockedSource(source) && !(intersectionState & IntersectionState.Target) && (isObjectLiteralType(source) && getObjectFlags(source) & ObjectFlags.FreshLiteral);
+ const isPerformingExcessPropertyChecks = !(intersectionState & IntersectionState.Target) && (isObjectLiteralType(source) && getObjectFlags(source) & ObjectFlags.FreshLiteral);
if (isPerformingExcessPropertyChecks) {
if (hasExcessProperties(source as FreshObjectLiteralType, target, reportErrors)) {
if (reportErrors) {
@@ -28521,13 +28521,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
// In a typed function call, an argument or substitution expression is contextually typed by the type of the corresponding parameter.
- function getContextualTypeForArgument(callTarget: CallLikeExpression, arg: Expression): Type | undefined {
+ function getContextualTypeForArgument(callTarget: CallLikeExpression, arg: Expression, contextFlags?: ContextFlags): Type | undefined {
const args = getEffectiveCallArguments(callTarget);
const argIndex = args.indexOf(arg); // -1 for e.g. the expression of a CallExpression, or the tag of a TaggedTemplateExpression
- return argIndex === -1 ? undefined : getContextualTypeForArgumentAtIndex(callTarget, argIndex);
+ return argIndex === -1 ? undefined : getContextualTypeForArgumentAtIndex(callTarget, argIndex, contextFlags);
}
- function getContextualTypeForArgumentAtIndex(callTarget: CallLikeExpression, argIndex: number): Type {
+ function getContextualTypeForArgumentAtIndex(callTarget: CallLikeExpression, argIndex: number, contextFlags?: ContextFlags): Type {
if (isImportCall(callTarget)) {
return argIndex === 0 ? stringType :
argIndex === 1 ? getGlobalImportCallOptionsType(/*reportErrors*/ false) :
@@ -28536,7 +28536,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
// If we're already in the process of resolving the given signature, don't resolve again as
// that could cause infinite recursion. Instead, return anySignature.
- const signature = getNodeLinks(callTarget).resolvedSignature === resolvingSignature ? resolvingSignature : getResolvedSignature(callTarget);
+ const signature = getNodeLinks(callTarget).resolvedSignature === resolvingSignature ? resolvingSignature : getResolvedSignature(callTarget, /*candidatesArray*/ undefined, (contextFlags || 0) & ContextFlags.Completions ? CheckMode.IsForStringLiteralArgumentCompletions : CheckMode.Normal);
if (isJsxOpeningLikeElement(callTarget) && argIndex === 0) {
return getEffectiveFirstArgumentForJsxSignature(signature, callTarget);
@@ -29017,7 +29017,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return getContextualTypeForAwaitOperand(parent as AwaitExpression, contextFlags);
case SyntaxKind.CallExpression:
case SyntaxKind.NewExpression:
- return getContextualTypeForArgument(parent as CallExpression | NewExpression | Decorator, node);
+ return getContextualTypeForArgument(parent as CallExpression | NewExpression | Decorator, node, contextFlags);
case SyntaxKind.Decorator:
return getContextualTypeForDecorator(parent as Decorator);
case SyntaxKind.TypeAssertionExpression:
@@ -32559,7 +32559,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const isTaggedTemplate = node.kind === SyntaxKind.TaggedTemplateExpression;
const isDecorator = node.kind === SyntaxKind.Decorator;
const isJsxOpeningOrSelfClosingElement = isJsxOpeningLikeElement(node);
- const reportErrors = !candidatesOutArray;
+ const reportErrors = !candidatesOutArray && !(checkMode & CheckMode.IsForStringLiteralArgumentCompletions);
let typeArguments: NodeArray<TypeNode> | undefined;
So later I tried muting the diff with `diagnostics.add` muted with `noop`diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts
index 2f943279c9..421ddcda8c 100644
--- a/src/compiler/checker.ts
+++ b/src/compiler/checker.ts
@@ -1808,26 +1808,33 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
};
function runWithInferenceBlockedFromSourceNode<T>(node: Node | undefined, fn: () => T): T {
- const containingCall = findAncestor(node, isCallLikeExpression);
- const containingCallResolvedSignature = containingCall && getNodeLinks(containingCall).resolvedSignature;
- if (containingCall) {
- let toMarkSkip = node!;
- do {
- getNodeLinks(toMarkSkip).skipDirectInference = true;
- toMarkSkip = toMarkSkip.parent;
- } while (toMarkSkip && toMarkSkip !== containingCall);
- getNodeLinks(containingCall).resolvedSignature = undefined;
- }
- const result = fn();
- if (containingCall) {
- let toMarkSkip = node!;
- do {
- getNodeLinks(toMarkSkip).skipDirectInference = undefined;
- toMarkSkip = toMarkSkip.parent;
- } while (toMarkSkip && toMarkSkip !== containingCall);
- getNodeLinks(containingCall).resolvedSignature = containingCallResolvedSignature;
+ const originalDiagnosticsAdd = diagnostics.add;
+ diagnostics.add = () => {};
+ try {
+ const containingCall = findAncestor(node, isCallLikeExpression);
+ const containingCallResolvedSignature = containingCall && getNodeLinks(containingCall).resolvedSignature;
+ if (containingCall) {
+ let toMarkSkip = node!;
+ do {
+ getNodeLinks(toMarkSkip).skipDirectInference = true;
+ toMarkSkip = toMarkSkip.parent;
+ } while (toMarkSkip && toMarkSkip !== containingCall);
+ getNodeLinks(containingCall).resolvedSignature = undefined;
+ }
+ const result = fn();
+ if (containingCall) {
+ let toMarkSkip = node!;
+ do {
+ getNodeLinks(toMarkSkip).skipDirectInference = undefined;
+ toMarkSkip = toMarkSkip.parent;
+ } while (toMarkSkip && toMarkSkip !== containingCall);
+ getNodeLinks(containingCall).resolvedSignature = containingCallResolvedSignature;
+ }
+ return result;
+ }
+ finally {
+ diagnostics.add = originalDiagnosticsAdd;
}
- return result;
}
function getResolvedSignatureWorker(nodeIn: CallLikeExpression, candidatesOutArray: Signature[] | undefined, argumentCount: number | undefined, checkMode: CheckMode, editingArgument?: Node): Signature | undefined {
@@ -20358,7 +20365,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
isSimpleTypeRelatedTo(source, target, relation, reportErrors ? reportError : undefined)) return Ternary.True;
if (source.flags & TypeFlags.StructuredOrInstantiable || target.flags & TypeFlags.StructuredOrInstantiable) {
- const isPerformingExcessPropertyChecks = !isFromInferenceBlockedSource(source) && !(intersectionState & IntersectionState.Target) && (isObjectLiteralType(source) && getObjectFlags(source) & ObjectFlags.FreshLiteral);
+ const isPerformingExcessPropertyChecks = !(intersectionState & IntersectionState.Target) && (isObjectLiteralType(source) && getObjectFlags(source) & ObjectFlags.FreshLiteral);
if (isPerformingExcessPropertyChecks) {
if (hasExcessProperties(source as FreshObjectLiteralType, target, reportErrors)) {
if (reportErrors) {
I think that both of those are not ideal. The first one would require a lot of plumbing and the second one merely mutes the So I actually like your third idea the best - I propose that I add a new "global" (to the checker) flag and "consult" it at all |
I’d like to get @weswigham’s thoughts. |
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.
Honestly, this is probably a reasonable starter approach, but we definitely need to make sure this is reflected in the relationship cache key of the parent relation, since, if this result is cached, this lack of an error result could pollute file errors in the same way the extra completions-triggered error is currently polluting the error list.
Because of how inference blocked sources are applied, I think you'd need to set up a new relation cache key part (a la intersectionState
) that gets checked & passed in at the top level of the relationship check (something like an isOrContainsInferenceBlockedType
which is called on the source & target to switch a bit in the cache key), since the inference-blocked-ness of a type isn't actually part of the type's identity (and so doesn't cascade into new identities for types which contain them).
Could you disambiguate "this" here? 😅 I'm not sure if you refer to the current content of this PR (avoiding excess property check) or to one of the mentioned alternatives.
I'll have to study this a little bit more to understand this properly. I didn't yet dive too much into the mentioned bits of the checker. Thanks for the pointers though! |
The current approach in the PR.
Yeah, I'm just saying that results that depend on this new check need to be cached separately from normal results. (If they should be cached at all - given you could get multiple completions requests in a row that inference block and unblock different things, it's very possible that any cached comparisons done during an inference-blocked request should get thrown out entirely - I just don't know how negative that could be for perf.) |
I've merged #52728; should this be closed, or is there still something to do here? |
fixes #50818
cc @jakebailey @andrewbranch