Skip to content

Correctly show instantiated signatures for JSX element signature help and quick info #23492

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

Merged
merged 4 commits into from
May 16, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 63 additions & 42 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,7 @@ namespace ts {
const unknownSignature = createSignature(undefined, undefined, undefined, emptyArray, unknownType, /*resolvedTypePredicate*/ undefined, 0, /*hasRestParameter*/ false, /*hasLiteralTypes*/ false);
const resolvingSignature = createSignature(undefined, undefined, undefined, emptyArray, anyType, /*resolvedTypePredicate*/ undefined, 0, /*hasRestParameter*/ false, /*hasLiteralTypes*/ false);
const silentNeverSignature = createSignature(undefined, undefined, undefined, emptyArray, silentNeverType, /*resolvedTypePredicate*/ undefined, 0, /*hasRestParameter*/ false, /*hasLiteralTypes*/ false);
const resolvingSignaturesArray = [resolvingSignature];

const enumNumberIndexInfo = createIndexInfo(stringType, /*isReadonly*/ true);
const jsObjectLiteralIndexInfo = createIndexInfo(anyType, /*isReadonly*/ false);
Expand Down Expand Up @@ -15255,8 +15256,17 @@ namespace ts {
}
}

if (context.typeArguments) {
signatures = mapDefined(signatures, s => getJsxSignatureTypeArgumentInstantiation(s, context, isJs));
const links = getNodeLinks(context);
if (!links.resolvedSignatures) {
links.resolvedSignatures = createMap();
}
const cacheKey = "" + getTypeId(valueType);
if (links.resolvedSignatures.get(cacheKey) && links.resolvedSignatures.get(cacheKey) !== resolvingSignaturesArray) {
signatures = links.resolvedSignatures.get(cacheKey);
}
else if (!links.resolvedSignatures.get(cacheKey)) {
links.resolvedSignatures.set(cacheKey, resolvingSignaturesArray);
links.resolvedSignatures.set(cacheKey, signatures = instantiateJsxSignatures(context, signatures));
}

return getUnionType(map(signatures, ctor ? t => getJsxPropsTypeFromClassType(t, isJs, context, /*reportErrors*/ false) : t => getJsxPropsTypeFromCallSignature(t, context)), UnionReduction.None);
Expand Down Expand Up @@ -16237,6 +16247,40 @@ namespace ts {
return undefined;
}

function getInstantiatedJsxSignatures(openingLikeElement: JsxOpeningLikeElement, elementType: Type, reportErrors?: boolean) {
const links = getNodeLinks(openingLikeElement);
if (!links.resolvedSignatures) {
links.resolvedSignatures = createMap();
}
const cacheKey = "" + getTypeId(elementType);
if (links.resolvedSignatures.get(cacheKey) && links.resolvedSignatures.get(cacheKey) === resolvingSignaturesArray) {
return;
}
else if (links.resolvedSignatures.get(cacheKey)) {
return links.resolvedSignatures.get(cacheKey);
}

links.resolvedSignatures.set(cacheKey, resolvingSignaturesArray);
// Resolve the signatures, preferring constructor
let signatures = getSignaturesOfType(elementType, SignatureKind.Construct);
if (signatures.length === 0) {
// No construct signatures, try call signatures
signatures = getSignaturesOfType(elementType, SignatureKind.Call);
if (signatures.length === 0) {
// We found no signatures at all, which is an error
if (reportErrors) {
error(openingLikeElement.tagName, Diagnostics.JSX_element_type_0_does_not_have_any_construct_or_call_signatures, getTextOfNode(openingLikeElement.tagName));
}
return;
}
}

// Instantiate in context of source type
const results = instantiateJsxSignatures(openingLikeElement, signatures);
links.resolvedSignatures.set(cacheKey, results);
return results;
}

/**
* Resolve attributes type of the given opening-like element. The attributes type is a type of attributes associated with the given elementType.
* For instance:
Expand Down Expand Up @@ -16299,20 +16343,10 @@ namespace ts {

// Get the element instance type (the result of newing or invoking this tag)

// Resolve the signatures, preferring constructor
let signatures = getSignaturesOfType(elementType, SignatureKind.Construct);
if (signatures.length === 0) {
// No construct signatures, try call signatures
signatures = getSignaturesOfType(elementType, SignatureKind.Call);
if (signatures.length === 0) {
// We found no signatures at all, which is an error
error(openingLikeElement.tagName, Diagnostics.JSX_element_type_0_does_not_have_any_construct_or_call_signatures, getTextOfNode(openingLikeElement.tagName));
return unknownType;
}
const instantiatedSignatures = getInstantiatedJsxSignatures(openingLikeElement, elementType, /*reportErrors*/ true);
if (!length(instantiatedSignatures)) {
return unknownType;
}

// Instantiate in context of source type
const instantiatedSignatures = instantiateJsxSignatures(openingLikeElement, signatures);
const elemInstanceType = getUnionType(map(instantiatedSignatures, getReturnTypeOfSignature), UnionReduction.Subtype);

// If we should include all stateless attributes type, then get all attributes type from all stateless function signature.
Expand Down Expand Up @@ -18002,11 +18036,11 @@ namespace ts {

let typeArguments: NodeArray<TypeNode>;

if (!isDecorator && !isJsxOpeningOrSelfClosingElement) {
if (!isDecorator) {
typeArguments = (<CallExpression>node).typeArguments;

// We already perform checking on the type arguments on the class declaration itself.
if (isTaggedTemplate || (<CallExpression>node).expression.kind !== SyntaxKind.SuperKeyword) {
if (isTaggedTemplate || isJsxOpeningOrSelfClosingElement || (<CallExpression>node).expression.kind !== SyntaxKind.SuperKeyword) {
forEach(typeArguments, checkSourceElement);
}
}
Expand Down Expand Up @@ -18595,30 +18629,6 @@ namespace ts {
*/
function getResolvedJsxStatelessFunctionSignature(openingLikeElement: JsxOpeningLikeElement, elementType: Type, candidatesOutArray: Signature[]): Signature | undefined {
Debug.assert(!(elementType.flags & TypeFlags.Union));
return resolveStatelessJsxOpeningLikeElement(openingLikeElement, elementType, candidatesOutArray);
}

/**
* Try treating a given opening-like element as stateless function component and resolve a tagName to a function signature.
* @param openingLikeElement an JSX opening-like element we want to try resolve its stateless function if possible
* @param elementType a type of the opening-like JSX element, a result of resolving tagName in opening-like element.
* @param candidatesOutArray an array of signature to be filled in by the function. It is passed by signature help in the language service;
* the function will fill it up with appropriate candidate signatures
* @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 resolveStatelessJsxOpeningLikeElement(openingLikeElement: JsxOpeningLikeElement, elementType: Type, candidatesOutArray: Signature[]): Signature | undefined {
// If this function is called from language service, elementType can be a union type. This is not possible if the function is called from compiler (see: resolveCustomJsxElementAttributesType)
if (elementType.flags & TypeFlags.Union) {
const types = (elementType as UnionType).types;
let result: Signature;
for (const type of types) {
result = result || resolveStatelessJsxOpeningLikeElement(openingLikeElement, type, candidatesOutArray);
}

return result;
}

const callSignatures = elementType && getSignaturesOfType(elementType, SignatureKind.Call);
if (callSignatures && callSignatures.length > 0) {
return resolveCall(openingLikeElement, callSignatures, candidatesOutArray);
Expand All @@ -18640,7 +18650,18 @@ namespace ts {
case SyntaxKind.JsxOpeningElement:
case SyntaxKind.JsxSelfClosingElement:
// This code-path is called by language service
return resolveStatelessJsxOpeningLikeElement(node, checkExpression(node.tagName), candidatesOutArray) || unknownSignature;
const exprTypes = checkExpression(node.tagName);
return forEachType(exprTypes, exprType => {
const sfcResult = getResolvedJsxStatelessFunctionSignature(node, exprType, candidatesOutArray);
if (sfcResult && sfcResult !== unknownSignature) {
return sfcResult;
}
const sigs = getInstantiatedJsxSignatures(node, exprType);
if (candidatesOutArray && length(sigs)) {
candidatesOutArray.push(...sigs);
}
return length(sigs) ? sigs[0] : unknownSignature;
}) || unknownSignature;
}
Debug.assertNever(node, "Branch in 'resolveSignature' should be unreachable.");
}
Expand Down
1 change: 1 addition & 0 deletions src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3560,6 +3560,7 @@ namespace ts {
flags?: NodeCheckFlags; // Set of flags specific to Node
resolvedType?: Type; // Cached type of type node
resolvedSignature?: Signature; // Cached signature of signature node or call expression
resolvedSignatures?: Map<Signature[]>; // Cached signatures of jsx node
resolvedSymbol?: Symbol; // Cached name resolution result
resolvedIndexInfo?: IndexInfo; // Cached indexing info resolution result
maybeTypePredicate?: boolean; // Cached check whether call expression might reference a type predicate
Expand Down
22 changes: 22 additions & 0 deletions tests/cases/fourslash/jsxGenericQuickInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/// <reference path="fourslash.ts" />
//@Filename: file.tsx
//// declare module JSX {
//// interface Element { }
//// interface IntrinsicElements {
//// }
//// interface ElementAttributesProperty { props }
//// }
//// interface Props<T> {
//// items: T[];
//// renderItem: (item: T) => string;
//// }
//// class Component<T> {
//// constructor(props: Props<T>) {}
//// props: Props<T>;
//// }
//// var b = new Component({items: [0, 1, 2], render/*0*/Item: it/*1*/em => item.toFixed()});
//// var c = <Component items={[0, 1, 2]} render/*2*/Item={it/*3*/em => item.toFixed()}
verify.quickInfoAt("0", "(property) Props<number>.renderItem: (item: number) => string");
verify.quickInfoAt("1", "(parameter) item: number");
verify.quickInfoAt("2", "(JSX attribute) renderItem: (item: number) => string");
verify.quickInfoAt("3", "(parameter) item: number");
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/// <reference path="./fourslash.ts" />

//// declare namespace JSX {
//// interface Element {
//// render(): Element | string | false;
//// }
//// }
////
//// function SFC<T>(_props: Record<string, T>) {
//// return '';
//// }
////
//// (</*1*/SFC/>);
//// (</*2*/SFC<string>/>);

goTo.marker("1");
verify.currentSignatureHelpIs("SFC(_props: Record<string, {}>): string");
goTo.marker("2");
verify.currentSignatureHelpIs("SFC(_props: Record<string, string>): string");