Skip to content

Commit

Permalink
feat(7411): Resolve intrinsics elements by JSX namespaced tag names (#…
Browse files Browse the repository at this point in the history
  • Loading branch information
a-tarasyuk authored Apr 17, 2023
1 parent f8b3ea7 commit 378ffa4
Show file tree
Hide file tree
Showing 12 changed files with 273 additions and 22 deletions.
5 changes: 5 additions & 0 deletions src/compiler/binder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ import {
getEnclosingBlockScopeContainer,
getErrorSpanForNode,
getEscapedTextOfIdentifierOrLiteral,
getEscapedTextOfJsxAttributeName,
getExpandoInitializer,
getHostSignatureFromJSDoc,
getImmediatelyInvokedFunctionExpression,
Expand Down Expand Up @@ -171,6 +172,7 @@ import {
isJSDocTemplateTag,
isJSDocTypeAlias,
isJsonSourceFile,
isJsxNamespacedName,
isLeftHandSideExpression,
isLogicalOrCoalescingAssignmentExpression,
isLogicalOrCoalescingAssignmentOperator,
Expand Down Expand Up @@ -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) {
Expand Down
37 changes: 23 additions & 14 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,6 +276,7 @@ import {
getErrorSpanForNode,
getEscapedTextOfIdentifierOrLiteral,
getEscapedTextOfJsxAttributeName,
getEscapedTextOfJsxNamespacedName,
getESModuleInterop,
getExpandoInitializer,
getExportAssignmentExpression,
Expand Down Expand Up @@ -429,6 +430,7 @@ import {
InternalSymbolName,
IntersectionType,
IntersectionTypeNode,
intrinsicTagNameToString,
IntrinsicType,
introducesArgumentsExoticObject,
isAccessExpression,
Expand Down Expand Up @@ -782,6 +784,7 @@ import {
JsxExpression,
JsxFlags,
JsxFragment,
JsxNamespacedName,
JsxOpeningElement,
JsxOpeningFragment,
JsxOpeningLikeElement,
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
Expand Down
22 changes: 19 additions & 3 deletions src/compiler/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -361,6 +361,7 @@ import {
JsxElement,
JsxEmit,
JsxFragment,
JsxNamespacedName,
JsxOpeningElement,
JsxOpeningLikeElement,
JsxSelfClosingElement,
Expand Down Expand Up @@ -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[] = ["", " "];
Expand Down Expand Up @@ -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 */
Expand All @@ -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);
}
4 changes: 4 additions & 0 deletions src/services/services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ import {
isJsxClosingElement,
isJsxElement,
isJsxFragment,
isJsxNamespacedName,
isJsxOpeningElement,
isJsxOpeningFragment,
isJsxText,
Expand Down Expand Up @@ -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;
}

Expand Down
17 changes: 16 additions & 1 deletion tests/baselines/reference/jsxElementType.errors.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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; }'.

11 changes: 11 additions & 0 deletions tests/baselines/reference/jsxElementType.js
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -231,3 +241,4 @@ function f1(Component) {
}
React.createElement(Unresolved, null);
React.createElement(Unresolved, { foo: "abc" });
React.createElement("a:b", { a: "accepted", b: "rejected" });
27 changes: 24 additions & 3 deletions tests/baselines/reference/jsxElementType.symbols
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down Expand Up @@ -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))

20 changes: 20 additions & 0 deletions tests/baselines/reference/jsxElementType.types
Original file line number Diff line number Diff line change
Expand Up @@ -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

Original file line number Diff line number Diff line change
@@ -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": {
Expand All @@ -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; }'.

Loading

0 comments on commit 378ffa4

Please sign in to comment.