Skip to content
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

fix(38388) Add support for this parameters in iterationTypes and fix caching issues #41451

Closed
wants to merge 12 commits into from

Conversation

Validark
Copy link

@Validark Validark commented Nov 9, 2020

Fixes #38388
Fixes #39693

This PR:

  • Adds support for this parameters in iterationTypes!
const obj = {
	x: 1,
	y: 2,
	z: 3,

	*[Symbol.iterator]<P>(this: P) {
		for (const k in this) yield k;
	},
};

for (const value of obj) {
    console.log(value); // value is `"x" | "y" | "z"`
}
  • Fixes the iterator caching logic. Looking at the code, I noticed a number of problems that could arise from the caching logic. Here is a reproduction of two of the issues I found. Basically, getIterationTypesOfIterable had 3 problems:
    • Cached iterationTypes don't error like they should (see the first bug in the playground).
      • To fix this, we don't cache iterationTypes that produced errors, which makes getIterationTypesOfIterableSlow get called every time and produce the proper errors.
      • We can also take advantage of this design by detecting in getIterationTypesOfIterable whether something was cached to determine whether or not to cache or reportTypeNotIterableError (which we don't want to do if there was already an error from resolveCall, for example).
      • Keep in mind that caches should only be read from or written to when implicitThisType === type.
    • When use permits either AsyncIterables or (Sync)Iterables converted via getAsyncFromSyncIterationTypes it would call these functions in this order: getIterationTypesOfIterableCached(type, asyncResolver) || getIterationTypesOfIterableFast(type, asyncResolver) || getIterationTypesOfIterableCached(type, syncResolver) || getIterationTypesOfIterableFast(type, syncResolver) || getIterationTypesOfIterableSlow(type, asyncResolver) || getIterationTypesOfIterableSlow(type, syncResolver). This is incorrect because an object could have an Symbol.asyncIterator that is neither cached nor an instance of a simple global type, and that logic could then fall back on the syncResolver section. Now it runs cached || fast || slow in order for both Symbol.asyncIterator and Symbol.iterator when applicable. This led to the 2nd and 3rd bugs demonstrated in the playground link above.
    • iterationTypesOfAsyncIterable was shared between Symbol.iterator and Symbol.asyncIterator. Now iterationTypesOfAsyncedSyncIterable is used to store Symbol.iterator calls that got converted via getAsyncFromSyncIterationTypes.

To make implicit Symbol.iterator calls like actual Symbol.iterator calls, I hooked them up to the resolveCall function. This function and a few of the ones it calls had to be modified so that arbitrary Nodes could be passed into it without analyzing the Node as whatever it is. The implicit call case is differentiated by the implicitThisType being passed around. I.e. when implicitThisType is present, then the node should be treated solely as an errorNode, and the only NodeKind-specific behavior should be in getting the boundaries for error messages. typeArguments and parametric args should always be empty.

Implicit iterator calls do have different behavior from regular calls though. Intersected iterator functions will produce intersected results instead of just grabbing the first one. See #39693. Unioned iterator functions will evaluate each unioned constituent separately (passing the constituent, not the top-level type, as the implicitThisType) and the results will be unioned together. See here for examples. This means there will be inconsistency between how for (const x of obj) vs for (const x of obj[Symbol.iterator]()) is evaluated, although there was already inconsistency before and I think it is for the better.

@ghost
Copy link

ghost commented Nov 9, 2020

CLA assistant check
All CLA requirements met.

@typescript-bot typescript-bot added the For Milestone Bug PRs that fix a bug with a specific milestone label Nov 9, 2020
@typescript-bot
Copy link
Collaborator

The TypeScript team hasn't accepted the linked issue #38388. If you can get it accepted, this PR will have a better chance of being reviewed.

@Validark
Copy link
Author

Validark commented Nov 9, 2020

I noticed on my machine that the errors in errorsInGenericTypeReference changed from Could not find symbol V to Cannot find name 'V'. Is that actually the product of one of my changes...? I don't think it is, but I could update the files if wanted. (that's all that's causing the checks to fail)

@@ -1,5 +1,4 @@
tests/cases/compiler/errorsInGenericTypeReference.ts(11,17): error TS2304: Cannot find name 'V'.
tests/cases/compiler/errorsInGenericTypeReference.ts(17,31): error TS2304: Cannot find name 'V'.
Copy link
Author

Choose a reason for hiding this comment

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

This is probably not good. I am still pretty sure this has nothing to do with my changes, I just wanted to update the baseline references so my tests don't fail. Perhaps another issue should be opened for this case?

@sandersn sandersn requested a review from rbuckton November 23, 2020 23:27
Copy link
Member

@rbuckton rbuckton left a comment

Choose a reason for hiding this comment

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

There's a lot going on here and it will take me some time to review. I'm not certain I agree with some of the changes here, as there are a large number of places where you've switched to use a less precise type so that you can treat node as only an "error node". A separate errorNode argument argument would likely be preferable in those cases.

It might be worth breaking out the changes to fix the caching issues you mentioned into a separate PR, as those changes seem less controversial to me.

@@ -26267,33 +26268,33 @@ namespace ts {
return true;
}

function callLikeExpressionMayHaveTypeArguments(node: CallLikeExpression): node is CallExpression | NewExpression | TaggedTemplateExpression | JsxOpeningElement {
function callLikeExpressionMayHaveTypeArguments(node: Node): node is CallExpression | NewExpression | TaggedTemplateExpression | JsxOpeningElement {
Copy link
Member

Choose a reason for hiding this comment

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

Why was the precision for this argument's type reduced?

// This gets us diagnostics for the type arguments and marks them as referenced.
forEach(node.typeArguments, checkSourceElement);
}
function resolveUntypedCall(node: Node): Signature {
Copy link
Member

Choose a reason for hiding this comment

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

Why was the precision for this argument's type reduced?

checkExpression(argument);
});
}
return anySignature;
}

function resolveErrorCall(node: CallLikeExpression): Signature {
resolveUntypedCall(node);
function resolveErrorCall(node?: Node): Signature {
Copy link
Member

Choose a reason for hiding this comment

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

I'm still not clear why you're using a less specific type here.

@@ -26367,58 +26368,68 @@ namespace ts {
return !!(t.flags & (TypeFlags.Void | TypeFlags.Undefined | TypeFlags.Unknown | TypeFlags.Any));
}

function hasCorrectArity(node: CallLikeExpression, args: readonly Expression[], signature: Signature, signatureHelpTrailingComma = false) {
function hasCorrectArity(node: Node | undefined, args: readonly Expression[], signature: Signature, signatureHelpTrailingComma: boolean, implicitThisType?: Type) {
Copy link
Member

Choose a reason for hiding this comment

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

Again, I don't agree with using a less precise type here.

const returnSourceType = instantiateType(contextualType, outerContext && outerContext.returnMapper);
inferTypes(returnContext.inferences, returnSourceType, inferenceTargetType);
context.returnMapper = some(returnContext.inferences, hasInferenceCandidates) ? getMapperFromContext(cloneInferredPartOfContext(returnContext)) : undefined;
function inferTypeArguments(node: Node | undefined, signature: Signature, args: readonly Expression[], checkMode: CheckMode, context: InferenceContext, implicitThisType?: Type): Type[] {
Copy link
Member

Choose a reason for hiding this comment

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

Another case of using a less precise type, and I'm still not clear on why implicitThisType should cause most of this function to be ignored.

@Validark
Copy link
Author

Validark commented Jan 5, 2021

There's a lot going on here and it will take me some time to review. I'm not certain I agree with some of the changes here, as there are a large number of places where you've switched to use a less precise type so that you can treat node as only an "error node". A separate errorNode argument argument would likely be preferable in those cases.

It might be worth breaking out the changes to fix the caching issues you mentioned into a separate PR, as those changes seem less controversial to me.

Thanks for getting back to me! I really wasn't sure whether to prioritize making fewer changes and making it work with what's already there or to make changes like you suggested. I tried to think of how to solve the problem while making the smallest number of changes, then take one step towards what I would consider ideal. I wasn't sure what kind of philosophy there is about these kinds of things so I just guessed. Since my guess was wrong, I'll go back and make the changes I think would be ideal instead of trying to guess where my hands should be tied. I did kinda deviate from that anyway and got caught up a bit when I noticed a bunch of other issues while looking at the code. I will work on this more this week. I'll make this better.

(My first implementation was more along the lines of your suggestion but I thought it could be rejected for adding more than absolutely necessary)

@RyanCavanaugh
Copy link
Member

🧹 Closing due to staleness. Please open a new PR if you'd like to continue. Thanks!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
For Milestone Bug PRs that fix a bug with a specific milestone
Projects
Archived in project
4 participants