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

[WIP] Using overload for JSX stateless function component #11871

Closed
wants to merge 73 commits into from

Conversation

yuit
Copy link
Contributor

@yuit yuit commented Oct 26, 2016

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

  • Treat JSXAttributes similar to ObjectLiteralExpression. We change the AST structure and store JSXAttributes as a AST Node instead of NodeArray
  • Using overload resolution logic in choosing signature of stateless function component
  • This change rely on @sandersn spread operator to resolve the attributs

Todo

  • - Handle of spreading with any

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:

declare function MainBuntton(attr1: {to: string}): JSX.Element;
declare function MainBuntton(attr2: {onClick: ()=> void}): JSX.Element;
declare var obj3: any;
const b = <MainBuntton {...obj3} to={10} />;
  • Add tests for quick-info, goto-definition, completion to make sure that we reflect the choosen overload
  • Add tests when using stateless with type arguments.

@RyanCavanaugh @mhegazy

Kanchalai Tanglertsampan added 30 commits October 14, 2016 16:13
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
Add tests for stateless-function overload

Update tests
Andy and others added 17 commits November 3, 2016 11:07
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
* add test for the fix for overwrite emitting error

* cr feedback
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
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.

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 {
Copy link
Member

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; ?

Copy link
Contributor Author

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

Copy link
Member

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
Copy link
Member

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.

Copy link
Member

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.

Copy link
Contributor Author

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

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.

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
Copy link
Member

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
Copy link
Member

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))) {
Copy link
Member

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.

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 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;
Copy link
Member

Choose a reason for hiding this comment

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

  1. Why not use prop.valueDeclaration as the errorNode for JSX too?
  2. If you can do (1), why not just push isJsxAttributes down into the first argument of reportError?

Copy link
Contributor Author

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) {
Copy link
Member

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.

Copy link
Member

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;
}

Copy link
Member

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.

Copy link
Member

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) ...

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.

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)
Copy link
Member

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) {
Copy link
Member

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) {
Copy link
Member

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) {
Copy link
Member

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) ...

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.

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 {
Copy link
Member

Choose a reason for hiding this comment

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

  1. Why not return a SymbolTable? You create one already: attributesTable, and both callers of this function immediately create another one.
  2. The name is ungainly. What about getJsxAttributeSymbolsFromJsxElement (or JsxOpeningLikeElement`). That's a little better anyway :)

Copy link
Member

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.

Copy link
Member

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.

Copy link
Contributor Author

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
Copy link
Member

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;
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 just use a ternary operator for this since the predicate is short.

attributesTable = createMap<Symbol>();
}
const exprType = checkExpression(attributeDecl.expression);
const widenExprType = getWidenedType(exprType);
Copy link
Member

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 ?

Copy link
Member

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?

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 reviewing this, I agree. I shouldn't widen here since if it is "undefined" then we shouldn't allow it to be spread.

Copy link
Member

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>) {
Copy link
Member

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;
Copy link
Member

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
Copy link
Member

Choose a reason for hiding this comment

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

  1. typo:opening
  2. 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 {
Copy link
Member

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
Copy link
Member

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 {
Copy link
Member

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!

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.

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
Copy link
Member

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
Copy link
Member

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) {
Copy link
Member

Choose a reason for hiding this comment

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

  1. Consider pulling this code into a separate function, maybe named getAttributesTypeOfMatchingSignatures.
  2. 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.

Copy link
Member

Choose a reason for hiding this comment

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

  1. You can also pull the body of tryGetDefault out as something like getAttributesTypeOfSingleSignature and call that from the loop of getAttributesTypeOfMatchingSignatures.
  2. 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;
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 rename these variables to allMatchingAttributesType and allAttributesType.

return links.resolvedJsxElementAttributesType;
}

function getAllAttributesTypeFromJsxOpeningLikeElement(node: JsxOpeningLikeElement): Type {
Copy link
Member

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) {
Copy link
Member

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.
Copy link
Member

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)) {
Copy link
Member

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 {
Copy link
Member

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

@mhegazy
Copy link
Contributor

mhegazy commented Dec 30, 2016

@yuit should this be closed in favour of #12107?

@yuit yuit closed this Jan 26, 2017
@yuit yuit deleted the wip-statelessOverload branch May 9, 2017 16:04
@microsoft microsoft locked and limited conversation to collaborators Jun 19, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants