Skip to content

Unify JSX And Normal Call Checking Codepaths #27627

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
merged 7 commits into from
Oct 17, 2018

Conversation

weswigham
Copy link
Member

@weswigham weswigham commented Oct 9, 2018

Fixes #27425
Fixes #27385

So. A long time ago, there existed only JSX class elements. Those elements were typechecked with special rules and behaved only slightly like calls - they were handled like any other expression with their own type checking code and everything. Then came stateless function components - these components behaved a lot more like function calls and merited even more special code to typecheck. Then the requests for correct behavior with respect to overloads, generic defaults, and type inference came, along with type arguments for both of the above, and by every metric, both class and stateless components acted very much like calls; but still, they were not checked as function calls (and indeed, they were not even treated similarly to one another).

Now, they are. This greatly simplifies how we check JSX. No longer does JSX have its own unique call; resolution, overload resolution, excess property checking, and language service codepaths. Now all of the normal variants of those (getResolvedSignature, chooseOverload, isRelatedTo, and getContextualType) all handle a JSX tag (or JSX-style type/signature) as one might expect (that the tag as a whole is like a function call, while the attributes are like an object literal).

This fixes a few outstanding bugs (listed above) by unifying the class and sfc checking codepaths with the more general signature checking codepaths, however this was not without sacrifice (though warranted). SFCs had this unique ability that, given a tag of type (props: A) => SFCComponent1 | (props: B) => SFCComponent2, we would check the passed attributes against every signature, rather than issue an error on the dissimilar signatures (as we do for other kinds of calls). The functionality was removed, and I believe rightfully so. In the context of contextually typed parameters, this had great potential for breakage, as contextual parameter types are fixed to the nodes once inference is completed. rechecking the same "signature" under multiple types would undoubtedly fail to append any types beyond those from the first signature (see checkFunctionExpressionOrObjectLiteralMethod and how it stores a checked flag and fixes types). This functionality should be created via some means like a general solution to #7294 instead, rather than the ad-hoc union-breakdown we were doing before.

Generally, this should be backwards compatible in almost every way; but because I've swapped from ad-hoc-psuedo-overload-resolution to bonafide overload resolution, it's possible that some overload signatures may need to be reordered or modified to continue to function in the intended fashion (I doubt this affects many beyond our test suite, since anyone writing signatures affected by this would be confused as to why writing their SFC as a function call behaved differently than it did as a tag). Previously JSX both skipped the subtype pass and had a strange arity-check-esque rule that wholly overlapped with excess property checking.

Additionally, this process uncovered a small bug in how weak types were checked that impacted some JSX tests once their checking was done correctly which required restructuring how the flag which skips the weak type check was passed down through isRelatedTo.

On the minor change list:

  • This is technically a breaking API change because I've removed getAllAttributesTypeFromJsxOpeningLikeElement from the API, and it was publicly exposed (why?). Anyone using it should just be using getContextualType instead, and, if the exact same behavior is needed, if they witness a union return type, convert it into an intersection instead (or, like our language service, handle unions intelligently).
    • The changes associated with this break should also allow prop types' completions to be discriminated, so this should improve the completion experience for JSX, too.
  • SFC signature validity checking, by virtue of now using the normal call resolution paths, now uses checkTypeRelatedToAndOptionallyElaborate, meaning it can now have deep elaborations appear on SFC argument errors. Previously this'd only happen for class components.
  • SFC target types once again include IntrinsicAttributes in error messages. They were removed by a small change a few weeks ago that moved where the SFC error was reported within the SFC checking codepath. If we want them gone again, we should implement the more general "skip intersections of only weak types when elaborating" kinda rule we were thinking about before.

@weswigham
Copy link
Member Author

Fun fact: Excluding test changes and new tests, this PR now sits at 227 lines added and 569 lines removed. 😄

@weswigham
Copy link
Member Author

@typescript-bot test this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Oct 9, 2018

Heya @weswigham, I've started to run the extended test suite on this PR at c51c496. You can monitor the build here. It should now contribute to this PR's status checks.

@sandersn
Copy link
Member

sandersn commented Oct 9, 2018

Once you fix the JSX excess property checking, it might help #27355 too? I'll try it out.

@weswigham
Copy link
Member Author

Nah, #27355 is less an excess property error and more "Property 'b' will always be overridden by the following spread.", IMO. The spread type entirely comes from the declared parameter type in that issue, so an excess property error doesn't make too much sense. In any case, I've fixed excess property errors on spreads (namely: they no longer trigger them, as the intent in #19775 indicates).

@weswigham
Copy link
Member Author

@typescript-bot test this - the baseline changes show now just be additional error lines (from the intersection returning) or moved errors from improved elaboration.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Oct 9, 2018

Heya @weswigham, I've started to run the extended test suite on this PR at 1ebbaa1. You can monitor the build here. It should now contribute to this PR's status checks.

@weswigham
Copy link
Member Author

weswigham commented Oct 9, 2018

The RWC changes look known now. There's a few places where the extra line with the intersection of intrinsics was added back, one case where the error was moved to the containing tag name, and one case where someone was using the jsx-can-resolve-unions-of-signatures-unintentionally feature (which, as I stated, is now gone as it's very much not safe to do that way it was being done).

@sandersn sandersn self-assigned this Oct 9, 2018
function isTypeInvalidDueToUnionDiscriminant(contextualType: Type, obj: ObjectLiteralExpression): boolean {
return obj.properties.some(property => {
function isTypeInvalidDueToUnionDiscriminant(contextualType: Type, obj: ObjectLiteralExpression | JsxAttributes): boolean {
const list = obj.properties as NodeArray<ObjectLiteralElementLike | JsxAttributeLike>;
Copy link
Member

Choose a reason for hiding this comment

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

can ObjectLiteralElementLike be expanded to include JsxAttributeLike, or could we introduce yet another union to unify the two?

Copy link
Member Author

Choose a reason for hiding this comment

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

I only need to perform this cast because we can't resolve signatures of unions, so operating on a union of two arrays is impossible.

@@ -11547,15 +11540,16 @@ namespace ts {
return hasExcessProperties(source, discriminant, /*discriminant*/ undefined, reportErrors);
}
for (const prop of getPropertiesOfObjectType(source)) {
if (!isPropertyFromSpread(prop, source.symbol) && !isKnownProperty(target, prop.escapedName, isComparingJsxAttributes)) {
if (shouldCheckAsExcessProp(prop, source.symbol) && !isKnownProperty(target, prop.escapedName, isComparingJsxAttributes)) {
Copy link
Member

Choose a reason for hiding this comment

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

I would name this Property

@@ -11433,8 +11429,7 @@ namespace ts {

let result = Ternary.False;
const saveErrorInfo = errorInfo;
const saveIsIntersectionConstituent = isIntersectionConstituent;
isIntersectionConstituent = false;
let isIntersectionConstituent = !!isApparentIntersectionConstituent;
Copy link
Member

Choose a reason for hiding this comment

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

I think the new distinction between isIntersectionConstituent and isApparentIntersectionConstituent is that isIntersection is not enclosed in checkTypeRelated, so it only propagates through passing to recursive calls. And isApparentIntersectionConstituent is enclosed in checkTypeRelated, but doesn't pass recursively.

Is that accurate? The distinction is subtle and warrants some explanation and/or renaming of variables because it'll be hard to reconstruct the design by reading the code later.

Copy link
Member Author

Choose a reason for hiding this comment

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

isIntersectionConstituent "propagates" through all calls, but is reset on every visit of isRelatedTo, while isApparentIntersectionConstituent is an argument used to prevent that reset

declaration,
/*typeParameters*/ undefined,
/*thisParameter*/ undefined,
[parameterSymbol],
Copy link
Member

Choose a reason for hiding this comment

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

nitpick: This function has so many parameters, it would be easier for me to have a comment on every non-identifier argument. Well, at least 1 and typeSymbol ? ....

Copy link
Member Author

Choose a reason for hiding this comment

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

This function has so many parameters that it should just take a configuration object.

@@ -17075,7 +17029,7 @@ namespace ts {
const hostClassType = getReturnTypeOfSignature(sig);
apparentAttributesType = intersectTypes(
typeParams
? createTypeReference(<GenericType>intrinsicClassAttribs, fillMissingTypeArguments([hostClassType], typeParams, getMinTypeArgumentCount(typeParams), isJs))
? createTypeReference(<GenericType>intrinsicClassAttribs, fillMissingTypeArguments([hostClassType], typeParams, getMinTypeArgumentCount(typeParams), isInJSFile(context)))
Copy link
Member

Choose a reason for hiding this comment

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

this line is too long!

Copy link
Member Author

Choose a reason for hiding this comment

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

fillMissingTypeArguments call lines are always long.


if (compilerOptions.jsx === JsxEmit.React && (compilerOptions.jsxFactory || getSourceFileOfNode(node).pragmas.has("jsx"))) {
error(node, compilerOptions.jsxFactory
? Diagnostics.JSX_fragment_is_not_supported_when_using_jsxFactory
: Diagnostics.JSX_fragment_is_not_supported_when_using_an_inline_JSX_factory_pragma);
}

checkJsxChildren(node);
Copy link
Member

Choose a reason for hiding this comment

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

so, uhh, we weren't checking children before?

Copy link
Member Author

Choose a reason for hiding this comment

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

If the attributes type is non-any, they can get checked via their contributions to the attributes type; however we need to ensure they get visited even in the any/error cases, so we do that here.

const intrinsicProp = getPropertyOfType(intrinsicElementsType, escapeLeadingUnderscores(stringLiteralTypeName));
if (intrinsicProp) {
return getTypeOfSymbol(intrinsicProp);
const target = getTypeOfSymbol(intrinsicProp);
Copy link
Member

Choose a reason for hiding this comment

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

you can collapse this back to one line, I think.

@@ -19637,6 +19263,7 @@ namespace ts {
const isTaggedTemplate = node.kind === SyntaxKind.TaggedTemplateExpression;
const isDecorator = node.kind === SyntaxKind.Decorator;
const isJsxOpeningOrSelfClosingElement = isJsxOpeningLikeElement(node);
const reportErrors = !candidatesOutArray;
Copy link
Member

Choose a reason for hiding this comment

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

I know it's redundant, but it might be more readable to add a reportErrors parameter.

Copy link
Member Author

Choose a reason for hiding this comment

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

It already has an optional fallbackError parameter that I'd then need to start providing (as undefined) at almost every callsite.

// TODO: Since this builds some nodes for the fake'd up signature, this might be worth caching on the node
const namespace = getJsxNamespaceAt(node);
const exports = namespace && getExportsOfSymbol(namespace);
// TODO: We fake up a SFC signature for each intrinsic, however a more specific per-element signature drawn from the JSX declaration
Copy link
Member

Choose a reason for hiding this comment

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

I would decide what to do about these TODOs and remove them. (I suspect the best thing to do for the second one is just to note that the more-specific signature would be preferable.)

Copy link
Member Author

Choose a reason for hiding this comment

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

The TODOs are here to gather some remarks on what y'all think is appropriate. If I had a solid stance I'd have done it. See, I don't know if the nodes need to be cached, since the signature is already cached when this is called via resolveSignature and since that signature should be the only handle to those generated nodes, it might not be necessary.

@sandersn
Copy link
Member

Looks good so far. I need to look at the tests still.

!!! error TS2322: Type '{ children: Element[]; a: number; b: string; }' is not assignable to type 'IntrinsicAttributes & SingleChildProp'.
!!! error TS2322: Type '{ children: Element[]; a: number; b: string; }' is not assignable to type 'SingleChildProp'.
Copy link
Member

Choose a reason for hiding this comment

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

I think it was special-case code that got rid of this line in the first place. Is it possible that it just needs to be moved or updated to work again?

Copy link
Member Author

Choose a reason for hiding this comment

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

As I mentioned in person, this is a result of bringing SFC checking back into line and doing the right thing - there was no "special case" removing the jsx types from the errors; they just straight up weren't being checked against!

@@ -37,26 +41,30 @@ tests/cases/conformance/types/contextualTypes/jsxAttributes/file.tsx(36,44): err

const b0 = <MainButton {...{onClick: (k) => {console.log(k)}}} extra />; // k has type "left" | "right"
~~~~~~~~~~
!!! error TS2322: Type '{ extra: true; onClick: (k: "left" | "right") => void; }' is not assignable to type 'LinkProps'.
!!! error TS2322: Property 'goTo' is missing in type '{ extra: true; onClick: (k: "left" | "right") => void; }'.
!!! error TS2322: Type '{ extra: true; onClick: (k: "left" | "right") => void; }' is not assignable to type 'IntrinsicAttributes & LinkProps'.
Copy link
Member

Choose a reason for hiding this comment

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

same comment here, although I'm not as sure there actually was special-case code.

@@ -18,9 +16,6 @@ tests/cases/conformance/jsx/file.tsx(19,2): error TS2322: Type '{ x: number; ren
}
var Obj1: Obj1type;
<Obj1 x={10} />; // Error, no render member
Copy link
Member

Choose a reason for hiding this comment

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

should this still be an error? The comment says so.

Copy link
Member Author

Choose a reason for hiding this comment

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

The argument assignability error suppresses the construction error now. Specifically, since no signature matched the arguments provided, an error signature is returned with an anyish return type, which is assignable to ElementClass.

!!! error TS2322: Property 'w' does not exist on type '{ n: string; }'.
Copy link
Member

Choose a reason for hiding this comment

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

The directionality of this error changed, meaning that maybe source swapped with target. Is that correct?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ehhhhh, not quite. Previous JSX didn't use normal excess-property-checking, and so the normal assignability error would be reported. Excess property errors in the usual codepath are reported at a higher priority than typical assignability errors, while in the old JSX code, excess properties only got checked if the type was otherwise assignable.

const decorator4 = function <T extends { x: number }>(Component: React.StatelessComponent<T>): React.StatelessComponent<T> {
return (props) => <Component {...props} y={"blah"} ></Component>
~~~~~~~~~~
!!! error TS2339: Property 'y' does not exist on type 'IntrinsicAttributes & { x: number; } & { children?: ReactNode; }'.
Copy link
Member

Choose a reason for hiding this comment

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

shouldn't this still be an excess property error?

Copy link
Member Author

Choose a reason for hiding this comment

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

The source is T & { y: string }, so not by our normal excess property checking rules, no!

thirdTarget: "firstSource",
fourthTarget: "firstSource",
fifthTarget: "secondSource",
sixthTarget: "thirdSource"
sixthTarget: "firstSource"
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 probably super confused, but I thought normal overload resolution chose the last overload in error cases. Why does this switch to the first overload here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Nope - when overload resolution completely fails we say the declaration is the first signature - see createUnionOfSignaturesForOverloadFailure.

Copy link
Member

@sandersn sandersn left a comment

Choose a reason for hiding this comment

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

I some questions on test changes before signing off.

And after this PR, I'd like to see one that improves error reporting against intersections that contain weak types so that we can get back our nice-ish error messages.

@weswigham weswigham merged commit 7b5ef64 into microsoft:master Oct 17, 2018
@weswigham weswigham deleted the tsx-rewrite-number-five branch October 17, 2018 00:16
@weswigham weswigham added the Breaking Change Would introduce errors in existing code label Oct 17, 2018
@weswigham weswigham added this to the TypeScript 3.2 milestone Oct 17, 2018
@weswigham weswigham added the Domain: JSX/TSX Relates to the JSX parser and emitter label Oct 17, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Breaking Change Would introduce errors in existing code Domain: JSX/TSX Relates to the JSX parser and emitter
Projects
None yet
3 participants