From 218f7587e4866b2f5a3d6ec1d005657a0ad0a334 Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Tue, 17 Apr 2018 15:46:11 -0700 Subject: [PATCH 1/3] Correctly show instantiated signatures for JSX element signature help --- src/compiler/checker.ts | 78 +++++++++---------- ...ParametershasInstantiatedSignatureHelp.tsx | 19 +++++ 2 files changed, 56 insertions(+), 41 deletions(-) create mode 100644 tests/cases/fourslash/jsxWithTypeParametershasInstantiatedSignatureHelp.tsx diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index d009a956c43ee..18cc3407c7221 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -15984,6 +15984,25 @@ namespace ts { return undefined; } + function getInstantiatedJsxSignatures(openingLikeElement: JsxOpeningLikeElement, elementType: Type, reportErrors?: boolean) { + // 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 + return instantiateJsxSignatures(openingLikeElement, signatures); + } + /** * Resolve attributes type of the given opening-like element. The attributes type is a type of attributes associated with the given elementType. * For instance: @@ -16046,20 +16065,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. @@ -17749,11 +17758,11 @@ namespace ts { let typeArguments: NodeArray; - if (!isTaggedTemplate && !isDecorator && !isJsxOpeningOrSelfClosingElement) { - typeArguments = (node).typeArguments; + if (!isTaggedTemplate && !isDecorator) { + typeArguments = (node).typeArguments; // We already perform checking on the type arguments on the class declaration itself. - if ((node).expression.kind !== SyntaxKind.SuperKeyword) { + if (isJsxOpeningOrSelfClosingElement || (node).expression.kind !== SyntaxKind.SuperKeyword) { forEach(typeArguments, checkSourceElement); } } @@ -18342,30 +18351,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); @@ -18387,7 +18372,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."); } diff --git a/tests/cases/fourslash/jsxWithTypeParametershasInstantiatedSignatureHelp.tsx b/tests/cases/fourslash/jsxWithTypeParametershasInstantiatedSignatureHelp.tsx new file mode 100644 index 0000000000000..e317fe338edb8 --- /dev/null +++ b/tests/cases/fourslash/jsxWithTypeParametershasInstantiatedSignatureHelp.tsx @@ -0,0 +1,19 @@ +/// + +//// declare namespace JSX { +//// interface Element { +//// render(): Element | string | false; +//// } +//// } +//// +//// function SFC(_props: Record) { +//// return ''; +//// } +//// +//// (); +//// (/>); + +goTo.marker("1"); +verify.currentSignatureHelpIs("SFC(_props: Record): string"); +goTo.marker("2"); +verify.currentSignatureHelpIs("SFC(_props: Record): string"); From 9ab03c0b06bb62f696bbb97541782596cfb68c0a Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Tue, 17 Apr 2018 17:37:39 -0700 Subject: [PATCH 2/3] Also bundle fix for quickinfo --- src/compiler/checker.ts | 10 +++++++-- src/compiler/types.ts | 1 + .../tsxTypeArgumentResolution.errors.txt | 5 ++++- tests/cases/fourslash/jsxGenericQuickInfo.tsx | 22 +++++++++++++++++++ 4 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 tests/cases/fourslash/jsxGenericQuickInfo.tsx diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 18cc3407c7221..96eec44b1bcaf 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -386,6 +386,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); @@ -14986,8 +14987,13 @@ namespace ts { } } - if (context.typeArguments) { - signatures = mapDefined(signatures, s => getJsxSignatureTypeArgumentInstantiation(s, context, isJs)); + const links = getNodeLinks(context); + if (links.resolvedSignatures && links.resolvedSignatures !== resolvingSignaturesArray) { + signatures = links.resolvedSignatures; + } + else if (!links.resolvedSignatures) { + links.resolvedSignatures = resolvingSignaturesArray; + links.resolvedSignatures = signatures = instantiateJsxSignatures(context, signatures); } return getUnionType(map(signatures, ctor ? t => getJsxPropsTypeFromClassType(t, isJs, context, /*reportErrors*/ false) : t => getJsxPropsTypeFromCallSignature(t, context)), UnionReduction.None); diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 63720d5590015..e8eee66ff64e1 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3539,6 +3539,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?: 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 diff --git a/tests/baselines/reference/tsxTypeArgumentResolution.errors.txt b/tests/baselines/reference/tsxTypeArgumentResolution.errors.txt index 950d9549ba8be..21d0e8e0bcb26 100644 --- a/tests/baselines/reference/tsxTypeArgumentResolution.errors.txt +++ b/tests/baselines/reference/tsxTypeArgumentResolution.errors.txt @@ -6,6 +6,7 @@ tests/cases/conformance/jsx/file.tsx(20,13): error TS2558: Expected 1 type argum tests/cases/conformance/jsx/file.tsx(22,13): error TS2558: Expected 1 type arguments, but got 2. tests/cases/conformance/jsx/file.tsx(24,12): error TS1099: Type argument list cannot be empty. tests/cases/conformance/jsx/file.tsx(26,12): error TS1099: Type argument list cannot be empty. +tests/cases/conformance/jsx/file.tsx(39,14): error TS2344: Type 'Prop' does not satisfy the constraint '{ a: string; }'. tests/cases/conformance/jsx/file.tsx(39,14): error TS2344: Type 'Prop' does not satisfy the constraint '{ a: string; }'. Types of property 'a' are incompatible. Type 'number' is not assignable to type 'string'. @@ -24,7 +25,7 @@ tests/cases/conformance/jsx/file.tsx(53,47): error TS2326: Types of property 'b' Type 'string' is not assignable to type 'number'. -==== tests/cases/conformance/jsx/file.tsx (16 errors) ==== +==== tests/cases/conformance/jsx/file.tsx (17 errors) ==== import React = require('react'); interface Prop { @@ -79,6 +80,8 @@ tests/cases/conformance/jsx/file.tsx(53,47): error TS2326: Types of property 'b' x = a={10} b="hi" />; // error ~~~~ +!!! error TS2344: Type 'Prop' does not satisfy the constraint '{ a: string; }'. + ~~~~ !!! error TS2344: Type 'Prop' does not satisfy the constraint '{ a: string; }'. !!! error TS2344: Types of property 'a' are incompatible. !!! error TS2344: Type 'number' is not assignable to type 'string'. diff --git a/tests/cases/fourslash/jsxGenericQuickInfo.tsx b/tests/cases/fourslash/jsxGenericQuickInfo.tsx new file mode 100644 index 0000000000000..898c7c17790a0 --- /dev/null +++ b/tests/cases/fourslash/jsxGenericQuickInfo.tsx @@ -0,0 +1,22 @@ +/// +//@Filename: file.tsx +//// declare module JSX { +//// interface Element { } +//// interface IntrinsicElements { +//// } +//// interface ElementAttributesProperty { props } +//// } +//// interface Props { +//// items: T[]; +//// renderItem: (item: T) => string; +//// } +//// class Component { +//// constructor(props: Props) {} +//// props: Props; +//// } +//// var b = new Component({items: [0, 1, 2], render/*0*/Item: it/*1*/em => item.toFixed()}); +//// var c = item.toFixed()} +verify.quickInfoAt("0", "(property) Props.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"); From 4c094cb042cd8a17e6e7ea50effb6b1328229a4f Mon Sep 17 00:00:00 2001 From: Wesley Wigham Date: Tue, 17 Apr 2018 17:53:19 -0700 Subject: [PATCH 3/3] Use more complete cache to avoid duplicate errors --- src/compiler/checker.ts | 31 +++++++++++++++---- src/compiler/types.ts | 2 +- .../tsxTypeArgumentResolution.errors.txt | 5 +-- 3 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 96eec44b1bcaf..2e9b0418dd700 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -14988,12 +14988,16 @@ namespace ts { } const links = getNodeLinks(context); - if (links.resolvedSignatures && links.resolvedSignatures !== resolvingSignaturesArray) { - signatures = links.resolvedSignatures; + if (!links.resolvedSignatures) { + links.resolvedSignatures = createMap(); } - else if (!links.resolvedSignatures) { - links.resolvedSignatures = resolvingSignaturesArray; - links.resolvedSignatures = signatures = instantiateJsxSignatures(context, signatures); + 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); @@ -15991,6 +15995,19 @@ namespace ts { } 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) { @@ -16006,7 +16023,9 @@ namespace ts { } // Instantiate in context of source type - return instantiateJsxSignatures(openingLikeElement, signatures); + const results = instantiateJsxSignatures(openingLikeElement, signatures); + links.resolvedSignatures.set(cacheKey, results); + return results; } /** diff --git a/src/compiler/types.ts b/src/compiler/types.ts index e8eee66ff64e1..180861d3afcfa 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -3539,7 +3539,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?: Signature[]; // Cached signatures of jsx node + resolvedSignatures?: Map; // 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 diff --git a/tests/baselines/reference/tsxTypeArgumentResolution.errors.txt b/tests/baselines/reference/tsxTypeArgumentResolution.errors.txt index 21d0e8e0bcb26..950d9549ba8be 100644 --- a/tests/baselines/reference/tsxTypeArgumentResolution.errors.txt +++ b/tests/baselines/reference/tsxTypeArgumentResolution.errors.txt @@ -6,7 +6,6 @@ tests/cases/conformance/jsx/file.tsx(20,13): error TS2558: Expected 1 type argum tests/cases/conformance/jsx/file.tsx(22,13): error TS2558: Expected 1 type arguments, but got 2. tests/cases/conformance/jsx/file.tsx(24,12): error TS1099: Type argument list cannot be empty. tests/cases/conformance/jsx/file.tsx(26,12): error TS1099: Type argument list cannot be empty. -tests/cases/conformance/jsx/file.tsx(39,14): error TS2344: Type 'Prop' does not satisfy the constraint '{ a: string; }'. tests/cases/conformance/jsx/file.tsx(39,14): error TS2344: Type 'Prop' does not satisfy the constraint '{ a: string; }'. Types of property 'a' are incompatible. Type 'number' is not assignable to type 'string'. @@ -25,7 +24,7 @@ tests/cases/conformance/jsx/file.tsx(53,47): error TS2326: Types of property 'b' Type 'string' is not assignable to type 'number'. -==== tests/cases/conformance/jsx/file.tsx (17 errors) ==== +==== tests/cases/conformance/jsx/file.tsx (16 errors) ==== import React = require('react'); interface Prop { @@ -80,8 +79,6 @@ tests/cases/conformance/jsx/file.tsx(53,47): error TS2326: Types of property 'b' x = a={10} b="hi" />; // error ~~~~ -!!! error TS2344: Type 'Prop' does not satisfy the constraint '{ a: string; }'. - ~~~~ !!! error TS2344: Type 'Prop' does not satisfy the constraint '{ a: string; }'. !!! error TS2344: Types of property 'a' are incompatible. !!! error TS2344: Type 'number' is not assignable to type 'string'.