Skip to content

Fixed an issue with errors not being correctly reported after completion requests in functions within nested calls #54944

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

Merged
Show file tree
Hide file tree
Changes from all 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
18 changes: 14 additions & 4 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1833,20 +1833,30 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
};

function runWithoutResolvedSignatureCaching<T>(node: Node | undefined, fn: () => T): T {
const cachedSignatures = [];
const cachedResolvedSignatures = [];
const cachedTypes = [];
Comment on lines +1836 to +1837
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am quite worried that this is not enough to fix all the cases. I feel like focusing on isCallLikeExpression and isFunctionLike won't handle everything. For example, a reverse mapped type (and its properties) might get cached and a "cleared' function/call might potentially pull from a "corrupted" type.

I really wonder if this just shouldn't clear every cached link on the way from the node to the root. It's still hard to determine what exact properties might have to be cleared from links but likely this would cover for more cases and with time the list of cleared (and brought back later) properties would get more complete.

This isn't ideal but with the given architecture, I don't see a better way around this. You had 2 separate checkers in the past but even that was likely prone to similar problems - they were just less apparent (one could request completions, or something else, at 2 different locations within the same call - each of those requests should not depend on the information computed by the other one but it definitely did depend on it in subtle ways).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should just temporarily unset all symbol/node links altogether here? it feels that they should be able to recreate themselves from scratch during the request 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After a second though... it's probably not that unlikely that a cached type within the same call~ can mess things up (even if it's like in a separate subtree of the call).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe we should just temporarily unset all symbol/node links altogether here? it feels that they should be able to recreate themselves from scratch during the request 🤔

That's more correct, what we have here is a compromise for performance in the common case. Cache invalidation is hard. We want to do 2 things in this function, ideally:

  1. Unset any existing, partial result for the signature in question (or just ignore the current result), since we're going to rerun the signature check with some differing flags, and, by extension, any signature whose result depends on it (thus, any syntactic parent, at least).
  2. Ensure that any calculations we do that are affected by those differing flags (hard to track) aren't cached into the checker long-term. Basically we want to speculate (like we do in the parser), but we lack a real speculation helper inside the checker.

This is... probably better, even if it's not a complete fix, just because it invalidates more potentially bad parts of the cache.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had one alternative idea for a potentially more complete fix. Symbol links and node links are primary cache mechanisms related to this class of problems. So maybe we could reassign getSymbolLinks and getNodeLinks to versions that would not persist results in the "real" links. We could likely still consult the real links for nodes/symbols from different source files. However, we'd use a different "cache" for all nodes/symbols from the current source file. This should avoid broken results altogether as broken results are related to cached information about the nodes close to the one that is the target~ location of the current LSP request.

while (node) {
if (isCallLikeExpression(node)) {
if (isCallLikeExpression(node) || isFunctionLike(node)) {
const nodeLinks = getNodeLinks(node);
const resolvedSignature = nodeLinks.resolvedSignature;
cachedSignatures.push([nodeLinks, resolvedSignature] as const);
cachedResolvedSignatures.push([nodeLinks, resolvedSignature] as const);
nodeLinks.resolvedSignature = undefined;
}
if (isFunctionLike(node)) {
const symbolLinks = getSymbolLinks(getSymbolOfDeclaration(node));
const type = symbolLinks.type;
cachedTypes.push([symbolLinks, type] as const);
symbolLinks.type = undefined;
}
node = node.parent;
}
const result = fn();
for (const [nodeLinks, resolvedSignature] of cachedSignatures) {
for (const [nodeLinks, resolvedSignature] of cachedResolvedSignatures) {
nodeLinks.resolvedSignature = resolvedSignature;
}
for (const [symbolLinks, type] of cachedTypes) {
symbolLinks.type = type;
}
return result;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
Syntactic Diagnostics for file '/tests/cases/fourslash/typeErrorAfterStringCompletionsInNestedCall2.ts':


==== /tests/cases/fourslash/typeErrorAfterStringCompletionsInNestedCall2.ts (0 errors) ====

type ActionFunction<
TExpressionEvent extends { type: string },
out TEvent extends { type: string }
> = {
({ event }: { event: TExpressionEvent }): void;
_out_TEvent?: TEvent;
};

interface MachineConfig<TEvent extends { type: string }> {
types: {
events: TEvent;
};
on: {
[K in TEvent["type"]]?: ActionFunction<
Extract<TEvent, { type: K }>,
TEvent
>;
};
}

declare function raise<
TExpressionEvent extends { type: string },
TEvent extends { type: string }
>(
resolve: ({ event }: { event: TExpressionEvent }) => TEvent
): {
({ event }: { event: TExpressionEvent }): void;
_out_TEvent?: TEvent;
};

declare function createMachine<TEvent extends { type: string }>(
config: MachineConfig<TEvent>
): void;

createMachine({
types: {
events: {} as { type: "FOO" } | { type: "BAR" },
},
on: {
FOO: raise(({ event }) => {
return {
type: "BAR" as const,
};
}),
},
});

Semantic Diagnostics for file '/tests/cases/fourslash/typeErrorAfterStringCompletionsInNestedCall2.ts':
/tests/cases/fourslash/typeErrorAfterStringCompletionsInNestedCall2.ts(41,5): error TS2322: Type '{ ({ event }: { event: { type: "FOO"; }; }): void; _out_TEvent?: { type: "BARx"; } | undefined; }' is not assignable to type 'ActionFunction<{ type: "FOO"; }, { type: "FOO"; } | { type: "BAR"; }>'.
Types of property '_out_TEvent' are incompatible.
Type '{ type: "BARx"; } | undefined' is not assignable to type '{ type: "FOO"; } | { type: "BAR"; } | undefined'.
Type '{ type: "BARx"; }' is not assignable to type '{ type: "FOO"; } | { type: "BAR"; } | undefined'.
Type '{ type: "BARx"; }' is not assignable to type '{ type: "FOO"; } | { type: "BAR"; }'.
Type '{ type: "BARx"; }' is not assignable to type '{ type: "BAR"; }'.
Types of property 'type' are incompatible.
Type '"BARx"' is not assignable to type '"BAR"'.


==== /tests/cases/fourslash/typeErrorAfterStringCompletionsInNestedCall2.ts (1 errors) ====

type ActionFunction<
TExpressionEvent extends { type: string },
out TEvent extends { type: string }
> = {
({ event }: { event: TExpressionEvent }): void;
_out_TEvent?: TEvent;
};

interface MachineConfig<TEvent extends { type: string }> {
types: {
events: TEvent;
};
on: {
[K in TEvent["type"]]?: ActionFunction<
Extract<TEvent, { type: K }>,
TEvent
>;
};
}

declare function raise<
TExpressionEvent extends { type: string },
TEvent extends { type: string }
>(
resolve: ({ event }: { event: TExpressionEvent }) => TEvent
): {
({ event }: { event: TExpressionEvent }): void;
_out_TEvent?: TEvent;
};

declare function createMachine<TEvent extends { type: string }>(
config: MachineConfig<TEvent>
): void;

createMachine({
types: {
events: {} as { type: "FOO" } | { type: "BAR" },
},
on: {
FOO: raise(({ event }) => {
~~~
!!! error TS2322: Type '{ ({ event }: { event: { type: "FOO"; }; }): void; _out_TEvent?: { type: "BARx"; } | undefined; }' is not assignable to type 'ActionFunction<{ type: "FOO"; }, { type: "FOO"; } | { type: "BAR"; }>'.
!!! error TS2322: Types of property '_out_TEvent' are incompatible.
!!! error TS2322: Type '{ type: "BARx"; } | undefined' is not assignable to type '{ type: "FOO"; } | { type: "BAR"; } | undefined'.
!!! error TS2322: Type '{ type: "BARx"; }' is not assignable to type '{ type: "FOO"; } | { type: "BAR"; } | undefined'.
!!! error TS2322: Type '{ type: "BARx"; }' is not assignable to type '{ type: "FOO"; } | { type: "BAR"; }'.
!!! error TS2322: Type '{ type: "BARx"; }' is not assignable to type '{ type: "BAR"; }'.
!!! error TS2322: Types of property 'type' are incompatible.
!!! error TS2322: Type '"BARx"' is not assignable to type '"BAR"'.
!!! related TS6500 /tests/cases/fourslash/typeErrorAfterStringCompletionsInNestedCall2.ts:14:7: The expected type comes from property 'FOO' which is declared here on type '{ FOO?: ActionFunction<{ type: "FOO"; }, { type: "FOO"; } | { type: "BAR"; }> | undefined; BAR?: ActionFunction<{ type: "BAR"; }, { type: "FOO"; } | { type: "BAR"; }> | undefined; }'
return {
type: "BAR" as const,
};
}),
},
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
///<reference path="fourslash.ts"/>
// @strict: true
////
//// type ActionFunction<
//// TExpressionEvent extends { type: string },
//// out TEvent extends { type: string }
//// > = {
//// ({ event }: { event: TExpressionEvent }): void;
//// _out_TEvent?: TEvent;
//// };
////
//// interface MachineConfig<TEvent extends { type: string }> {
//// types: {
//// events: TEvent;
//// };
//// on: {
//// [K in TEvent["type"]]?: ActionFunction<
//// Extract<TEvent, { type: K }>,
//// TEvent
//// >;
//// };
//// }
////
//// declare function raise<
//// TExpressionEvent extends { type: string },
//// TEvent extends { type: string }
//// >(
//// resolve: ({ event }: { event: TExpressionEvent }) => TEvent
//// ): {
//// ({ event }: { event: TExpressionEvent }): void;
//// _out_TEvent?: TEvent;
//// };
////
//// declare function createMachine<TEvent extends { type: string }>(
//// config: MachineConfig<TEvent>
//// ): void;
////
//// createMachine({
//// types: {
//// events: {} as { type: "FOO" } | { type: "BAR" },
//// },
//// on: {
//// [|/*error*/FOO|]: raise(({ event }) => {
//// return {
//// type: "BAR/*1*/" as const,
//// };
//// }),
//// },
//// });

goTo.marker("1");
edit.insert(`x`)
verify.completions({ exact: ["FOO", "BAR"] });
verify.baselineSyntacticAndSemanticDiagnostics()