-
Notifications
You must be signed in to change notification settings - Fork 12.7k
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] Clean up version of using overload for JSX stateless function component #12107
Conversation
Add language service support for JSXAttributes Add completion support Add find-all-references support Add goto-definition support
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.
Comments and questions on the checker. I'll look at the other files next.
const paramType = getTypeAtPosition(signature, 0); | ||
|
||
// JSX opening-like element has correct arity for stateless-function component if the one of the following condition is true: | ||
// 1. callIsInCompletes |
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:callIsIncomplete #Closed
// We can figure that out by resolving attributes property and check number of properties in the resolved type | ||
// If the call has correct arity, we will then check if the argument type and parameter type is assignable | ||
|
||
const callIsIncomplete = node.attributes.end === node.end; // If we are missing the close "/>", the call is incomplete |
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 you should move if (callIsIncomplete)
right below here to avoid the checkExpression call when possible. #Closed
let shouldCheckArgumentAndParameter = true; | ||
for (const arg of argProperties) { | ||
if (!getPropertyOfType(paramType, arg.name) && isUnhyphenatedJsxName(arg.name)) { | ||
shouldCheckArgumentAndParameter = 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.
can't you just return false here and get rid of the boolean flag? #Closed
break; | ||
} | ||
} | ||
if (shouldCheckArgumentAndParameter && checkTypeRelatedTo(argType, paramType, relation, /*errorNode*/ undefined, headMessage)) { |
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 requirement that the types have the same set of properties kind of looks like compareTypesIdentical (except for hyphenated properties). But each property is still compared for normal assignability. This is kind of confusing. Why not just use one-directional assignability that checkTypeRelatedTo gives you? #Pending
@@ -12604,6 +12861,14 @@ namespace ts { | |||
return result; | |||
} | |||
|
|||
// Do not report any error if we are doing so for stateless function component as such error will be error will be handle in "resolveCustomJsxElementAttributesType". |
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.
replace: "will be error will be handle" with "will be handled" #Closed
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 suspicious to see error handling paths duplicated like this. Can you treat jsx stateless function components close enough to a normal call that you can use the same error paths? So far I have seen three obstacles:
- hypenated properties are treated differently
- The rules for applicability are (perhaps?) different.
- Error reporting on JSX as if it were a function call might be confusing.
Can you think of ways to handle these on the normal code paths instead of putting them in a set of alternate functions? Are there other problems? (3) doesn't seem that hard to fix locally. #Closed
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 that JSX stateless function can't be treat as normal call because
- To be valid stateless function, we have to check that the return type match JSXElementType (see :
defaultTryGetJsxStatelessFunctionAttributesType
) I don't want to move that check here since it is such JSX specific. - When trying to resolve JSX tag-name that is not intrinsic, we do following : try as if it is stateless, if fails then try as stateful. #Resolved
* @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.
More MicroSoft disease: StateLess → Stateless #Closed
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 I think resolved
should be resolve
so that the first word is a verb #Closed
* @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.
JSX → Jsx #Closed
if (elementType.flags & TypeFlags.Union) { | ||
const types = (elementType as UnionType).types; | ||
let result: Signature; | ||
types.map(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.
should just be for (const type of types)
? #Closed
function getResolvedJSXStatelessFunctionSignature(openingLikeElement: JsxOpeningLikeElement, elementType: Type, candidatesOutArray: Signature[]): Signature { | ||
Debug.assert(!(elementType.flags & TypeFlags.Union)); | ||
const links = getNodeLinks(openingLikeElement); | ||
// If getResolvedSignature has already been called, we will have cached the resolvedSignature. |
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 code looks a lot like getResolvedSignature
. Are you sure you can't re-use getResolvedSignature
and move the core difference either down in to resolveStatelessJsxOpeningLikeElement
or up into tryGetJsxStatelessFuncitonAttributesType
? #Pending
let callSignature: Signature; | ||
callSignature = resolveCall(openingLikeElement, callSignatures, candidatesOutArray); | ||
return callSignature; | ||
} |
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 we need to fill up candidatesArray
? Is it for the language service? Shouldn't normal resolveCall
fill up candidatesArray
for a union? #Closed
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 function resolveCall will fill up candidatesArray
which is pass in from language service. I don't quite understand the second question XD - what union are you referring to?
In reply to: 87291084 [](ancestors = 87291084)
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.
Comments for rest of source. Looking at tests next.
@@ -988,6 +994,24 @@ namespace Harness { | |||
} | |||
} | |||
|
|||
if (libFiles) { |
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.
is this needed as part of this PR or can it be broken into a separate change? #Closed
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 kind of because otherwise test will be very slow since we will reparse lib file everytime
#Resolved
// attributes: JsxAttributes | ||
// properties: NodeArray<JsxAttributeLike> | ||
// each JsxAttribute can have initializer as JsxExpression | ||
return /*JsxExpression*/parent./*JsxAttribute*/parent./*JsxAttributes*/parent.parent as 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.
the inline comments are a neat idea, but I think they're still too distracting for their value. It's probably good enough to have the tree comment preceding the line of code #Closed
return <JsxOpeningLikeElement>parent.parent.parent; | ||
parent.parent && parent.parent.kind === SyntaxKind.JsxAttribute) { | ||
// Currently we parse JsxOpeninLikeElement as: | ||
// JsxOpeninLikeElement |
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.
JsxOpeningLikeElement
is missing a 'g', everywhere in the comments of this file. #Closed
return forEach(typeChecker.getRootSymbols(contextualSymbol), s => searchSymbols.indexOf(s) >= 0 ? s : undefined); | ||
}); | ||
|
||
if (contextualSymbol) { | ||
return contextualSymbol; | ||
} | ||
|
||
// If the reference location is the name of property from object literal destructuring pattern | ||
// If the reference location is the name of property from object literal destructing pattern |
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.
original spelling was correct #Closed
// /*firstSource*/declare function MainButton(buttonProps: ButtonProps): JSX.Element; | ||
// /*secondSource*/declare function MainButton(linkProps: LinkProps): JSX.Element; | ||
// /*thirdSource*/declare function MainButton(props: ButtonProps | LinkProps): JSX.Element; | ||
// let opt = <Main/*firstTarget*/Button />; // We get undefined for resolved signature indicating an error, then just return the first declaration |
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.
wouldn't it make more sense to return the last one since it's the most general? #Pending
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.
hmmm I was trying to mimic how normal call-expression does when there is an error. I swear I thought it goto the first declaration but I was wrong. I think then it should go to the last one
In reply to: 87302820 [](ancestors = 87302820)
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.
did you already change this to returning the last? I thought I saw the change but now I can't find it.
In reply to: 88111830 [](ancestors = 88111830,87302820)
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 change is done in checker resolveCall
by returning "candidateForArgumentError" instead of undefined. Though I should remove this.
In reply to: 88706239 [](ancestors = 88706239,88111830,87302820)
if (contextualType) { | ||
let result: DefinitionInfo[] = []; | ||
forEach(getPropertySymbolsFromContextualType(typeChecker, container), contextualSymbol => { | ||
result = result.concat(getDefinitionFromSymbol(typeChecker, contextualSymbol, 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.
how does this differ from for( ... of ... )
#Closed
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 doesn't, as far as I can tell, so I would use for-of.
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 this loop get every property of the contextual type, instead of just the one that matches /*1*/
? #Closed
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 am not sure I understand "just the one that matches but if i understand correctly, the function getPropertySymbolsFromContextualType
done the filtering already
In reply to: 87305445 [](ancestors = 87305445)
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 was wrong -- getPropertySymbolsForContextualType could return undefined if the property it finds has no name, which it might not for some computed properties. (Probably should have a test for this case too)
In reply to: 87303918 [](ancestors = 87303918)
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.
Yeah, the older version was better. Sorry for the bad suggestion.
In reply to: 88699276 [](ancestors = 88699276,87303918)
@@ -1967,6 +1967,76 @@ namespace ts { | |||
} | |||
} | |||
|
|||
function isObjectLiteralPropertyDeclaration(node: Node): node is 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.
- shouldn't it be ObjectLiteralElementLike? I didn't think JsxAttribute was part of the main ObjectLiteralElement union.
- Does this function exist elsewhere? I thought the rest of the compiler might need this too. But maybe it doesn't. #Closed
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 shouldn't be just objectLiteralElementLike because that only includes "PropertyAssignment | ShorthandPropertyAssignment | MethodDeclaration | AccessorDeclaration | SpreadAssignment;" It should just be ObjectLiteralElement as ObjectlIteralElement is the base interface for ObjectLiteralElementLike and JsxAttributesLike. We want to include Jsx here so that when we goto-definition or find all reference we actually look into contextual type flow from stateless function
In reply to: 87306417 [](ancestors = 87306417)
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.
@@ -295,7 +296,7 @@ namespace ts.SignatureHelp { | |||
|
|||
// findListItemInfo can return undefined if we are not in parent's argument list | |||
// or type argument list. This includes cases where the cursor is: | |||
// - To the right of the closing paren, non-substitution template, or template tail. | |||
// - To the right of the closing parenthesize, non-substitution template, or template tail. |
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.
parenthesis #Closed
// This is not guarantee that JSX tag-name is resolved into stateless function component. (that is done in "getSignatureHelpItems") | ||
// i.e | ||
// export function MainButton(props: ButtonProps, context: any): JSX.Element { ... } | ||
// <MainBuntton /*signatureHelp*/ |
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:MainButton #Closed
if (!signature && candidateSignatures.length) { | ||
// Use the first candidate: | ||
signature = candidateSignatures[0]; | ||
} | ||
|
||
const useConstructSignatures = callExpression.kind === SyntaxKind.NewExpression || callExpression.expression.kind === SyntaxKind.SuperKeyword; |
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 liked this form better. You just have to do more formatting now that the predicate is longer. #Closed
…alType property directly
Address comment: using ternary operator, fix comments
…AttributesTypeFromAttributesProperty
…ement` from inside `checkApplicableSignature`
…eralPropertyDeclaration => isObjectLiteralElement
… error in statelss function component
*/ | ||
function checkJsxAttributesAssignableToTagnameAttributes(openingLikeElement: JsxOpeningLikeElement) { | ||
// The function involves following steps: | ||
// 1. Figure out expected attributes type expected by resolving tag-name of the JSX opening-like element, tagetAttributesType. |
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:targetAttributesType
attributesTable = createMap<Symbol>(); | ||
if (attributesArray) { | ||
forEach(attributesArray, (attr) => { | ||
if (!filter || (filter && filter(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.
filter [](start = 36, length = 6)
this part of the predicate is implied by the preceding !filter ||
so you can simplify to !filter || filter(attr)
// i.e <div attr1={10} attr2="string" /> | ||
// attr1 and attr2 are treated as JSXAttributes attached in the JsxOpeningLikeElement as "attributes". They resolved to be sourceAttributesType. | ||
const sourceAttributesType = createJsxAttributesTypeFromAttributesProperty(openingLikeElement, | ||
(attribute: 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.
(attribute: Symbol) => [](start = 16, length = 22)
can't you just say attribute =>
? Or is there some reason to state the type here?
// object literal, lookup the property symbol in the contextual type, and use this for goto-definition. | ||
// For example | ||
// interface Props{ | ||
// /first*/prop1: number |
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.
/first*/ [](start = 20, length = 8)
should be /*first*/
const result: Symbol[] = []; | ||
const symbol = contextualType.getProperty(name); | ||
if (symbol) { | ||
result.push(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.
why not return [symbol]
here? For a union, do you want both the union's synthetic property and all constituent properties? Seems like the union check should come first and it should return result
at the end too.
// Foo( { pr/*1*/op1: 10, prop2: true }) | ||
const container = getContainingObjectLiteralElement(node); | ||
if (container) { | ||
const contextualType = typeChecker.getContextualType(node.parent.parent as Expression); |
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.
node.parent.parent [](start = 65, length = 18)
why not container.parent
here? They are the same thing. Also, contextualType isn't used later -- it's just checked. You could inline this variable, but it's probably better to have propertySymbols return undefined if it doesn't find a contextual type.
return false; | ||
} | ||
|
||
function getNameFromObjectLiteralElement(node: 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.
getNameFromObjectLiteralElement [](start = 13, length = 31)
I think getTextOfPropertyName from compiler's utilities is exactly the same as this 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.
Well, getTextOfPropertyName(node.name) ===
getNameFromObjectLiteralElement(node)`
In reply to: 88698797 [](ancestors = 88698797)
// } | ||
// function Foo(arg: Props) {} | ||
// Foo( { pr/*1*/op1: 10, prop2: true }) | ||
const container = getContainingObjectLiteralElement(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.
container [](start = 14, length = 9)
can you name container
to something more meaningful 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.
I like the changes although I still have some questions about gotoDefinition.
@sandersn Thanks for reviewing the original PR! This one should make life much easier
@mhegazy @RyanCavanaugh