feat(7411): Resolve intrinsics elements by JSX namespaced tag names (#53799)

This commit is contained in:
Oleksandr T 2023-04-17 03:20:10 +03:00 committed by GitHub
parent f8b3ea7972
commit 378ffa4bc9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 273 additions and 22 deletions

View File

@ -89,6 +89,7 @@ import {
getEnclosingBlockScopeContainer,
getErrorSpanForNode,
getEscapedTextOfIdentifierOrLiteral,
getEscapedTextOfJsxAttributeName,
getExpandoInitializer,
getHostSignatureFromJSDoc,
getImmediatelyInvokedFunctionExpression,
@ -171,6 +172,7 @@ import {
isJSDocTemplateTag,
isJSDocTypeAlias,
isJsonSourceFile,
isJsxNamespacedName,
isLeftHandSideExpression,
isLogicalOrCoalescingAssignmentExpression,
isLogicalOrCoalescingAssignmentOperator,
@ -679,6 +681,9 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
const containingClassSymbol = containingClass.symbol;
return getSymbolNameForPrivateIdentifier(containingClassSymbol, name.escapedText);
}
if (isJsxNamespacedName(name)) {
return getEscapedTextOfJsxAttributeName(name);
}
return isPropertyNameLiteral(name) ? getEscapedTextOfIdentifierOrLiteral(name) : undefined;
}
switch (node.kind) {

View File

@ -276,6 +276,7 @@ import {
getErrorSpanForNode,
getEscapedTextOfIdentifierOrLiteral,
getEscapedTextOfJsxAttributeName,
getEscapedTextOfJsxNamespacedName,
getESModuleInterop,
getExpandoInitializer,
getExportAssignmentExpression,
@ -429,6 +430,7 @@ import {
InternalSymbolName,
IntersectionType,
IntersectionTypeNode,
intrinsicTagNameToString,
IntrinsicType,
introducesArgumentsExoticObject,
isAccessExpression,
@ -782,6 +784,7 @@ import {
JsxExpression,
JsxFlags,
JsxFragment,
JsxNamespacedName,
JsxOpeningElement,
JsxOpeningFragment,
JsxOpeningLikeElement,
@ -29597,7 +29600,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
function getStaticTypeOfReferencedJsxConstructor(context: JsxOpeningLikeElement) {
if (isJsxIntrinsicIdentifier(context.tagName)) {
if (isJsxIntrinsicTagName(context.tagName)) {
const result = getIntrinsicAttributesTypeFromJsxOpeningLikeElement(context);
const fakeSignature = createSignatureForJSXIntrinsic(context, result);
return getOrCreateTypeFromSignature(fakeSignature);
@ -30317,7 +30320,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
checkJsxOpeningLikeElementOrOpeningFragment(node.openingElement);
// Perform resolution on the closing tag so that rename/go to definition/etc work
if (isJsxIntrinsicIdentifier(node.closingElement.tagName)) {
if (isJsxIntrinsicTagName(node.closingElement.tagName)) {
getIntrinsicTagSymbol(node.closingElement);
}
else {
@ -30357,8 +30360,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
/**
* Returns true iff React would emit this tag name as a string rather than an identifier or qualified name
*/
function isJsxIntrinsicIdentifier(tagName: JsxTagNameExpression): tagName is Identifier {
return tagName.kind === SyntaxKind.Identifier && isIntrinsicJsxName(tagName.escapedText);
function isJsxIntrinsicTagName(tagName: Node): tagName is Identifier | JsxNamespacedName {
return isIdentifier(tagName) && isIntrinsicJsxName(tagName.escapedText) || isJsxNamespacedName(tagName);
}
function checkJsxAttribute(node: JsxAttribute, checkMode?: CheckMode) {
@ -30563,8 +30566,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const intrinsicElementsType = getJsxType(JsxNames.IntrinsicElements, node);
if (!isErrorType(intrinsicElementsType)) {
// Property case
if (!isIdentifier(node.tagName)) return Debug.fail();
const intrinsicProp = getPropertyOfType(intrinsicElementsType, node.tagName.escapedText);
if (!isIdentifier(node.tagName) && !isJsxNamespacedName(node.tagName)) return Debug.fail();
const intrinsicProp = getPropertyOfType(intrinsicElementsType, isJsxNamespacedName(node.tagName) ? getEscapedTextOfJsxNamespacedName(node.tagName) : node.tagName.escapedText);
if (intrinsicProp) {
links.jsxFlags |= JsxFlags.IntrinsicNamedElement;
return links.resolvedSymbol = intrinsicProp;
@ -30578,7 +30581,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
// Wasn't found
error(node, Diagnostics.Property_0_does_not_exist_on_type_1, idText(node.tagName), "JSX." + JsxNames.IntrinsicElements);
error(node, Diagnostics.Property_0_does_not_exist_on_type_1, intrinsicTagNameToString(node.tagName), "JSX." + JsxNames.IntrinsicElements);
return links.resolvedSymbol = unknownSymbol;
}
else {
@ -30787,7 +30790,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
* @param node an intrinsic JSX opening-like element
*/
function getIntrinsicAttributesTypeFromJsxOpeningLikeElement(node: JsxOpeningLikeElement): Type {
Debug.assert(isJsxIntrinsicIdentifier(node.tagName));
Debug.assert(isJsxIntrinsicTagName(node.tagName));
const links = getNodeLinks(node);
if (!links.resolvedJsxElementAttributesType) {
const symbol = getIntrinsicTagSymbol(node);
@ -30900,8 +30903,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const elementTypeConstraint = getJsxElementTypeTypeAt(jsxOpeningLikeNode);
if (elementTypeConstraint !== undefined) {
const tagName = jsxOpeningLikeNode.tagName;
const tagType = isJsxIntrinsicIdentifier(tagName)
? getStringLiteralType(unescapeLeadingUnderscores(tagName.escapedText))
const tagType = isJsxIntrinsicTagName(tagName)
? getStringLiteralType(intrinsicTagNameToString(tagName))
: checkExpression(tagName);
checkTypeRelatedTo(tagType, elementTypeConstraint, assignableRelation, tagName, Diagnostics.Its_type_0_is_not_a_valid_JSX_element_type, () => {
const componentName = getTextOfNode(tagName);
@ -32521,7 +32524,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
function getJsxReferenceKind(node: JsxOpeningLikeElement): JsxReferenceKind {
if (isJsxIntrinsicIdentifier(node.tagName)) {
if (isJsxIntrinsicTagName(node.tagName)) {
return JsxReferenceKind.Mixed;
}
const tagType = getApparentType(checkExpression(node.tagName));
@ -32568,7 +32571,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
if (getJsxNamespaceContainerForImplicitImport(node)) {
return true; // factory is implicitly jsx/jsxdev - assume it fits the bill, since we don't strongly look for the jsx/jsxs/jsxDEV factory APIs anywhere else (at least not yet)
}
const tagType = isJsxOpeningElement(node) || isJsxSelfClosingElement(node) && !isJsxIntrinsicIdentifier(node.tagName) ? checkExpression(node.tagName) : undefined;
const tagType = isJsxOpeningElement(node) || isJsxSelfClosingElement(node) && !isJsxIntrinsicTagName(node.tagName) ? checkExpression(node.tagName) : undefined;
if (!tagType) {
return true;
}
@ -33972,7 +33975,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
}
function resolveJsxOpeningLikeElement(node: JsxOpeningLikeElement, candidatesOutArray: Signature[] | undefined, checkMode: CheckMode): Signature {
if (isJsxIntrinsicIdentifier(node.tagName)) {
if (isJsxIntrinsicTagName(node.tagName)) {
const result = getIntrinsicAttributesTypeFromJsxOpeningLikeElement(node);
const fakeSignature = createSignatureForJSXIntrinsic(node, result);
checkTypeAssignableToAndOptionallyElaborate(checkExpressionWithContextualType(node.attributes, getEffectiveFirstArgumentForJsxSignature(fakeSignature, node), /*inferenceContext*/ undefined, CheckMode.Normal), result, node.tagName, node.attributes);
@ -45438,7 +45441,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const isJSDoc = findAncestor(name, or(isJSDocLinkLike, isJSDocNameReference, isJSDocMemberName));
const meaning = isJSDoc ? SymbolFlags.Type | SymbolFlags.Namespace | SymbolFlags.Value : SymbolFlags.Value;
if (name.kind === SyntaxKind.Identifier) {
if (isJSXTagName(name) && isJsxIntrinsicIdentifier(name)) {
if (isJSXTagName(name) && isJsxIntrinsicTagName(name)) {
const symbol = getIntrinsicTagSymbol(name.parent as JsxOpeningLikeElement);
return symbol === unknownSymbol ? undefined : symbol;
}
@ -45685,6 +45688,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
return isMetaProperty(node.parent) ? checkMetaPropertyKeyword(node.parent).symbol : undefined;
case SyntaxKind.MetaProperty:
return checkExpression(node as Expression).symbol;
case SyntaxKind.JsxNamespacedName:
if (isJSXTagName(node) && isJsxIntrinsicTagName(node)) {
const symbol = getIntrinsicTagSymbol(node.parent as JsxOpeningLikeElement);
return symbol === unknownSymbol ? undefined : symbol;
}
// falls through
default:
return undefined;

View File

@ -361,6 +361,7 @@ import {
JsxElement,
JsxEmit,
JsxFragment,
JsxNamespacedName,
JsxOpeningElement,
JsxOpeningLikeElement,
JsxSelfClosingElement,
@ -5828,7 +5829,7 @@ function isQuoteOrBacktick(charCode: number) {
/** @internal */
export function isIntrinsicJsxName(name: __String | string) {
const ch = (name as string).charCodeAt(0);
return (ch >= CharacterCodes.a && ch <= CharacterCodes.z) || stringContains((name as string), "-") || stringContains((name as string), ":");
return (ch >= CharacterCodes.a && ch <= CharacterCodes.z) || stringContains((name as string), "-");
}
const indentStrings: string[] = ["", " "];
@ -10177,12 +10178,12 @@ export function tryGetJSDocSatisfiesTypeNode(node: Node) {
/** @internal */
export function getEscapedTextOfJsxAttributeName(node: JsxAttributeName): __String {
return isIdentifier(node) ? node.escapedText : `${node.namespace.escapedText}:${idText(node.name)}` as __String;
return isIdentifier(node) ? node.escapedText : getEscapedTextOfJsxNamespacedName(node);
}
/** @internal */
export function getTextOfJsxAttributeName(node: JsxAttributeName): string {
return isIdentifier(node) ? idText(node) : `${idText(node.namespace)}:${idText(node.name)}`;
return isIdentifier(node) ? idText(node) : getTextOfJsxNamespacedName(node);
}
/** @internal */
@ -10191,3 +10192,18 @@ export function isJsxAttributeName(node: Node): node is JsxAttributeName {
return kind === SyntaxKind.Identifier
|| kind === SyntaxKind.JsxNamespacedName;
}
/** @internal */
export function getEscapedTextOfJsxNamespacedName(node: JsxNamespacedName): __String {
return `${node.namespace.escapedText}:${idText(node.name)}` as __String;
}
/** @internal */
export function getTextOfJsxNamespacedName(node: JsxNamespacedName) {
return `${idText(node.namespace)}:${idText(node.name)}`;
}
/** @internal */
export function intrinsicTagNameToString(node: Identifier | JsxNamespacedName) {
return isIdentifier(node) ? idText(node) : getTextOfJsxNamespacedName(node);
}

View File

@ -156,6 +156,7 @@ import {
isJsxClosingElement,
isJsxElement,
isJsxFragment,
isJsxNamespacedName,
isJsxOpeningElement,
isJsxOpeningFragment,
isJsxText,
@ -2064,6 +2065,9 @@ export function createLanguageService(
if (isImportMeta(node.parent) && node.parent.name === node) {
return node.parent;
}
if (isJsxNamespacedName(node.parent)) {
return node.parent;
}
return node;
}

View File

@ -34,9 +34,11 @@ tests/cases/compiler/jsxElementType.tsx(91,2): error TS2786: 'ReactNativeFlatLis
tests/cases/compiler/jsxElementType.tsx(95,11): error TS2322: Type '{}' is not assignable to type 'LibraryManagedAttributes<T, {}>'.
tests/cases/compiler/jsxElementType.tsx(98,2): error TS2304: Cannot find name 'Unresolved'.
tests/cases/compiler/jsxElementType.tsx(99,2): error TS2304: Cannot find name 'Unresolved'.
tests/cases/compiler/jsxElementType.tsx(109,19): error TS2322: Type '{ a: string; b: string; }' is not assignable to type '{ a: string; }'.
Property 'b' does not exist on type '{ a: string; }'.
==== tests/cases/compiler/jsxElementType.tsx (18 errors) ====
==== tests/cases/compiler/jsxElementType.tsx (19 errors) ====
/// <reference path="/.lib/react16.d.ts" />
import * as React from "react";
@ -197,4 +199,17 @@ tests/cases/compiler/jsxElementType.tsx(99,2): error TS2304: Cannot find name 'U
<Unresolved foo="abc" />;
~~~~~~~~~~
!!! error TS2304: Cannot find name 'Unresolved'.
declare global {
namespace JSX {
interface IntrinsicElements {
['a:b']: { a: string };
}
}
}
<a:b a="accepted" b="rejected" />;
~
!!! error TS2322: Type '{ a: string; b: string; }' is not assignable to type '{ a: string; }'.
!!! error TS2322: Property 'b' does not exist on type '{ a: string; }'.

View File

@ -98,6 +98,16 @@ function f1<T extends (props: {}) => React.ReactElement<any>>(Component: T) {
<Unresolved />;
<Unresolved foo="abc" />;
declare global {
namespace JSX {
interface IntrinsicElements {
['a:b']: { a: string };
}
}
}
<a:b a="accepted" b="rejected" />;
//// [jsxElementType.js]
@ -231,3 +241,4 @@ function f1(Component) {
}
React.createElement(Unresolved, null);
React.createElement(Unresolved, { foo: "abc" });
React.createElement("a:b", { a: "accepted", b: "rejected" });

View File

@ -49,17 +49,17 @@ type NewReactJSXElementConstructor<P> =
>P : Symbol(P, Decl(jsxElementType.tsx, 16, 35))
declare global {
>global : Symbol(global, Decl(jsxElementType.tsx, 18, 48))
>global : Symbol(global, Decl(jsxElementType.tsx, 18, 48), Decl(jsxElementType.tsx, 98, 25))
namespace JSX {
>JSX : Symbol(JSX, Decl(react16.d.ts, 2493, 12), Decl(jsxElementType.tsx, 20, 16))
>JSX : Symbol(JSX, Decl(react16.d.ts, 2493, 12), Decl(jsxElementType.tsx, 20, 16), Decl(jsxElementType.tsx, 100, 16))
type ElementType = string | NewReactJSXElementConstructor<any>;
>ElementType : Symbol(ElementType, Decl(jsxElementType.tsx, 21, 17))
>NewReactJSXElementConstructor : Symbol(NewReactJSXElementConstructor, Decl(jsxElementType.tsx, 13, 30))
interface IntrinsicElements {
>IntrinsicElements : Symbol(IntrinsicElements, Decl(react16.d.ts, 2514, 86), Decl(jsxElementType.tsx, 22, 67))
>IntrinsicElements : Symbol(IntrinsicElements, Decl(react16.d.ts, 2514, 86), Decl(jsxElementType.tsx, 22, 67), Decl(jsxElementType.tsx, 101, 19))
['my-custom-element']: React.DOMAttributes<unknown>;
>['my-custom-element'] : Symbol(IntrinsicElements['my-custom-element'], Decl(jsxElementType.tsx, 23, 33))
@ -272,3 +272,24 @@ function f1<T extends (props: {}) => React.ReactElement<any>>(Component: T) {
<Unresolved foo="abc" />;
>foo : Symbol(foo, Decl(jsxElementType.tsx, 98, 11))
declare global {
>global : Symbol(global, Decl(jsxElementType.tsx, 18, 48), Decl(jsxElementType.tsx, 98, 25))
namespace JSX {
>JSX : Symbol(JSX, Decl(react16.d.ts, 2493, 12), Decl(jsxElementType.tsx, 20, 16), Decl(jsxElementType.tsx, 100, 16))
interface IntrinsicElements {
>IntrinsicElements : Symbol(IntrinsicElements, Decl(react16.d.ts, 2514, 86), Decl(jsxElementType.tsx, 22, 67), Decl(jsxElementType.tsx, 101, 19))
['a:b']: { a: string };
>['a:b'] : Symbol(IntrinsicElements['a:b'], Decl(jsxElementType.tsx, 102, 35))
>'a:b' : Symbol(IntrinsicElements['a:b'], Decl(jsxElementType.tsx, 102, 35))
>a : Symbol(a, Decl(jsxElementType.tsx, 103, 20))
}
}
}
<a:b a="accepted" b="rejected" />;
>a : Symbol(a, Decl(jsxElementType.tsx, 108, 4))
>b : Symbol(b, Decl(jsxElementType.tsx, 108, 17))

View File

@ -290,3 +290,23 @@ function f1<T extends (props: {}) => React.ReactElement<any>>(Component: T) {
>Unresolved : any
>foo : string
declare global {
>global : any
namespace JSX {
interface IntrinsicElements {
['a:b']: { a: string };
>['a:b'] : { a: string; }
>'a:b' : "a:b"
>a : string
}
}
}
<a:b a="accepted" b="rejected" />;
><a:b a="accepted" b="rejected" /> : JSX.Element
>a : any
>b : any
>a : string
>b : string

View File

@ -1,7 +1,11 @@
tests/cases/compiler/jsxNamespacePrefixIntrinsics.tsx(15,18): error TS2339: Property 'element' does not exist on type 'JSX.IntrinsicElements'.
tests/cases/compiler/jsxNamespacePrefixIntrinsics.tsx(16,30): error TS2322: Type '{ attribute: string; }' is not assignable to type '{ "ns:attribute": string; }'.
Property 'attribute' does not exist on type '{ "ns:attribute": string; }'. Did you mean '"ns:attribute"'?
tests/cases/compiler/jsxNamespacePrefixIntrinsics.tsx(17,30): error TS2322: Type '{ "ns:invalid": string; }' is not assignable to type '{ "ns:attribute": string; }'.
Property 'ns:invalid' does not exist on type '{ "ns:attribute": string; }'.
==== tests/cases/compiler/jsxNamespacePrefixIntrinsics.tsx (1 errors) ====
==== tests/cases/compiler/jsxNamespacePrefixIntrinsics.tsx (3 errors) ====
declare namespace JSX {
interface IntrinsicElements {
"ns:element": {
@ -20,5 +24,11 @@ tests/cases/compiler/jsxNamespacePrefixIntrinsics.tsx(15,18): error TS2339: Prop
~~~~~~~~~~~
!!! error TS2339: Property 'element' does not exist on type 'JSX.IntrinsicElements'.
const invalid2 = <ns:element attribute="nope" />;
~~~~~~~~~
!!! error TS2322: Type '{ attribute: string; }' is not assignable to type '{ "ns:attribute": string; }'.
!!! error TS2322: Property 'attribute' does not exist on type '{ "ns:attribute": string; }'. Did you mean '"ns:attribute"'?
const invalid3 = <ns:element ns:invalid="nope" />;
~~~~~~~~~~
!!! error TS2322: Type '{ "ns:invalid": string; }' is not assignable to type '{ "ns:attribute": string; }'.
!!! error TS2322: Property 'ns:invalid' does not exist on type '{ "ns:attribute": string; }'.

View File

@ -0,0 +1,117 @@
=== /a.tsx ===
// <a:b a="accepted" b="rejected" />;
// ^^^
// | ----------------------------------------------------------------------
// | (property) JSX.IntrinsicElements['a:b']: {
// | a: string;
// | }
// | ----------------------------------------------------------------------
[
{
"marker": {
"fileName": "/a.tsx",
"position": 1,
"name": ""
},
"item": {
"kind": "property",
"kindModifiers": "declare",
"textSpan": {
"start": 1,
"length": 3
},
"displayParts": [
{
"text": "(",
"kind": "punctuation"
},
{
"text": "property",
"kind": "text"
},
{
"text": ")",
"kind": "punctuation"
},
{
"text": " ",
"kind": "space"
},
{
"text": "JSX",
"kind": "moduleName"
},
{
"text": ".",
"kind": "punctuation"
},
{
"text": "IntrinsicElements",
"kind": "interfaceName"
},
{
"text": "[",
"kind": "punctuation"
},
{
"text": "'a:b'",
"kind": "stringLiteral"
},
{
"text": "]",
"kind": "punctuation"
},
{
"text": ":",
"kind": "punctuation"
},
{
"text": " ",
"kind": "space"
},
{
"text": "{",
"kind": "punctuation"
},
{
"text": "\n",
"kind": "lineBreak"
},
{
"text": " ",
"kind": "space"
},
{
"text": "a",
"kind": "propertyName"
},
{
"text": ":",
"kind": "punctuation"
},
{
"text": " ",
"kind": "space"
},
{
"text": "string",
"kind": "keyword"
},
{
"text": ";",
"kind": "punctuation"
},
{
"text": "\n",
"kind": "lineBreak"
},
{
"text": "}",
"kind": "punctuation"
}
],
"documentation": []
}
}
]

View File

@ -99,3 +99,13 @@ function f1<T extends (props: {}) => React.ReactElement<any>>(Component: T) {
<Unresolved />;
<Unresolved foo="abc" />;
declare global {
namespace JSX {
interface IntrinsicElements {
['a:b']: { a: string };
}
}
}
<a:b a="accepted" b="rejected" />;

View File

@ -0,0 +1,13 @@
/// <reference path="fourslash.ts" />
// @jsx: react
// @Filename: /types.d.ts
////declare namespace JSX {
//// interface IntrinsicElements { ['a:b']: { a: string }; }
////}
// @filename: /a.tsx
////</**/a:b a="accepted" b="rejected" />;
verify.baselineQuickInfo();