Add language service support for JSXAttributes

Add language service support for JSXAttributes

Add completion support

Add find-all-references support

Add goto-definition support
This commit is contained in:
Kanchalai Tanglertsampan
2016-11-08 08:56:08 -08:00
parent 41108dbaae
commit 16d1b5dc50
8 changed files with 185 additions and 52 deletions

View File

@@ -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(<JsxOpeningLikeElement>jsxContainer);
attrsType = typeChecker.getAllAttributesTypeFromJsxOpeningLikeElement(<JsxOpeningLikeElement>jsxContainer);
if (attrsType) {
symbols = filterJsxAttributes(typeChecker.getPropertiesOfType(attrsType), (<JsxOpeningLikeElement>jsxContainer).attributes);
symbols = filterJsxAttributes(typeChecker.getPropertiesOfType(attrsType), (<JsxOpeningLikeElement>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 <JsxOpeningLikeElement>parent;
}
else if (parent.kind === SyntaxKind.JsxAttribute) {
return <JsxOpeningLikeElement>parent.parent;
// Currently we parse JsxOpeninLikeElement as:
// JsxOpeninLikeElement
// attributes: JsxAttributes
// properties: NodeArray<JsxAttributeLike>
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 <JsxOpeningLikeElement>parent.parent;
// Currently we parse JsxOpeninLikeElement as:
// JsxOpeninLikeElement
// attributes: JsxAttributes
// properties: NodeArray<JsxAttributeLike>
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 <JsxOpeningLikeElement>parent.parent.parent;
parent.parent && parent.parent.kind === SyntaxKind.JsxAttribute) {
// Currently we parse JsxOpeninLikeElement as:
// JsxOpeninLikeElement
// attributes: JsxAttributes
// properties: NodeArray<JsxAttributeLike>
// each JsxAttribute can have initializer as JsxExpression
return /*JsxExpression*/parent./*JsxAttribute*/parent./*JsxAttributes*/parent.parent as JsxOpeningLikeElement;
}
if (parent && parent.kind === SyntaxKind.JsxSpreadAttribute) {
return <JsxOpeningLikeElement>parent.parent;
// Currently we parse JsxOpeninLikeElement as:
// JsxOpeninLikeElement
// attributes: JsxAttributes
// properties: NodeArray<JsxAttributeLike>
return /*properties list*/parent./*attributes*/parent.parent as JsxOpeningLikeElement;
}
break;

View File

@@ -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);

View File

@@ -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 = <Main/*firstTarget*/Button />; // 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;
}
}
}

View File

@@ -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 = (<ComputedPropertyName>node.name).expression;
// treat computed property names where expression is string/numeric literal as just string/numeric literal
if (isStringOrNumericLiteral(nameExpression.kind)) {
return (<LiteralExpression>nameExpression).text;
}
return undefined;
}
return (<Identifier | LiteralExpression>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) &&
(<ObjectLiteralElement>node.parent).name === node ? node.parent as ObjectLiteralElement : undefined;
}
return undefined;
}
/* @internal */
export function getPropertySymbolsFromContextualType(typeChecker: TypeChecker, node: ObjectLiteralElement): Symbol[] {
const objectLiteral = <ObjectLiteralExpression>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((<UnionType>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 &&

View File

@@ -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 = <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<T, U>#(a, b)# -> The token is either not associated with a list, or ends a list, so the session should end
// Case 3:
// foo<T#, U#>(a#, #b#) -> The token is buried inside a list, and should give sig help
// foo<T#, U#>(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 { ... }
// <MainBuntton /*signatureHelp*/
const attributeSpanStart = node.parent.attributes.getFullStart();
const attributeSpanEnd = skipTrivia(sourceFile.text, node.parent.attributes.getEnd(), /*stopAfterLineBreak*/ false);
return {
kind: ArgumentListKind.JSXAttributesArguments,
invocation: node.parent,
argumentsSpan: createTextSpan(attributeSpanStart, attributeSpanEnd - attributeSpanStart),
argumentIndex: 0,
argumentCount: 1
};
}
return undefined;
}
@@ -392,7 +409,7 @@ namespace ts.SignatureHelp {
//
// Note: this subtlety only applies to the last comma. If you had "Foo(a,," then
// we'll have: 'a' '<comma>' '<missing>'
// 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 {
: (<TemplateExpression>tagExpression.template).templateSpans.length + 1;
Debug.assert(argumentIndex === 0 || argumentIndex < argumentCount, `argumentCount < argumentIndex, ${argumentCount} < ${argumentIndex}`);
return {
kind: ArgumentListKind.TaggedTemplateArguments,
invocation: tagExpression,

View File

@@ -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 = <CallExpression | NewExpression>location;
callExpressionLike = <CallExpression | NewExpression>location;
}
else if (isCallExpressionTarget(location) || isNewExpressionTarget(location)) {
callExpression = <CallExpression | NewExpression>location.parent;
callExpressionLike = <CallExpression | NewExpression>location.parent;
}
else if (location.parent && isJsxOpeningLikeElement(location.parent) && isFunctionLike(symbol.valueDeclaration)) {
callExpressionLike = <JsxOpeningLikeElement>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) {

View File

@@ -765,6 +765,11 @@ namespace ts {
export const directory = "directory";
export const externalModuleName = "external module name";
/**
* <JsxTagName attribute1 attribute2={0} />
**/
export const jsxAttribute = "JSX attribute";
}
export namespace ScriptElementKindModifier {

View File

@@ -31,6 +31,7 @@ namespace ts {
case SyntaxKind.FunctionExpression:
case SyntaxKind.ArrowFunction:
case SyntaxKind.CatchClause:
case SyntaxKind.JsxAttribute:
return SemanticMeaning.Value;
case SyntaxKind.TypeParameter: