-
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
[WIP] Using overload for JSX stateless function component #11871
Conversation
Convert attributes property in JsxOpeningElement and JsxSelfClosingElement to use JsxAttributes type Convert jsxAttributes to be based on ObjectLiteralExpressionBase Fix how to get NodeEmitFlag in factory Update JsxSpreadAttribute
…LikeElement to clarify that it get the attributes type by resolving opening-like element's tagName
wip-get attributes from opening-like-element attributes Node Check jsx attributes for intrinsic element to base on object-literal logic Change the spreadElementType
…state-less component wip-resovle custom JSX attributes type wip-using overload resolution to figure out stateless functions wip-complete using resolve signature logic to try picking stateless function
Update comments Update comment
…Type Update baselines
…n objectLiteral-like
Add tests for stateless-function overload Update tests
…TypeScript into wip-statelessOverload
Lock tslint version to 4.0.0-dev.0, because 4.0.0-dev.1 complains about unnecessary semicolons following properties
Fix 'keyof' for constrained type parameters
Ensure transformFlags are correct before visiting a node.
Mark property referenced in the destructuring as referenced
# Conflicts: # src/compiler/checker.ts # src/compiler/types.ts # src/harness/harness.ts # src/services/completions.ts
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.
Some initial comments. I'm starting on checker now and will have more comments later.
export interface JsxAttribute extends Node { | ||
kind: SyntaxKind.JsxAttribute; | ||
// @kind(SyntaxKind.JsxAttribute) | ||
export interface JsxAttribute extends ObjectLiteralElement { |
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.
doesn't JsxAttribute still need a kind: SyntaxKind.JsxAttribute;
?
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.
you are right. will fix that
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.
also don't need the // @kind(...)
any more
@@ -2704,6 +2710,8 @@ namespace ts { | |||
ContainsObjectLiteral = 1 << 22, // Type is or contains object literal type | |||
/* @internal */ | |||
ContainsAnyFunctionType = 1 << 23, // Type is or contains object literal type | |||
/* @internal */ | |||
JsxAttributes = 1 << 24, // Jsx attributes type |
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.
My preliminary reaction is that JsxAttributes can't produces types that are very different from normal typescript types, so I expect that this flag is not really needed. But I still need to read the rest of the code.
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, it's because of the ObjectLiteralBase type. Hm.
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.
Yea the reason I need this flag is because in checking "hasExcessProperties" jsx attributes have different requirement
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 few more comments for now. I have quite a bit more of the checker to read still.
return true; | ||
} | ||
else if (getPropertyOfType(type, name) || (isComparingJsxAttributes && !isUnhyphenatedJsxName(name))) { | ||
// For JSXAttributes, if the attribute has hyphenated name considered the attribute to be known |
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.
rewrite: "if the attribute has a hyphenated name**, consider** the attribute to be known"
let sourceAttributesType = anyType as Type; | ||
let isSourceAttributesTypeEmpty = true; | ||
if (symbolArray) { | ||
// Filter out any hyphenated names as those are not play any role in type-checking unless there are corresponding properties in the target type |
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.
as these do not play
else if (resolved.stringIndexInfo || (resolved.numberIndexInfo && isNumericLiteralName(name))) { | ||
return true; | ||
} | ||
else if (getPropertyOfType(type, name) || (isComparingJsxAttributes && !isUnhyphenatedJsxName(name))) { |
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.
It's a little sad to see !isUnhyphenated
instead of isHyphenated
. But the other 3 uses are positive and they would get more confusing to read if they were !isHyphenated
. Still, it might be worth adding another function named isHyphenated
.
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 agree the double negative here is a bit confused. It is the only place that check for hyphenate so I am hesistant to just add a function for that
reportError(Diagnostics.Property_0_does_not_exist_on_type_1, symbolToString(prop), typeToString(target)); | ||
} | ||
else { | ||
errorNode = prop.valueDeclaration; |
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.
- Why not use
prop.valueDeclaration
as theerrorNode
for JSX too? - If you can do (1), why not just push
isJsxAttributes
down into the first argument ofreportError
?
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.
The reason we don't want to just use prop.valueDecalration
is that when we report an error we want it to highlight entire JSX-attributes not just one individual attribute.
// When we trying to resolve JsxOpeningLikeElement as a stateless function element, we will already give JSXAttributes a contextual type | ||
// which is a type of the parameter of the signature we are trying out. This is not the case if it is a statefull Jsx (i.e ReactComponenet class) | ||
// So if that is the case, just return the type of the JsxAttribute in such contextual type with out going into resolving of the JsxOpeningLikeElement again | ||
if ((<JsxAttributes>attribute.parent).contextualType) { |
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 is probably wrong. Any time I played games with contextual typing, it turned out that I was missing the root of the problem. I need to read the rest of the review to see whether I can figure out what the root actually is, though.
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.
At least, would it work to express this as
const attributesType = getContextualType(attribute.parent);
if (attributesType) {
return isJsxAttribute(attribute) ? getTypeOfPropertyOfType(attributesType, attribute.name.text) : 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.
Or maybe you need an additional predicate next to attributesType
to find out if attribute.parent is a stateful component or not.
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.
Actually, as we looked at it together, I think you can simplify it even more with something like
const attributesType = getContextualType(attribute.parent) || getAttributesTypeFromJsxOpeningLikeElement(attribute.parent.parent);
if (kind === SyntaxKind.JsxAttribute) ...
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.
More comments on the contextual typing problem
@@ -10497,17 +10545,24 @@ namespace ts { | |||
} | |||
|
|||
function getContextualTypeForJsxAttribute(attribute: JsxAttribute | JsxSpreadAttribute) { | |||
// When we trying to resolve JsxOpeningLikeElement as a stateless function element, we will already give JSXAttributes a contextual type | |||
// which is a type of the parameter of the signature we are trying out. This is not the case if it is a statefull Jsx (i.e ReactComponenet class) |
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.
typo:stateful
// When we trying to resolve JsxOpeningLikeElement as a stateless function element, we will already give JSXAttributes a contextual type | ||
// which is a type of the parameter of the signature we are trying out. This is not the case if it is a statefull Jsx (i.e ReactComponenet class) | ||
// So if that is the case, just return the type of the JsxAttribute in such contextual type with out going into resolving of the JsxOpeningLikeElement again | ||
if ((<JsxAttributes>attribute.parent).contextualType) { |
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.
At least, would it work to express this as
const attributesType = getContextualType(attribute.parent);
if (attributesType) {
return isJsxAttribute(attribute) ? getTypeOfPropertyOfType(attributesType, attribute.name.text) : undefined;
}
// When we trying to resolve JsxOpeningLikeElement as a stateless function element, we will already give JSXAttributes a contextual type | ||
// which is a type of the parameter of the signature we are trying out. This is not the case if it is a statefull Jsx (i.e ReactComponenet class) | ||
// So if that is the case, just return the type of the JsxAttribute in such contextual type with out going into resolving of the JsxOpeningLikeElement again | ||
if ((<JsxAttributes>attribute.parent).contextualType) { |
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.
Or maybe you need an additional predicate next to attributesType
to find out if attribute.parent is a stateful component or not.
// When we trying to resolve JsxOpeningLikeElement as a stateless function element, we will already give JSXAttributes a contextual type | ||
// which is a type of the parameter of the signature we are trying out. This is not the case if it is a statefull Jsx (i.e ReactComponenet class) | ||
// So if that is the case, just return the type of the JsxAttribute in such contextual type with out going into resolving of the JsxOpeningLikeElement again | ||
if ((<JsxAttributes>attribute.parent).contextualType) { |
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.
Actually, as we looked at it together, I think you can simplify it even more with something like
const attributesType = getContextualType(attribute.parent) || getAttributesTypeFromJsxOpeningLikeElement(attribute.parent.parent);
if (kind === SyntaxKind.JsxAttribute) ...
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.
More comments, although I am still reviewing through the resolve stateless function stuff.
* @param openingLikeElement a Jsx opening-like element | ||
* @return a symbol table resulted from resolving "attributes" property or undefined if any of the attribute resolved to any or there is no attributes. | ||
*/ | ||
function getJsxAttributesSymbolArrayFromAttributesProperty(openingLikeElement: JsxOpeningLikeElement): Symbol[] | 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.
- Why not return a SymbolTable? You create one already:
attributesTable
, and both callers of this function immediately create another one. - The name is ungainly. What about
getJsxAttributeSymbolsFromJsxElement
(or JsxOpeningLikeElement`). That's a little better anyway :)
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.
And if you do (1) then you'll not need attributesArray
any more, just a boolean to track whether attributesTable
is empty or not.
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.
In fact, it looks like both callers immediately call createJsxAttributesType
. I would do that as the last action of this function instead and call this getJsxAttributesTypeFromJsxElement
.
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.
Talk offline, I will try to refactor the code here
function getJsxAttributesSymbolArrayFromAttributesProperty(openingLikeElement: JsxOpeningLikeElement): Symbol[] | undefined { | ||
const attributes = openingLikeElement.attributes; | ||
let attributesTable = createMap<Symbol>(); | ||
let spread: Type = emptyObjectType |
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.
missing semi-colon. (I can't believe I caught that, I do this all the time myself)
for (const attributeDecl of attributes.properties) { | ||
const member = attributeDecl.symbol; | ||
if (isJsxAttribute(attributeDecl)) { | ||
let exprType: Type; |
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 would just use a ternary operator for this since the predicate is short.
attributesTable = createMap<Symbol>(); | ||
} | ||
const exprType = checkExpression(attributeDecl.expression); | ||
const widenExprType = getWidenedType(exprType); |
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.
probably better spelt as widenedExprType
?
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.
why do you have to widen the type here?
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.
After reviewing this, I agree. I shouldn't widen here since if it is "undefined" then we shouldn't allow it to be spread.
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.
ES standard spread allows { ...undefined }
but defines it as equal to {}
. Same for null. Should JSX differ here?
* @param jsxAttributesSymb a JsxAttributes node containing attributes in attributesTable | ||
* @param attributesTable a symbol table of attributes property | ||
*/ | ||
function createJsxAttributesType(jsxAttributesSymb: Symbol, attributesTable: Map<Symbol>) { |
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.
can you un-abbreviate to either symbol
or jsxAttributesSymbol
? I prefer symbol
because this is a short function. Also attributes
would be a fine name for the second parameter.
if (symbolArray) { | ||
const symbolTable = createMap<Symbol>(); | ||
forEach(symbolArray, (attr) => { | ||
symbolTable[attr.name] = attr; |
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 about hyphenated names? The other caller handles hyphenated names differently.
* us which attributes are valid on a given element. | ||
* Get attributes type of the given intrinsic opening-like Jsx element by resolving the tag name. | ||
* The function is intended to be called from a function which has checked that the opening element is an intrinsic element. | ||
* @param node an intrinsic JSX oopening-like element |
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.
- typo:opening
- can you add an assert to verify that it's an intrinsic attribute, like
Debug.assert(symbol)
after calling getIntrinsicTagSymbol or something?
* The function is intended to be called from a function which has handle intrinsic Jsx element already. | ||
* @param node a custom Jsx opening-like element | ||
*/ | ||
function getCustomJsxElementAttributesType(node: JsxOpeningLikeElement, shouldIncludeAllStatelessAttributesType = false): Type { |
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.
since all callers are new, can you just make the boolean required? It's a little more straightforward to read
}), /*subtypeReduction*/ true); | ||
} | ||
|
||
// If the elemType is a string type, we have to return anyType to prevent an error downstream as we will try to find construct or call signature of the type | ||
if (elemType.flags & TypeFlags.String) { | ||
// If the elemType is a string type, we have to return anyType to prevent an error downstream as we will try to find construct or call signature of the type |
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.
Duped line
* @return a resolved signature if we can find function matching function signature through resolve call or a first signature in the list of functions. | ||
* otherwise return undefined if tag-name of the opening-like element doesn't have call signatures | ||
*/ | ||
function resolvedStateLessJsxOpeningLikeElement(openingLikeElement: JsxOpeningLikeElement, elementType: Type, candidatesOutArray: Signature[]): Signature { |
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 no! The MicroSoft disease has turned Stateless into StateLess!
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 are my comments for now. I'm switching to the cleaned-up PR so I can use codeflow.
/** | ||
* Resolve attributes type of the given node. The function is intended to initially be called from getAttributesTypeFromJsxOpeningLikeElement which already handle JSX-intrinsic-element. | ||
* @param openingLikeElement a non-intrinsic JSXOPeningLikeElement | ||
* @param elementType an instance type of the given node |
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.
Add a param tag for shouldIncludeAllStatelessAttributesType or just remove the param tags.
return paramType; | ||
} | ||
} | ||
// Is this is a stateless function component? See if its single signature's return type is assignable to the JSX Element Type |
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.
update comment
getResolvedJSXStatelessFunctionSignature(openingLikeElement, elementType, candidatesOutArray); | ||
let result: Type; | ||
let defaultResult: Type; | ||
for (const candidate of candidatesOutArray) { |
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.
- Consider pulling this code into a separate function, maybe named
getAttributesTypeOfMatchingSignatures
. - Then maybe you can combine these two functions back into one that takes a boolean to decide whether to return attributes for all candidates or just one.
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.
- You can also pull the body of tryGetDefault out as something like
getAttributesTypeOfSingleSignature
and call that from the loop ofgetAttributesTypeOfMatchingSignatures
. - Then, I think, you can do something like
getAttributesTypeOfMatchingSignatures(shouldIncludeAllStatelessAttributesType ? candidatesOutArray ? [callSignature])
const candidatesOutArray: Signature[] = []; | ||
getResolvedJSXStatelessFunctionSignature(openingLikeElement, elementType, candidatesOutArray); | ||
let result: Type; | ||
let defaultResult: Type; |
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 would rename these variables to allMatchingAttributesType
and allAttributesType
.
return links.resolvedJsxElementAttributesType; | ||
} | ||
|
||
function getAllAttributesTypeFromJsxOpeningLikeElement(node: JsxOpeningLikeElement): Type { |
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.
can you document that this is meant to be called from the services layer? (if it is — I think that is what you told me.)
* Check assignablity between given attributes property, "attributes" and the target attributes resulted from resolving tag-name | ||
* @param openingLikeElement an opening-like JSX element to check its JSXAttributes | ||
*/ | ||
function checkJSXAttributesAssignableToTagnameAttributes(openingLikeElement: JsxOpeningLikeElement) { |
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.
seems like other names use Jsx
instead of JSX
.
if (isJsxOpeningLikeElement(node)) { | ||
// For JSX opening-like element, we will ignore regular arity check (which is what is done here). | ||
// Instead, the arity check will be done in "checkApplicableSignatureForJsxOpeningLikeElement" as we are required to figure out | ||
// all property inside give attributes. |
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.
rewrite: "inside the given attributes"
if (isJsxOpeningOrSelfClosingElement && !checkApplicableSignatureForJsxOpeningLikeElement(<JsxOpeningLikeElement>node, candidate, relation)) { | ||
break; | ||
} | ||
else if (!isJsxOpeningOrSelfClosingElement && !checkApplicableSignature(node, args, candidate, relation, excludeArgument, /*reportErrors*/ false)) { |
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.
Normally I like this style, but I think this is actually easier to read as
if (isJsx...) {
if (!checkApplicable...) {
break;
}
}
else if (!checkApplicable...) {
break;
}
* @param elementType | ||
* @param candidatesOutArray | ||
*/ | ||
function getResolvedJSXStatelessFunctionSignature(openingLikeElement: JsxOpeningLikeElement, elementType: Type, candidatesOutArray: Signature[]): Signature { |
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.
Should use Jsx
like other names
This PR is still WIP. Some clean up will be needed. It is branch off from @sandersn PR on spread operator.
Fix: #11187 (see this test: tsxSpreadAttributesResolution1), #9703
Overview of the change
JSXAttributes
similar toObjectLiteralExpression
. We change the AST structure and store JSXAttributes as a AST Node instead of NodeArrayTodo
Currently when we see spread of any type, the entire attributes has any type which means that follow example is allowed even though we should give an error:
@RyanCavanaugh @mhegazy