Skip to content

Commit db0d8e0

Browse files
authored
Fix 8549: Using variable as Jsx tagname (#9337)
* Parse JSXElement's name as property access instead of just entity name. So when one accesses property of the class through this, checker will check correctly * wip - just resolve to any type for now * Resolve string type to anytype and look up property in intrinsicElementsType of Jsx * Add tests and update baselines * Remove unneccessary comment * wip-address PR * Address PR * Add tets and update baselines * Fix linting error
1 parent 2aa1d71 commit db0d8e0

40 files changed

+836
-24
lines changed

src/compiler/checker.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9657,8 +9657,9 @@ namespace ts {
96579657
/**
96589658
* Returns true iff React would emit this tag name as a string rather than an identifier or qualified name
96599659
*/
9660-
function isJsxIntrinsicIdentifier(tagName: Identifier | QualifiedName) {
9661-
if (tagName.kind === SyntaxKind.QualifiedName) {
9660+
function isJsxIntrinsicIdentifier(tagName: JsxTagNameExpression) {
9661+
// TODO (yuisu): comment
9662+
if (tagName.kind === SyntaxKind.PropertyAccessExpression || tagName.kind === SyntaxKind.ThisKeyword) {
96629663
return false;
96639664
}
96649665
else {
@@ -9854,6 +9855,29 @@ namespace ts {
98549855
}));
98559856
}
98569857

9858+
// If the elemType is a string type, we have to return anyType to prevent an error downstream as we will try to find construct or call signature of the type
9859+
if (elemType.flags & TypeFlags.String) {
9860+
return anyType;
9861+
}
9862+
else if (elemType.flags & TypeFlags.StringLiteral) {
9863+
// If the elemType is a stringLiteral type, we can then provide a check to make sure that the string literal type is one of the Jsx intrinsic element type
9864+
const intrinsicElementsType = getJsxType(JsxNames.IntrinsicElements);
9865+
if (intrinsicElementsType !== unknownType) {
9866+
const stringLiteralTypeName = (<StringLiteralType>elemType).text;
9867+
const intrinsicProp = getPropertyOfType(intrinsicElementsType, stringLiteralTypeName);
9868+
if (intrinsicProp) {
9869+
return getTypeOfSymbol(intrinsicProp);
9870+
}
9871+
const indexSignatureType = getIndexTypeOfType(intrinsicElementsType, IndexKind.String);
9872+
if (indexSignatureType) {
9873+
return indexSignatureType;
9874+
}
9875+
error(node, Diagnostics.Property_0_does_not_exist_on_type_1, stringLiteralTypeName, "JSX." + JsxNames.IntrinsicElements);
9876+
}
9877+
// If we need to report an error, we already done so here. So just return any to prevent any more error downstream
9878+
return anyType;
9879+
}
9880+
98579881
// Get the element instance type (the result of newing or invoking this tag)
98589882
const elemInstanceType = getJsxElementInstanceType(node, elemType);
98599883

src/compiler/emitter.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1219,7 +1219,7 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
12191219
function jsxEmitReact(node: JsxElement | JsxSelfClosingElement) {
12201220
/// Emit a tag name, which is either '"div"' for lower-cased names, or
12211221
/// 'Div' for upper-cased or dotted names
1222-
function emitTagName(name: Identifier | QualifiedName) {
1222+
function emitTagName(name: LeftHandSideExpression) {
12231223
if (name.kind === SyntaxKind.Identifier && isIntrinsicJsxName((<Identifier>name).text)) {
12241224
write('"');
12251225
emit(name);

src/compiler/parser.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3576,7 +3576,7 @@ namespace ts {
35763576
return finishNode(node);
35773577
}
35783578

3579-
function tagNamesAreEquivalent(lhs: EntityName, rhs: EntityName): boolean {
3579+
function tagNamesAreEquivalent(lhs: JsxTagNameExpression, rhs: JsxTagNameExpression): boolean {
35803580
if (lhs.kind !== rhs.kind) {
35813581
return false;
35823582
}
@@ -3585,8 +3585,15 @@ namespace ts {
35853585
return (<Identifier>lhs).text === (<Identifier>rhs).text;
35863586
}
35873587

3588-
return (<QualifiedName>lhs).right.text === (<QualifiedName>rhs).right.text &&
3589-
tagNamesAreEquivalent((<QualifiedName>lhs).left, (<QualifiedName>rhs).left);
3588+
if (lhs.kind === SyntaxKind.ThisKeyword) {
3589+
return true;
3590+
}
3591+
3592+
// If we are at this statement then we must have PropertyAccessExpression and because tag name in Jsx element can only
3593+
// take forms of JsxTagNameExpression which includes an identifier, "this" expression, or another propertyAccessExpression
3594+
// it is safe to case the expression property as such. See parseJsxElementName for how we parse tag name in Jsx element
3595+
return (<PropertyAccessExpression>lhs).name.text === (<PropertyAccessExpression>rhs).name.text &&
3596+
tagNamesAreEquivalent((<PropertyAccessExpression>lhs).expression as JsxTagNameExpression, (<PropertyAccessExpression>rhs).expression as JsxTagNameExpression);
35903597
}
35913598

35923599

@@ -3654,7 +3661,7 @@ namespace ts {
36543661
Debug.fail("Unknown JSX child kind " + token);
36553662
}
36563663

3657-
function parseJsxChildren(openingTagName: EntityName): NodeArray<JsxChild> {
3664+
function parseJsxChildren(openingTagName: LeftHandSideExpression): NodeArray<JsxChild> {
36583665
const result = <NodeArray<JsxChild>>[];
36593666
result.pos = scanner.getStartPos();
36603667
const saveParsingContext = parsingContext;
@@ -3717,17 +3724,22 @@ namespace ts {
37173724
return finishNode(node);
37183725
}
37193726

3720-
function parseJsxElementName(): EntityName {
3727+
function parseJsxElementName(): JsxTagNameExpression {
37213728
scanJsxIdentifier();
3722-
let elementName: EntityName = parseIdentifierName();
3729+
// JsxElement can have name in the form of
3730+
// propertyAccessExpression
3731+
// primaryExpression in the form of an identifier and "this" keyword
3732+
// We can't just simply use parseLeftHandSideExpressionOrHigher because then we will start consider class,function etc as a keyword
3733+
// We only want to consider "this" as a primaryExpression
3734+
let expression: JsxTagNameExpression = token === SyntaxKind.ThisKeyword ?
3735+
parseTokenNode<PrimaryExpression>() : parseIdentifierName();
37233736
while (parseOptional(SyntaxKind.DotToken)) {
3724-
scanJsxIdentifier();
3725-
const node: QualifiedName = <QualifiedName>createNode(SyntaxKind.QualifiedName, elementName.pos); // !!!
3726-
node.left = elementName;
3727-
node.right = parseIdentifierName();
3728-
elementName = finishNode(node);
3737+
const propertyAccess: PropertyAccessExpression = <PropertyAccessExpression>createNode(SyntaxKind.PropertyAccessExpression, expression.pos);
3738+
propertyAccess.expression = expression;
3739+
propertyAccess.name = parseRightSideOfDot(/*allowIdentifierNames*/ true);
3740+
expression = finishNode(propertyAccess);
37293741
}
3730-
return elementName;
3742+
return expression;
37313743
}
37323744

37333745
function parseJsxExpression(inExpressionContext: boolean): JsxExpression {

src/compiler/types.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1042,11 +1042,13 @@ namespace ts {
10421042
closingElement: JsxClosingElement;
10431043
}
10441044

1045+
export type JsxTagNameExpression = PrimaryExpression | PropertyAccessExpression;
1046+
10451047
/// The opening element of a <Tag>...</Tag> JsxElement
10461048
// @kind(SyntaxKind.JsxOpeningElement)
10471049
export interface JsxOpeningElement extends Expression {
10481050
_openingElementBrand?: any;
1049-
tagName: EntityName;
1051+
tagName: JsxTagNameExpression;
10501052
attributes: NodeArray<JsxAttribute | JsxSpreadAttribute>;
10511053
}
10521054

@@ -1073,7 +1075,7 @@ namespace ts {
10731075

10741076
// @kind(SyntaxKind.JsxClosingElement)
10751077
export interface JsxClosingElement extends Node {
1076-
tagName: EntityName;
1078+
tagName: JsxTagNameExpression;
10771079
}
10781080

10791081
// @kind(SyntaxKind.JsxExpression)
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
//// [tsxDynamicTagName1.tsx]
2+
3+
var CustomTag = "h1";
4+
<CustomTag> Hello World </CustomTag> // No error
5+
6+
//// [tsxDynamicTagName1.jsx]
7+
var CustomTag = "h1";
8+
<CustomTag> Hello World </CustomTag>; // No error
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
=== tests/cases/conformance/jsx/tsxDynamicTagName1.tsx ===
2+
3+
var CustomTag = "h1";
4+
>CustomTag : Symbol(CustomTag, Decl(tsxDynamicTagName1.tsx, 1, 3))
5+
6+
<CustomTag> Hello World </CustomTag> // No error
7+
>CustomTag : Symbol(CustomTag, Decl(tsxDynamicTagName1.tsx, 1, 3))
8+
>CustomTag : Symbol(CustomTag, Decl(tsxDynamicTagName1.tsx, 1, 3))
9+
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
=== tests/cases/conformance/jsx/tsxDynamicTagName1.tsx ===
2+
3+
var CustomTag = "h1";
4+
>CustomTag : string
5+
>"h1" : string
6+
7+
<CustomTag> Hello World </CustomTag> // No error
8+
><CustomTag> Hello World </CustomTag> : any
9+
>CustomTag : string
10+
>CustomTag : string
11+
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
tests/cases/conformance/jsx/tsxDynamicTagName2.tsx(10,1): error TS2339: Property 'customTag' does not exist on type 'JSX.IntrinsicElements'.
2+
tests/cases/conformance/jsx/tsxDynamicTagName2.tsx(10,25): error TS2339: Property 'customTag' does not exist on type 'JSX.IntrinsicElements'.
3+
4+
5+
==== tests/cases/conformance/jsx/tsxDynamicTagName2.tsx (2 errors) ====
6+
7+
declare module JSX {
8+
interface Element { }
9+
interface IntrinsicElements {
10+
div: any
11+
}
12+
}
13+
14+
var customTag = "h1";
15+
<customTag> Hello World </customTag> // This should be an error. The lower-case is look up as an intrinsic element name
16+
~~~~~~~~~~~
17+
!!! error TS2339: Property 'customTag' does not exist on type 'JSX.IntrinsicElements'.
18+
~~~~~~~~~~~~
19+
!!! error TS2339: Property 'customTag' does not exist on type 'JSX.IntrinsicElements'.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//// [tsxDynamicTagName2.tsx]
2+
3+
declare module JSX {
4+
interface Element { }
5+
interface IntrinsicElements {
6+
div: any
7+
}
8+
}
9+
10+
var customTag = "h1";
11+
<customTag> Hello World </customTag> // This should be an error. The lower-case is look up as an intrinsic element name
12+
13+
//// [tsxDynamicTagName2.jsx]
14+
var customTag = "h1";
15+
<customTag> Hello World </customTag>; // This should be an error. The lower-case is look up as an intrinsic element name
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
tests/cases/conformance/jsx/tsxDynamicTagName3.tsx(10,1): error TS2339: Property 'h1' does not exist on type 'JSX.IntrinsicElements'.
2+
3+
4+
==== tests/cases/conformance/jsx/tsxDynamicTagName3.tsx (1 errors) ====
5+
6+
declare module JSX {
7+
interface Element { }
8+
interface IntrinsicElements {
9+
div: any
10+
}
11+
}
12+
13+
var CustomTag: "h1" = "h1";
14+
<CustomTag> Hello World </CustomTag> // This should be an error. we will try look up string literal type in JSX.IntrinsicElements
15+
~~~~~~~~~~~
16+
!!! error TS2339: Property 'h1' does not exist on type 'JSX.IntrinsicElements'.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
//// [tsxDynamicTagName3.tsx]
2+
3+
declare module JSX {
4+
interface Element { }
5+
interface IntrinsicElements {
6+
div: any
7+
}
8+
}
9+
10+
var CustomTag: "h1" = "h1";
11+
<CustomTag> Hello World </CustomTag> // This should be an error. we will try look up string literal type in JSX.IntrinsicElements
12+
13+
//// [tsxDynamicTagName3.jsx]
14+
var CustomTag = "h1";
15+
<CustomTag> Hello World </CustomTag>; // This should be an error. we will try look up string literal type in JSX.IntrinsicElements
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
//// [tsxDynamicTagName4.tsx]
2+
3+
declare module JSX {
4+
interface Element { }
5+
interface IntrinsicElements {
6+
div: any
7+
h1: any
8+
}
9+
}
10+
11+
var CustomTag: "h1" = "h1";
12+
<CustomTag> Hello World </CustomTag>
13+
14+
//// [tsxDynamicTagName4.jsx]
15+
var CustomTag = "h1";
16+
<CustomTag> Hello World </CustomTag>;
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
=== tests/cases/conformance/jsx/tsxDynamicTagName4.tsx ===
2+
3+
declare module JSX {
4+
>JSX : Symbol(JSX, Decl(tsxDynamicTagName4.tsx, 0, 0))
5+
6+
interface Element { }
7+
>Element : Symbol(Element, Decl(tsxDynamicTagName4.tsx, 1, 20))
8+
9+
interface IntrinsicElements {
10+
>IntrinsicElements : Symbol(IntrinsicElements, Decl(tsxDynamicTagName4.tsx, 2, 22))
11+
12+
div: any
13+
>div : Symbol(IntrinsicElements.div, Decl(tsxDynamicTagName4.tsx, 3, 30))
14+
15+
h1: any
16+
>h1 : Symbol(IntrinsicElements.h1, Decl(tsxDynamicTagName4.tsx, 4, 10))
17+
}
18+
}
19+
20+
var CustomTag: "h1" = "h1";
21+
>CustomTag : Symbol(CustomTag, Decl(tsxDynamicTagName4.tsx, 9, 3))
22+
23+
<CustomTag> Hello World </CustomTag>
24+
>CustomTag : Symbol(CustomTag, Decl(tsxDynamicTagName4.tsx, 9, 3))
25+
>CustomTag : Symbol(CustomTag, Decl(tsxDynamicTagName4.tsx, 9, 3))
26+
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
=== tests/cases/conformance/jsx/tsxDynamicTagName4.tsx ===
2+
3+
declare module JSX {
4+
>JSX : any
5+
6+
interface Element { }
7+
>Element : Element
8+
9+
interface IntrinsicElements {
10+
>IntrinsicElements : IntrinsicElements
11+
12+
div: any
13+
>div : any
14+
15+
h1: any
16+
>h1 : any
17+
}
18+
}
19+
20+
var CustomTag: "h1" = "h1";
21+
>CustomTag : "h1"
22+
>"h1" : "h1"
23+
24+
<CustomTag> Hello World </CustomTag>
25+
><CustomTag> Hello World </CustomTag> : JSX.Element
26+
>CustomTag : "h1"
27+
>CustomTag : "h1"
28+
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
//// [tests/cases/conformance/jsx/tsxDynamicTagName5.tsx] ////
2+
3+
//// [react.d.ts]
4+
5+
declare module 'react' {
6+
class Component<T, U> { }
7+
}
8+
9+
//// [app.tsx]
10+
import * as React from 'react';
11+
12+
export class Text extends React.Component<{}, {}> {
13+
_tagName: string = 'div';
14+
15+
render() {
16+
return (
17+
<this._tagName />
18+
);
19+
}
20+
}
21+
22+
//// [app.jsx]
23+
"use strict";
24+
var __extends = (this && this.__extends) || function (d, b) {
25+
for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p];
26+
function __() { this.constructor = d; }
27+
d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __());
28+
};
29+
var React = require('react');
30+
var Text = (function (_super) {
31+
__extends(Text, _super);
32+
function Text() {
33+
_super.apply(this, arguments);
34+
this._tagName = 'div';
35+
}
36+
Text.prototype.render = function () {
37+
return (<this._tagName />);
38+
};
39+
return Text;
40+
}(React.Component));
41+
exports.Text = Text;
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
=== tests/cases/conformance/jsx/react.d.ts ===
2+
3+
declare module 'react' {
4+
class Component<T, U> { }
5+
>Component : Symbol(Component, Decl(react.d.ts, 1, 24))
6+
>T : Symbol(T, Decl(react.d.ts, 2, 17))
7+
>U : Symbol(U, Decl(react.d.ts, 2, 19))
8+
}
9+
10+
=== tests/cases/conformance/jsx/app.tsx ===
11+
import * as React from 'react';
12+
>React : Symbol(React, Decl(app.tsx, 0, 6))
13+
14+
export class Text extends React.Component<{}, {}> {
15+
>Text : Symbol(Text, Decl(app.tsx, 0, 31))
16+
>React.Component : Symbol(React.Component, Decl(react.d.ts, 1, 24))
17+
>React : Symbol(React, Decl(app.tsx, 0, 6))
18+
>Component : Symbol(React.Component, Decl(react.d.ts, 1, 24))
19+
20+
_tagName: string = 'div';
21+
>_tagName : Symbol(Text._tagName, Decl(app.tsx, 2, 51))
22+
23+
render() {
24+
>render : Symbol(Text.render, Decl(app.tsx, 3, 27))
25+
26+
return (
27+
<this._tagName />
28+
>this._tagName : Symbol(Text._tagName, Decl(app.tsx, 2, 51))
29+
>this : Symbol(Text, Decl(app.tsx, 0, 31))
30+
>_tagName : Symbol(Text._tagName, Decl(app.tsx, 2, 51))
31+
32+
);
33+
}
34+
}

0 commit comments

Comments
 (0)