Skip to content

Commit 378ffa4

Browse files
authored
feat(7411): Resolve intrinsics elements by JSX namespaced tag names (#53799)
1 parent f8b3ea7 commit 378ffa4

12 files changed

+273
-22
lines changed

src/compiler/binder.ts

+5
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ import {
8989
getEnclosingBlockScopeContainer,
9090
getErrorSpanForNode,
9191
getEscapedTextOfIdentifierOrLiteral,
92+
getEscapedTextOfJsxAttributeName,
9293
getExpandoInitializer,
9394
getHostSignatureFromJSDoc,
9495
getImmediatelyInvokedFunctionExpression,
@@ -171,6 +172,7 @@ import {
171172
isJSDocTemplateTag,
172173
isJSDocTypeAlias,
173174
isJsonSourceFile,
175+
isJsxNamespacedName,
174176
isLeftHandSideExpression,
175177
isLogicalOrCoalescingAssignmentExpression,
176178
isLogicalOrCoalescingAssignmentOperator,
@@ -679,6 +681,9 @@ function createBinder(): (file: SourceFile, options: CompilerOptions) => void {
679681
const containingClassSymbol = containingClass.symbol;
680682
return getSymbolNameForPrivateIdentifier(containingClassSymbol, name.escapedText);
681683
}
684+
if (isJsxNamespacedName(name)) {
685+
return getEscapedTextOfJsxAttributeName(name);
686+
}
682687
return isPropertyNameLiteral(name) ? getEscapedTextOfIdentifierOrLiteral(name) : undefined;
683688
}
684689
switch (node.kind) {

src/compiler/checker.ts

+23-14
Original file line numberDiff line numberDiff line change
@@ -276,6 +276,7 @@ import {
276276
getErrorSpanForNode,
277277
getEscapedTextOfIdentifierOrLiteral,
278278
getEscapedTextOfJsxAttributeName,
279+
getEscapedTextOfJsxNamespacedName,
279280
getESModuleInterop,
280281
getExpandoInitializer,
281282
getExportAssignmentExpression,
@@ -429,6 +430,7 @@ import {
429430
InternalSymbolName,
430431
IntersectionType,
431432
IntersectionTypeNode,
433+
intrinsicTagNameToString,
432434
IntrinsicType,
433435
introducesArgumentsExoticObject,
434436
isAccessExpression,
@@ -782,6 +784,7 @@ import {
782784
JsxExpression,
783785
JsxFlags,
784786
JsxFragment,
787+
JsxNamespacedName,
785788
JsxOpeningElement,
786789
JsxOpeningFragment,
787790
JsxOpeningLikeElement,
@@ -29597,7 +29600,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
2959729600
}
2959829601

2959929602
function getStaticTypeOfReferencedJsxConstructor(context: JsxOpeningLikeElement) {
29600-
if (isJsxIntrinsicIdentifier(context.tagName)) {
29603+
if (isJsxIntrinsicTagName(context.tagName)) {
2960129604
const result = getIntrinsicAttributesTypeFromJsxOpeningLikeElement(context);
2960229605
const fakeSignature = createSignatureForJSXIntrinsic(context, result);
2960329606
return getOrCreateTypeFromSignature(fakeSignature);
@@ -30317,7 +30320,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3031730320
checkJsxOpeningLikeElementOrOpeningFragment(node.openingElement);
3031830321

3031930322
// Perform resolution on the closing tag so that rename/go to definition/etc work
30320-
if (isJsxIntrinsicIdentifier(node.closingElement.tagName)) {
30323+
if (isJsxIntrinsicTagName(node.closingElement.tagName)) {
3032130324
getIntrinsicTagSymbol(node.closingElement);
3032230325
}
3032330326
else {
@@ -30357,8 +30360,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3035730360
/**
3035830361
* Returns true iff React would emit this tag name as a string rather than an identifier or qualified name
3035930362
*/
30360-
function isJsxIntrinsicIdentifier(tagName: JsxTagNameExpression): tagName is Identifier {
30361-
return tagName.kind === SyntaxKind.Identifier && isIntrinsicJsxName(tagName.escapedText);
30363+
function isJsxIntrinsicTagName(tagName: Node): tagName is Identifier | JsxNamespacedName {
30364+
return isIdentifier(tagName) && isIntrinsicJsxName(tagName.escapedText) || isJsxNamespacedName(tagName);
3036230365
}
3036330366

3036430367
function checkJsxAttribute(node: JsxAttribute, checkMode?: CheckMode) {
@@ -30563,8 +30566,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3056330566
const intrinsicElementsType = getJsxType(JsxNames.IntrinsicElements, node);
3056430567
if (!isErrorType(intrinsicElementsType)) {
3056530568
// Property case
30566-
if (!isIdentifier(node.tagName)) return Debug.fail();
30567-
const intrinsicProp = getPropertyOfType(intrinsicElementsType, node.tagName.escapedText);
30569+
if (!isIdentifier(node.tagName) && !isJsxNamespacedName(node.tagName)) return Debug.fail();
30570+
const intrinsicProp = getPropertyOfType(intrinsicElementsType, isJsxNamespacedName(node.tagName) ? getEscapedTextOfJsxNamespacedName(node.tagName) : node.tagName.escapedText);
3056830571
if (intrinsicProp) {
3056930572
links.jsxFlags |= JsxFlags.IntrinsicNamedElement;
3057030573
return links.resolvedSymbol = intrinsicProp;
@@ -30578,7 +30581,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3057830581
}
3057930582

3058030583
// Wasn't found
30581-
error(node, Diagnostics.Property_0_does_not_exist_on_type_1, idText(node.tagName), "JSX." + JsxNames.IntrinsicElements);
30584+
error(node, Diagnostics.Property_0_does_not_exist_on_type_1, intrinsicTagNameToString(node.tagName), "JSX." + JsxNames.IntrinsicElements);
3058230585
return links.resolvedSymbol = unknownSymbol;
3058330586
}
3058430587
else {
@@ -30787,7 +30790,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3078730790
* @param node an intrinsic JSX opening-like element
3078830791
*/
3078930792
function getIntrinsicAttributesTypeFromJsxOpeningLikeElement(node: JsxOpeningLikeElement): Type {
30790-
Debug.assert(isJsxIntrinsicIdentifier(node.tagName));
30793+
Debug.assert(isJsxIntrinsicTagName(node.tagName));
3079130794
const links = getNodeLinks(node);
3079230795
if (!links.resolvedJsxElementAttributesType) {
3079330796
const symbol = getIntrinsicTagSymbol(node);
@@ -30900,8 +30903,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3090030903
const elementTypeConstraint = getJsxElementTypeTypeAt(jsxOpeningLikeNode);
3090130904
if (elementTypeConstraint !== undefined) {
3090230905
const tagName = jsxOpeningLikeNode.tagName;
30903-
const tagType = isJsxIntrinsicIdentifier(tagName)
30904-
? getStringLiteralType(unescapeLeadingUnderscores(tagName.escapedText))
30906+
const tagType = isJsxIntrinsicTagName(tagName)
30907+
? getStringLiteralType(intrinsicTagNameToString(tagName))
3090530908
: checkExpression(tagName);
3090630909
checkTypeRelatedTo(tagType, elementTypeConstraint, assignableRelation, tagName, Diagnostics.Its_type_0_is_not_a_valid_JSX_element_type, () => {
3090730910
const componentName = getTextOfNode(tagName);
@@ -32521,7 +32524,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3252132524
}
3252232525

3252332526
function getJsxReferenceKind(node: JsxOpeningLikeElement): JsxReferenceKind {
32524-
if (isJsxIntrinsicIdentifier(node.tagName)) {
32527+
if (isJsxIntrinsicTagName(node.tagName)) {
3252532528
return JsxReferenceKind.Mixed;
3252632529
}
3252732530
const tagType = getApparentType(checkExpression(node.tagName));
@@ -32568,7 +32571,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3256832571
if (getJsxNamespaceContainerForImplicitImport(node)) {
3256932572
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)
3257032573
}
32571-
const tagType = isJsxOpeningElement(node) || isJsxSelfClosingElement(node) && !isJsxIntrinsicIdentifier(node.tagName) ? checkExpression(node.tagName) : undefined;
32574+
const tagType = isJsxOpeningElement(node) || isJsxSelfClosingElement(node) && !isJsxIntrinsicTagName(node.tagName) ? checkExpression(node.tagName) : undefined;
3257232575
if (!tagType) {
3257332576
return true;
3257432577
}
@@ -33972,7 +33975,7 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3397233975
}
3397333976

3397433977
function resolveJsxOpeningLikeElement(node: JsxOpeningLikeElement, candidatesOutArray: Signature[] | undefined, checkMode: CheckMode): Signature {
33975-
if (isJsxIntrinsicIdentifier(node.tagName)) {
33978+
if (isJsxIntrinsicTagName(node.tagName)) {
3397633979
const result = getIntrinsicAttributesTypeFromJsxOpeningLikeElement(node);
3397733980
const fakeSignature = createSignatureForJSXIntrinsic(node, result);
3397833981
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 {
4543845441
const isJSDoc = findAncestor(name, or(isJSDocLinkLike, isJSDocNameReference, isJSDocMemberName));
4543945442
const meaning = isJSDoc ? SymbolFlags.Type | SymbolFlags.Namespace | SymbolFlags.Value : SymbolFlags.Value;
4544045443
if (name.kind === SyntaxKind.Identifier) {
45441-
if (isJSXTagName(name) && isJsxIntrinsicIdentifier(name)) {
45444+
if (isJSXTagName(name) && isJsxIntrinsicTagName(name)) {
4544245445
const symbol = getIntrinsicTagSymbol(name.parent as JsxOpeningLikeElement);
4544345446
return symbol === unknownSymbol ? undefined : symbol;
4544445447
}
@@ -45685,6 +45688,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
4568545688
return isMetaProperty(node.parent) ? checkMetaPropertyKeyword(node.parent).symbol : undefined;
4568645689
case SyntaxKind.MetaProperty:
4568745690
return checkExpression(node as Expression).symbol;
45691+
case SyntaxKind.JsxNamespacedName:
45692+
if (isJSXTagName(node) && isJsxIntrinsicTagName(node)) {
45693+
const symbol = getIntrinsicTagSymbol(node.parent as JsxOpeningLikeElement);
45694+
return symbol === unknownSymbol ? undefined : symbol;
45695+
}
45696+
// falls through
4568845697

4568945698
default:
4569045699
return undefined;

src/compiler/utilities.ts

+19-3
Original file line numberDiff line numberDiff line change
@@ -361,6 +361,7 @@ import {
361361
JsxElement,
362362
JsxEmit,
363363
JsxFragment,
364+
JsxNamespacedName,
364365
JsxOpeningElement,
365366
JsxOpeningLikeElement,
366367
JsxSelfClosingElement,
@@ -5828,7 +5829,7 @@ function isQuoteOrBacktick(charCode: number) {
58285829
/** @internal */
58295830
export function isIntrinsicJsxName(name: __String | string) {
58305831
const ch = (name as string).charCodeAt(0);
5831-
return (ch >= CharacterCodes.a && ch <= CharacterCodes.z) || stringContains((name as string), "-") || stringContains((name as string), ":");
5832+
return (ch >= CharacterCodes.a && ch <= CharacterCodes.z) || stringContains((name as string), "-");
58325833
}
58335834

58345835
const indentStrings: string[] = ["", " "];
@@ -10177,12 +10178,12 @@ export function tryGetJSDocSatisfiesTypeNode(node: Node) {
1017710178

1017810179
/** @internal */
1017910180
export function getEscapedTextOfJsxAttributeName(node: JsxAttributeName): __String {
10180-
return isIdentifier(node) ? node.escapedText : `${node.namespace.escapedText}:${idText(node.name)}` as __String;
10181+
return isIdentifier(node) ? node.escapedText : getEscapedTextOfJsxNamespacedName(node);
1018110182
}
1018210183

1018310184
/** @internal */
1018410185
export function getTextOfJsxAttributeName(node: JsxAttributeName): string {
10185-
return isIdentifier(node) ? idText(node) : `${idText(node.namespace)}:${idText(node.name)}`;
10186+
return isIdentifier(node) ? idText(node) : getTextOfJsxNamespacedName(node);
1018610187
}
1018710188

1018810189
/** @internal */
@@ -10191,3 +10192,18 @@ export function isJsxAttributeName(node: Node): node is JsxAttributeName {
1019110192
return kind === SyntaxKind.Identifier
1019210193
|| kind === SyntaxKind.JsxNamespacedName;
1019310194
}
10195+
10196+
/** @internal */
10197+
export function getEscapedTextOfJsxNamespacedName(node: JsxNamespacedName): __String {
10198+
return `${node.namespace.escapedText}:${idText(node.name)}` as __String;
10199+
}
10200+
10201+
/** @internal */
10202+
export function getTextOfJsxNamespacedName(node: JsxNamespacedName) {
10203+
return `${idText(node.namespace)}:${idText(node.name)}`;
10204+
}
10205+
10206+
/** @internal */
10207+
export function intrinsicTagNameToString(node: Identifier | JsxNamespacedName) {
10208+
return isIdentifier(node) ? idText(node) : getTextOfJsxNamespacedName(node);
10209+
}

src/services/services.ts

+4
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ import {
156156
isJsxClosingElement,
157157
isJsxElement,
158158
isJsxFragment,
159+
isJsxNamespacedName,
159160
isJsxOpeningElement,
160161
isJsxOpeningFragment,
161162
isJsxText,
@@ -2064,6 +2065,9 @@ export function createLanguageService(
20642065
if (isImportMeta(node.parent) && node.parent.name === node) {
20652066
return node.parent;
20662067
}
2068+
if (isJsxNamespacedName(node.parent)) {
2069+
return node.parent;
2070+
}
20672071
return node;
20682072
}
20692073

tests/baselines/reference/jsxElementType.errors.txt

+16-1
Original file line numberDiff line numberDiff line change
@@ -34,9 +34,11 @@ tests/cases/compiler/jsxElementType.tsx(91,2): error TS2786: 'ReactNativeFlatLis
3434
tests/cases/compiler/jsxElementType.tsx(95,11): error TS2322: Type '{}' is not assignable to type 'LibraryManagedAttributes<T, {}>'.
3535
tests/cases/compiler/jsxElementType.tsx(98,2): error TS2304: Cannot find name 'Unresolved'.
3636
tests/cases/compiler/jsxElementType.tsx(99,2): error TS2304: Cannot find name 'Unresolved'.
37+
tests/cases/compiler/jsxElementType.tsx(109,19): error TS2322: Type '{ a: string; b: string; }' is not assignable to type '{ a: string; }'.
38+
Property 'b' does not exist on type '{ a: string; }'.
3739

3840

39-
==== tests/cases/compiler/jsxElementType.tsx (18 errors) ====
41+
==== tests/cases/compiler/jsxElementType.tsx (19 errors) ====
4042
/// <reference path="/.lib/react16.d.ts" />
4143
import * as React from "react";
4244

@@ -197,4 +199,17 @@ tests/cases/compiler/jsxElementType.tsx(99,2): error TS2304: Cannot find name 'U
197199
<Unresolved foo="abc" />;
198200
~~~~~~~~~~
199201
!!! error TS2304: Cannot find name 'Unresolved'.
202+
203+
declare global {
204+
namespace JSX {
205+
interface IntrinsicElements {
206+
['a:b']: { a: string };
207+
}
208+
}
209+
}
210+
211+
<a:b a="accepted" b="rejected" />;
212+
~
213+
!!! error TS2322: Type '{ a: string; b: string; }' is not assignable to type '{ a: string; }'.
214+
!!! error TS2322: Property 'b' does not exist on type '{ a: string; }'.
200215

tests/baselines/reference/jsxElementType.js

+11
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,16 @@ function f1<T extends (props: {}) => React.ReactElement<any>>(Component: T) {
9898

9999
<Unresolved />;
100100
<Unresolved foo="abc" />;
101+
102+
declare global {
103+
namespace JSX {
104+
interface IntrinsicElements {
105+
['a:b']: { a: string };
106+
}
107+
}
108+
}
109+
110+
<a:b a="accepted" b="rejected" />;
101111

102112

103113
//// [jsxElementType.js]
@@ -231,3 +241,4 @@ function f1(Component) {
231241
}
232242
React.createElement(Unresolved, null);
233243
React.createElement(Unresolved, { foo: "abc" });
244+
React.createElement("a:b", { a: "accepted", b: "rejected" });

tests/baselines/reference/jsxElementType.symbols

+24-3
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,17 @@ type NewReactJSXElementConstructor<P> =
4949
>P : Symbol(P, Decl(jsxElementType.tsx, 16, 35))
5050

5151
declare global {
52-
>global : Symbol(global, Decl(jsxElementType.tsx, 18, 48))
52+
>global : Symbol(global, Decl(jsxElementType.tsx, 18, 48), Decl(jsxElementType.tsx, 98, 25))
5353

5454
namespace JSX {
55-
>JSX : Symbol(JSX, Decl(react16.d.ts, 2493, 12), Decl(jsxElementType.tsx, 20, 16))
55+
>JSX : Symbol(JSX, Decl(react16.d.ts, 2493, 12), Decl(jsxElementType.tsx, 20, 16), Decl(jsxElementType.tsx, 100, 16))
5656

5757
type ElementType = string | NewReactJSXElementConstructor<any>;
5858
>ElementType : Symbol(ElementType, Decl(jsxElementType.tsx, 21, 17))
5959
>NewReactJSXElementConstructor : Symbol(NewReactJSXElementConstructor, Decl(jsxElementType.tsx, 13, 30))
6060

6161
interface IntrinsicElements {
62-
>IntrinsicElements : Symbol(IntrinsicElements, Decl(react16.d.ts, 2514, 86), Decl(jsxElementType.tsx, 22, 67))
62+
>IntrinsicElements : Symbol(IntrinsicElements, Decl(react16.d.ts, 2514, 86), Decl(jsxElementType.tsx, 22, 67), Decl(jsxElementType.tsx, 101, 19))
6363

6464
['my-custom-element']: React.DOMAttributes<unknown>;
6565
>['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) {
272272
<Unresolved foo="abc" />;
273273
>foo : Symbol(foo, Decl(jsxElementType.tsx, 98, 11))
274274

275+
declare global {
276+
>global : Symbol(global, Decl(jsxElementType.tsx, 18, 48), Decl(jsxElementType.tsx, 98, 25))
277+
278+
namespace JSX {
279+
>JSX : Symbol(JSX, Decl(react16.d.ts, 2493, 12), Decl(jsxElementType.tsx, 20, 16), Decl(jsxElementType.tsx, 100, 16))
280+
281+
interface IntrinsicElements {
282+
>IntrinsicElements : Symbol(IntrinsicElements, Decl(react16.d.ts, 2514, 86), Decl(jsxElementType.tsx, 22, 67), Decl(jsxElementType.tsx, 101, 19))
283+
284+
['a:b']: { a: string };
285+
>['a:b'] : Symbol(IntrinsicElements['a:b'], Decl(jsxElementType.tsx, 102, 35))
286+
>'a:b' : Symbol(IntrinsicElements['a:b'], Decl(jsxElementType.tsx, 102, 35))
287+
>a : Symbol(a, Decl(jsxElementType.tsx, 103, 20))
288+
}
289+
}
290+
}
291+
292+
<a:b a="accepted" b="rejected" />;
293+
>a : Symbol(a, Decl(jsxElementType.tsx, 108, 4))
294+
>b : Symbol(b, Decl(jsxElementType.tsx, 108, 17))
295+

tests/baselines/reference/jsxElementType.types

+20
Original file line numberDiff line numberDiff line change
@@ -290,3 +290,23 @@ function f1<T extends (props: {}) => React.ReactElement<any>>(Component: T) {
290290
>Unresolved : any
291291
>foo : string
292292

293+
declare global {
294+
>global : any
295+
296+
namespace JSX {
297+
interface IntrinsicElements {
298+
['a:b']: { a: string };
299+
>['a:b'] : { a: string; }
300+
>'a:b' : "a:b"
301+
>a : string
302+
}
303+
}
304+
}
305+
306+
<a:b a="accepted" b="rejected" />;
307+
><a:b a="accepted" b="rejected" /> : JSX.Element
308+
>a : any
309+
>b : any
310+
>a : string
311+
>b : string
312+

tests/baselines/reference/jsxNamespacePrefixIntrinsics.errors.txt

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
tests/cases/compiler/jsxNamespacePrefixIntrinsics.tsx(15,18): error TS2339: Property 'element' does not exist on type 'JSX.IntrinsicElements'.
2+
tests/cases/compiler/jsxNamespacePrefixIntrinsics.tsx(16,30): error TS2322: Type '{ attribute: string; }' is not assignable to type '{ "ns:attribute": string; }'.
3+
Property 'attribute' does not exist on type '{ "ns:attribute": string; }'. Did you mean '"ns:attribute"'?
4+
tests/cases/compiler/jsxNamespacePrefixIntrinsics.tsx(17,30): error TS2322: Type '{ "ns:invalid": string; }' is not assignable to type '{ "ns:attribute": string; }'.
5+
Property 'ns:invalid' does not exist on type '{ "ns:attribute": string; }'.
26

37

4-
==== tests/cases/compiler/jsxNamespacePrefixIntrinsics.tsx (1 errors) ====
8+
==== tests/cases/compiler/jsxNamespacePrefixIntrinsics.tsx (3 errors) ====
59
declare namespace JSX {
610
interface IntrinsicElements {
711
"ns:element": {
@@ -20,5 +24,11 @@ tests/cases/compiler/jsxNamespacePrefixIntrinsics.tsx(15,18): error TS2339: Prop
2024
~~~~~~~~~~~
2125
!!! error TS2339: Property 'element' does not exist on type 'JSX.IntrinsicElements'.
2226
const invalid2 = <ns:element attribute="nope" />;
27+
~~~~~~~~~
28+
!!! error TS2322: Type '{ attribute: string; }' is not assignable to type '{ "ns:attribute": string; }'.
29+
!!! error TS2322: Property 'attribute' does not exist on type '{ "ns:attribute": string; }'. Did you mean '"ns:attribute"'?
2330
const invalid3 = <ns:element ns:invalid="nope" />;
31+
~~~~~~~~~~
32+
!!! error TS2322: Type '{ "ns:invalid": string; }' is not assignable to type '{ "ns:attribute": string; }'.
33+
!!! error TS2322: Property 'ns:invalid' does not exist on type '{ "ns:attribute": string; }'.
2434

0 commit comments

Comments
 (0)