From 16d1b5dc50f232da801d7fc1d947899890fb2cc1 Mon Sep 17 00:00:00 2001 From: Kanchalai Tanglertsampan Date: Tue, 8 Nov 2016 08:56:08 -0800 Subject: [PATCH] Add language service support for JSXAttributes Add language service support for JSXAttributes Add completion support Add find-all-references support Add goto-definition support --- src/services/completions.ts | 33 +++++++++++---- src/services/findAllReferences.ts | 29 ------------- src/services/goToDefinition.ts | 40 +++++++++++++++++- src/services/services.ts | 70 +++++++++++++++++++++++++++++++ src/services/signatureHelp.ts | 32 ++++++++++---- src/services/symbolDisplay.ts | 27 +++++++++--- src/services/types.ts | 5 +++ src/services/utilities.ts | 1 + 8 files changed, 185 insertions(+), 52 deletions(-) diff --git a/src/services/completions.ts b/src/services/completions.ts index d6d3a9ce060..294210c1245 100644 --- a/src/services/completions.ts +++ b/src/services/completions.ts @@ -1029,10 +1029,10 @@ namespace ts.Completions { let attrsType: Type; if ((jsxContainer.kind === SyntaxKind.JsxSelfClosingElement) || (jsxContainer.kind === SyntaxKind.JsxOpeningElement)) { // Cursor is inside a JSX self-closing element or opening element - attrsType = typeChecker.getJsxElementAttributesType(jsxContainer); + attrsType = typeChecker.getAllAttributesTypeFromJsxOpeningLikeElement(jsxContainer); if (attrsType) { - symbols = filterJsxAttributes(typeChecker.getPropertiesOfType(attrsType), (jsxContainer).attributes); + symbols = filterJsxAttributes(typeChecker.getPropertiesOfType(attrsType), (jsxContainer).attributes.properties); isMemberCompletion = true; isNewIdentifierLocation = false; return true; @@ -1374,13 +1374,18 @@ namespace ts.Completions { case SyntaxKind.LessThanSlashToken: case SyntaxKind.SlashToken: case SyntaxKind.Identifier: + case SyntaxKind.JsxAttributes: case SyntaxKind.JsxAttribute: case SyntaxKind.JsxSpreadAttribute: if (parent && (parent.kind === SyntaxKind.JsxSelfClosingElement || parent.kind === SyntaxKind.JsxOpeningElement)) { return parent; } else if (parent.kind === SyntaxKind.JsxAttribute) { - return parent.parent; + // Currently we parse JsxOpeninLikeElement as: + // JsxOpeninLikeElement + // attributes: JsxAttributes + // properties: NodeArray + return /*properties list*/parent./*attributes*/parent.parent as JsxOpeningLikeElement; } break; @@ -1389,7 +1394,11 @@ namespace ts.Completions { // whose parent is a JsxOpeningLikeElement case SyntaxKind.StringLiteral: if (parent && ((parent.kind === SyntaxKind.JsxAttribute) || (parent.kind === SyntaxKind.JsxSpreadAttribute))) { - return parent.parent; + // Currently we parse JsxOpeninLikeElement as: + // JsxOpeninLikeElement + // attributes: JsxAttributes + // properties: NodeArray + return /*properties list*/parent./*attributes*/parent.parent as JsxOpeningLikeElement; } break; @@ -1397,13 +1406,21 @@ namespace ts.Completions { case SyntaxKind.CloseBraceToken: if (parent && parent.kind === SyntaxKind.JsxExpression && - parent.parent && - (parent.parent.kind === SyntaxKind.JsxAttribute)) { - return parent.parent.parent; + parent.parent && parent.parent.kind === SyntaxKind.JsxAttribute) { + // Currently we parse JsxOpeninLikeElement as: + // JsxOpeninLikeElement + // attributes: JsxAttributes + // properties: NodeArray + // each JsxAttribute can have initializer as JsxExpression + return /*JsxExpression*/parent./*JsxAttribute*/parent./*JsxAttributes*/parent.parent as JsxOpeningLikeElement; } if (parent && parent.kind === SyntaxKind.JsxSpreadAttribute) { - return parent.parent; + // Currently we parse JsxOpeninLikeElement as: + // JsxOpeninLikeElement + // attributes: JsxAttributes + // properties: NodeArray + return /*properties list*/parent./*attributes*/parent.parent as JsxOpeningLikeElement; } break; diff --git a/src/services/findAllReferences.ts b/src/services/findAllReferences.ts index 090357a56b2..68960c7a887 100644 --- a/src/services/findAllReferences.ts +++ b/src/services/findAllReferences.ts @@ -1358,35 +1358,6 @@ namespace ts.FindAllReferences { }); } - /** - * Returns the containing object literal property declaration given a possible name node, e.g. "a" in x = { "a": 1 } - */ - function getContainingObjectLiteralElement(node: Node): ObjectLiteralElement { - switch (node.kind) { - case SyntaxKind.StringLiteral: - case SyntaxKind.NumericLiteral: - if (node.parent.kind === SyntaxKind.ComputedPropertyName) { - return isObjectLiteralPropertyDeclaration(node.parent.parent) ? node.parent.parent : undefined; - } - // intential fall through - case SyntaxKind.Identifier: - return isObjectLiteralPropertyDeclaration(node.parent) && node.parent.name === node ? node.parent : undefined; - } - return undefined; - } - - function isObjectLiteralPropertyDeclaration(node: Node): node is ObjectLiteralElement { - switch (node.kind) { - case SyntaxKind.PropertyAssignment: - case SyntaxKind.ShorthandPropertyAssignment: - case SyntaxKind.MethodDeclaration: - case SyntaxKind.GetAccessor: - case SyntaxKind.SetAccessor: - return true; - } - return false; - } - /** Get `C` given `N` if `N` is in the position `class C extends N` or `class C extends foo.N` where `N` is an identifier. */ function tryGetClassByExtendingIdentifier(node: Node): ClassLikeDeclaration | undefined { return tryGetClassExtendingExpressionWithTypeArguments(climbPastPropertyAccess(node).parent); diff --git a/src/services/goToDefinition.ts b/src/services/goToDefinition.ts index ab35a17fcef..c8114e1809e 100644 --- a/src/services/goToDefinition.ts +++ b/src/services/goToDefinition.ts @@ -85,6 +85,39 @@ namespace ts.GoToDefinition { declaration => createDefinitionInfo(declaration, shorthandSymbolKind, shorthandSymbolName, shorthandContainerName)); } + if (isJsxOpeningLikeElement(node.parent)) { + // For JSX opening-like element, the tag can be resolved either as stateful component (e.g class) or stateless function component. + // Because if it is a stateless function component with an error while trying to resolve the signature, we don't want to return all + // possible overloads but just the first one. + // For example: + // /*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 =
; // We get undefined for resolved signature indicating an error, then just return the first declaration + const {symbolName, symbolKind, containerName} = getSymbolInfo(typeChecker, symbol, node); + return [createDefinitionInfo(symbol.valueDeclaration, symbolKind, symbolName, containerName)]; + } + + // If the current location we want to find its definition is in an object literal, try to get the contextual type for the + // object literal, lookup the property symbol in the contextual type, and use this for goto-definition. + // For example + // interface Props{ + // /first*/prop1: number + // prop2: boolean + // } + // function Foo(arg: Props) {} + // Foo( { pr/*1*/op1: 10, prop2: true }) + const container = getContainingObjectLiteralElement(node); + if (container) { + const contextualType = typeChecker.getContextualType(node.parent.parent as Expression); + if (contextualType) { + let result: DefinitionInfo[] = []; + forEach(getPropertySymbolsFromContextualType(typeChecker, container), contextualSymbol => { + result = result.concat(getDefinitionFromSymbol(typeChecker, contextualSymbol, node)); + }); + return result; + } + } return getDefinitionFromSymbol(typeChecker, symbol, node); } @@ -254,6 +287,11 @@ namespace ts.GoToDefinition { function tryGetSignatureDeclaration(typeChecker: TypeChecker, node: Node): SignatureDeclaration | undefined { const callLike = getAncestorCallLikeExpression(node); - return callLike && typeChecker.getResolvedSignature(callLike).declaration; + if (callLike) { + const resolvedSignature = typeChecker.getResolvedSignature(callLike); + // We have to check that resolvedSignature is not undefined because in the case of JSX opening-like element, + // it may not be a stateless function component which then will cause getResolvedSignature to return undefined. + return resolvedSignature && resolvedSignature.declaration; + } } } diff --git a/src/services/services.ts b/src/services/services.ts index 26374eccbfd..e43a5356a7b 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -1990,6 +1990,76 @@ namespace ts { } } + function isObjectLiteralPropertyDeclaration(node: Node): node is ObjectLiteralElement { + switch (node.kind) { + case SyntaxKind.JsxAttribute: + case SyntaxKind.PropertyAssignment: + case SyntaxKind.ShorthandPropertyAssignment: + case SyntaxKind.MethodDeclaration: + case SyntaxKind.GetAccessor: + case SyntaxKind.SetAccessor: + return true; + } + return false; + } + + function getNameFromObjectLiteralElement(node: ObjectLiteralElement) { + if (node.name.kind === SyntaxKind.ComputedPropertyName) { + const nameExpression = (node.name).expression; + // treat computed property names where expression is string/numeric literal as just string/numeric literal + if (isStringOrNumericLiteral(nameExpression.kind)) { + return (nameExpression).text; + } + return undefined; + } + return (node.name).text; + } + + /** + * Returns the containing object literal property declaration given a possible name node, e.g. "a" in x = { "a": 1 } + */ + /* @internal */ + export function getContainingObjectLiteralElement(node: Node): ObjectLiteralElement { + switch (node.kind) { + case SyntaxKind.StringLiteral: + case SyntaxKind.NumericLiteral: + if (node.parent.kind === SyntaxKind.ComputedPropertyName) { + return isObjectLiteralPropertyDeclaration(node.parent.parent) ? node.parent.parent : undefined; + } + // intentionally fall through + case SyntaxKind.Identifier: + return isObjectLiteralPropertyDeclaration(node.parent) && + (node.parent.parent.kind === SyntaxKind.ObjectLiteralExpression || node.parent.parent.kind === SyntaxKind.JsxAttributes) && + (node.parent).name === node ? node.parent as ObjectLiteralElement : undefined; + } + return undefined; + } + + /* @internal */ + export function getPropertySymbolsFromContextualType(typeChecker: TypeChecker, node: ObjectLiteralElement): Symbol[] { + const objectLiteral = node.parent; + const contextualType = typeChecker.getContextualType(objectLiteral); + const name = getNameFromObjectLiteralElement(node); + if (name && contextualType) { + const result: Symbol[] = []; + const symbol = contextualType.getProperty(name); + if (symbol) { + result.push(symbol); + } + + if (contextualType.flags & TypeFlags.Union) { + forEach((contextualType).types, t => { + const symbol = t.getProperty(name); + if (symbol) { + result.push(symbol); + } + }); + } + return result; + } + return undefined; + } + function isArgumentOfElementAccessExpression(node: Node) { return node && node.parent && diff --git a/src/services/signatureHelp.ts b/src/services/signatureHelp.ts index a78370dba72..4bf40a98452 100644 --- a/src/services/signatureHelp.ts +++ b/src/services/signatureHelp.ts @@ -168,7 +168,8 @@ namespace ts.SignatureHelp { export const enum ArgumentListKind { TypeArguments, CallArguments, - TaggedTemplateArguments + TaggedTemplateArguments, + JSXAttributesArguments } export interface ArgumentListInfo { @@ -264,18 +265,18 @@ namespace ts.SignatureHelp { if (node.parent.kind === SyntaxKind.CallExpression || node.parent.kind === SyntaxKind.NewExpression) { const callExpression = node.parent; // There are 3 cases to handle: - // 1. The token introduces a list, and should begin a sig help session + // 1. The token introduces a list, and should begin a signature help session // 2. The token is either not associated with a list, or ends a list, so the session should end - // 3. The token is buried inside a list, and should give sig help + // 3. The token is buried inside a list, and should give signature help // // The following are examples of each: // // Case 1: - // foo<#T, U>(#a, b) -> The token introduces a list, and should begin a sig help session + // foo<#T, U>(#a, b) -> The token introduces a list, and should begin a signature help session // Case 2: // fo#o#(a, b)# -> The token is either not associated with a list, or ends a list, so the session should end // Case 3: - // foo(a#, #b#) -> The token is buried inside a list, and should give sig help + // foo(a#, #b#) -> The token is buried inside a list, and should give signature help // Find out if 'node' is an argument, a type argument, or neither if (node.kind === SyntaxKind.LessThanToken || node.kind === SyntaxKind.OpenParenToken) { @@ -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. // - Between the type arguments and the arguments (greater than token) // - On the target of the call (parent.func) // - On the 'new' keyword in a 'new' expression @@ -352,6 +353,22 @@ namespace ts.SignatureHelp { return getArgumentListInfoForTemplate(tagExpression, argumentIndex, sourceFile); } + else if (node.parent && isJsxOpeningLikeElement(node.parent)) { + // Provide a signature help for JSX opening element or JSX self-closing element. + // 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 { ... } + // ' '' - // That will give us 2 non-commas. We then add one for the last comma, givin us an + // That will give us 2 non-commas. We then add one for the last comma, giving us an // arg count of 3. const listChildren = argumentsList.getChildren(); @@ -435,7 +452,6 @@ namespace ts.SignatureHelp { : (tagExpression.template).templateSpans.length + 1; Debug.assert(argumentIndex === 0 || argumentIndex < argumentCount, `argumentCount < argumentIndex, ${argumentCount} < ${argumentIndex}`); - return { kind: ArgumentListKind.TaggedTemplateArguments, invocation: tagExpression, diff --git a/src/services/symbolDisplay.ts b/src/services/symbolDisplay.ts index b32bd8f331d..d43fdd3bf2a 100644 --- a/src/services/symbolDisplay.ts +++ b/src/services/symbolDisplay.ts @@ -71,6 +71,9 @@ namespace ts.SymbolDisplay { } return unionPropertyKind; } + if (location.parent && isJsxAttribute(location.parent)) { + return ScriptElementKind.jsxAttribute; + } return ScriptElementKind.memberVariableElement; } @@ -114,23 +117,33 @@ namespace ts.SymbolDisplay { } // try get the call/construct signature from the type if it matches - let callExpression: CallExpression | NewExpression; + let callExpressionLike: CallExpression | NewExpression | JsxOpeningLikeElement; if (location.kind === SyntaxKind.CallExpression || location.kind === SyntaxKind.NewExpression) { - callExpression = location; + callExpressionLike = location; } else if (isCallExpressionTarget(location) || isNewExpressionTarget(location)) { - callExpression = location.parent; + callExpressionLike = location.parent; + } + else if (location.parent && isJsxOpeningLikeElement(location.parent) && isFunctionLike(symbol.valueDeclaration)) { + callExpressionLike = location.parent; } - if (callExpression) { + if (callExpressionLike) { const candidateSignatures: Signature[] = []; - signature = typeChecker.getResolvedSignature(callExpression, candidateSignatures); + signature = typeChecker.getResolvedSignature(callExpressionLike, candidateSignatures); if (!signature && candidateSignatures.length) { // Use the first candidate: signature = candidateSignatures[0]; } - const useConstructSignatures = callExpression.kind === SyntaxKind.NewExpression || callExpression.expression.kind === SyntaxKind.SuperKeyword; + let useConstructSignatures = false; + if (callExpressionLike.kind === SyntaxKind.NewExpression) { + useConstructSignatures = true; + } + else if (isCallExpression(callExpressionLike) && callExpressionLike.expression.kind === SyntaxKind.SuperKeyword) { + useConstructSignatures = true; + } + const allSignatures = useConstructSignatures ? type.getConstructSignatures() : type.getCallSignatures(); if (!contains(allSignatures, signature.target) && !contains(allSignatures, signature)) { @@ -160,6 +173,7 @@ namespace ts.SymbolDisplay { } switch (symbolKind) { + case ScriptElementKind.jsxAttribute: case ScriptElementKind.memberVariableElement: case ScriptElementKind.variableElement: case ScriptElementKind.constElement: @@ -373,6 +387,7 @@ namespace ts.SymbolDisplay { // For properties, variables and local vars: show the type if (symbolKind === ScriptElementKind.memberVariableElement || + symbolKind === ScriptElementKind.jsxAttribute || symbolFlags & SymbolFlags.Variable || symbolKind === ScriptElementKind.localVariableElement || isThisExpression) { diff --git a/src/services/types.ts b/src/services/types.ts index 88ffe2950ce..3a195c82f42 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -765,6 +765,11 @@ namespace ts { export const directory = "directory"; export const externalModuleName = "external module name"; + + /** + * + **/ + export const jsxAttribute = "JSX attribute"; } export namespace ScriptElementKindModifier { diff --git a/src/services/utilities.ts b/src/services/utilities.ts index def7ab8e4bd..eeb9725ba47 100644 --- a/src/services/utilities.ts +++ b/src/services/utilities.ts @@ -31,6 +31,7 @@ namespace ts { case SyntaxKind.FunctionExpression: case SyntaxKind.ArrowFunction: case SyntaxKind.CatchClause: + case SyntaxKind.JsxAttribute: return SemanticMeaning.Value; case SyntaxKind.TypeParameter: