Skip to content

Commit adfd5d3

Browse files
uniqueiniquitymhegazy
authored andcommitted
Porting #19249 and #19968 to release-2.6 (#20002)
* Add support for JSX fragment syntax * Update tests * Add baselines * Update API baselines * Respond to CR * Respond to CR * Respond to CR * Remove unnecessary type annotation * Error when using fragment with jsxFactory * Added test for preventing fragment with jsxFactory * Extract JSXChildren checking * Add test * Update baseline
1 parent 82f7bcb commit adfd5d3

40 files changed

+1575
-145
lines changed

src/compiler/binder.ts

+3
Original file line numberDiff line numberDiff line change
@@ -3296,6 +3296,9 @@ namespace ts {
32963296
case SyntaxKind.JsxOpeningElement:
32973297
case SyntaxKind.JsxText:
32983298
case SyntaxKind.JsxClosingElement:
3299+
case SyntaxKind.JsxFragment:
3300+
case SyntaxKind.JsxOpeningFragment:
3301+
case SyntaxKind.JsxClosingFragment:
32993302
case SyntaxKind.JsxAttribute:
33003303
case SyntaxKind.JsxAttributes:
33013304
case SyntaxKind.JsxSpreadAttribute:

src/compiler/checker.ts

+53-20
Original file line numberDiff line numberDiff line change
@@ -13481,7 +13481,13 @@ namespace ts {
1348113481
// JSX expression can appear in two position : JSX Element's children or JSX attribute
1348213482
const jsxAttributes = isJsxAttributeLike(node.parent) ?
1348313483
node.parent.parent :
13484-
node.parent.openingElement.attributes; // node.parent is JsxElement
13484+
isJsxElement(node.parent) ?
13485+
node.parent.openingElement.attributes :
13486+
undefined; // node.parent is JsxFragment with no attributes
13487+
13488+
if (!jsxAttributes) {
13489+
return undefined; // don't check children of a fragment
13490+
}
1348513491

1348613492
// When we trying to resolve JsxOpeningLikeElement as a stateless function element, we will already give its attributes a contextual type
1348713493
// which is a type of the parameter of the signature we are trying out.
@@ -14061,13 +14067,13 @@ namespace ts {
1406114067
}
1406214068

1406314069
function checkJsxSelfClosingElement(node: JsxSelfClosingElement): Type {
14064-
checkJsxOpeningLikeElement(node);
14070+
checkJsxOpeningLikeElementOrOpeningFragment(node);
1406514071
return getJsxGlobalElementType() || anyType;
1406614072
}
1406714073

1406814074
function checkJsxElement(node: JsxElement): Type {
1406914075
// Check attributes
14070-
checkJsxOpeningLikeElement(node.openingElement);
14076+
checkJsxOpeningLikeElementOrOpeningFragment(node.openingElement);
1407114077

1407214078
// Perform resolution on the closing tag so that rename/go to definition/etc work
1407314079
if (isJsxIntrinsicIdentifier(node.closingElement.tagName)) {
@@ -14080,6 +14086,16 @@ namespace ts {
1408014086
return getJsxGlobalElementType() || anyType;
1408114087
}
1408214088

14089+
function checkJsxFragment(node: JsxFragment): Type {
14090+
checkJsxOpeningLikeElementOrOpeningFragment(node.openingFragment);
14091+
14092+
if (compilerOptions.jsx === JsxEmit.React && compilerOptions.jsxFactory) {
14093+
error(node, Diagnostics.JSX_fragment_is_not_supported_when_using_jsxFactory);
14094+
}
14095+
14096+
return getJsxGlobalElementType() || anyType;
14097+
}
14098+
1408314099
/**
1408414100
* Returns true iff the JSX element name would be a valid JS identifier, ignoring restrictions about keywords not being identifiers
1408514101
*/
@@ -14184,19 +14200,7 @@ namespace ts {
1418414200
const parent = openingLikeElement.parent.kind === SyntaxKind.JsxElement ? openingLikeElement.parent as JsxElement : undefined;
1418514201
// We have to check that openingElement of the parent is the one we are visiting as this may not be true for selfClosingElement
1418614202
if (parent && parent.openingElement === openingLikeElement && parent.children.length > 0) {
14187-
const childrenTypes: Type[] = [];
14188-
for (const child of (parent as JsxElement).children) {
14189-
// In React, JSX text that contains only whitespaces will be ignored so we don't want to type-check that
14190-
// because then type of children property will have constituent of string type.
14191-
if (child.kind === SyntaxKind.JsxText) {
14192-
if (!child.containsOnlyWhiteSpaces) {
14193-
childrenTypes.push(stringType);
14194-
}
14195-
}
14196-
else {
14197-
childrenTypes.push(checkExpression(child, checkMode));
14198-
}
14199-
}
14203+
const childrenTypes: Type[] = checkJsxChildren(parent as JsxElement, checkMode);
1420014204

1420114205
if (!hasSpreadAnyType && jsxChildrenPropertyName && jsxChildrenPropertyName !== "") {
1420214206
// Error if there is a attribute named "children" explicitly specified and children element.
@@ -14236,6 +14240,23 @@ namespace ts {
1423614240
}
1423714241
}
1423814242

14243+
function checkJsxChildren(node: JsxElement | JsxFragment, checkMode?: CheckMode) {
14244+
const childrenTypes: Type[] = [];
14245+
for (const child of node.children) {
14246+
// In React, JSX text that contains only whitespaces will be ignored so we don't want to type-check that
14247+
// because then type of children property will have constituent of string type.
14248+
if (child.kind === SyntaxKind.JsxText) {
14249+
if (!child.containsOnlyWhiteSpaces) {
14250+
childrenTypes.push(stringType);
14251+
}
14252+
}
14253+
else {
14254+
childrenTypes.push(checkExpression(child, checkMode));
14255+
}
14256+
}
14257+
return childrenTypes;
14258+
}
14259+
1423914260
/**
1424014261
* Check attributes property of opening-like element. This function is called during chooseOverload to get call signature of a JSX opening-like element.
1424114262
* (See "checkApplicableSignatureForJsxOpeningLikeElement" for how the function is used)
@@ -14743,14 +14764,19 @@ namespace ts {
1474314764
}
1474414765
}
1474514766

14746-
function checkJsxOpeningLikeElement(node: JsxOpeningLikeElement) {
14747-
checkGrammarJsxElement(node);
14767+
function checkJsxOpeningLikeElementOrOpeningFragment(node: JsxOpeningLikeElement | JsxOpeningFragment) {
14768+
const isNodeOpeningLikeElement = isJsxOpeningLikeElement(node);
14769+
14770+
if (isNodeOpeningLikeElement) {
14771+
checkGrammarJsxElement(<JsxOpeningLikeElement>node);
14772+
}
1474814773
checkJsxPreconditions(node);
1474914774
// The reactNamespace/jsxFactory's root symbol should be marked as 'used' so we don't incorrectly elide its import.
1475014775
// And if there is no reactNamespace/jsxFactory's symbol in scope when targeting React emit, we should issue an error.
1475114776
const reactRefErr = diagnostics && compilerOptions.jsx === JsxEmit.React ? Diagnostics.Cannot_find_name_0 : undefined;
1475214777
const reactNamespace = getJsxNamespace();
14753-
const reactSym = resolveName(node.tagName, reactNamespace, SymbolFlags.Value, reactRefErr, reactNamespace, /*isUse*/ true);
14778+
const reactLocation = isNodeOpeningLikeElement ? (<JsxOpeningLikeElement>node).tagName : node;
14779+
const reactSym = resolveName(reactLocation, reactNamespace, SymbolFlags.Value, reactRefErr, reactNamespace, /*isUse*/ true);
1475414780
if (reactSym) {
1475514781
// Mark local symbol as referenced here because it might not have been marked
1475614782
// if jsx emit was not react as there wont be error being emitted
@@ -14762,7 +14788,12 @@ namespace ts {
1476214788
}
1476314789
}
1476414790

14765-
checkJsxAttributesAssignableToTagNameAttributes(node);
14791+
if (isNodeOpeningLikeElement) {
14792+
checkJsxAttributesAssignableToTagNameAttributes(<JsxOpeningLikeElement>node);
14793+
}
14794+
else {
14795+
checkJsxChildren((node as JsxOpeningFragment).parent);
14796+
}
1476614797
}
1476714798

1476814799
/**
@@ -18526,6 +18557,8 @@ namespace ts {
1852618557
return checkJsxElement(<JsxElement>node);
1852718558
case SyntaxKind.JsxSelfClosingElement:
1852818559
return checkJsxSelfClosingElement(<JsxSelfClosingElement>node);
18560+
case SyntaxKind.JsxFragment:
18561+
return checkJsxFragment(<JsxFragment>node);
1852918562
case SyntaxKind.JsxAttributes:
1853018563
return checkJsxAttributes(<JsxAttributes>node, checkMode);
1853118564
case SyntaxKind.JsxOpeningElement:

src/compiler/diagnosticMessages.json

+12
Original file line numberDiff line numberDiff line change
@@ -3603,6 +3603,18 @@
36033603
"category": "Error",
36043604
"code": 17013
36053605
},
3606+
"JSX fragment has no corresponding closing tag.": {
3607+
"category": "Error",
3608+
"code": 17014
3609+
},
3610+
"Expected corresponding closing tag for JSX fragment.": {
3611+
"category": "Error",
3612+
"code": 17015
3613+
},
3614+
"JSX fragment is not supported when using --jsxFactory": {
3615+
"category": "Error",
3616+
"code":17016
3617+
},
36063618

36073619
"Circularity detected while resolving configuration: {0}": {
36083620
"category": "Error",

src/compiler/emitter.ts

+28-18
Original file line numberDiff line numberDiff line change
@@ -699,9 +699,11 @@ namespace ts {
699699
case SyntaxKind.JsxText:
700700
return emitJsxText(<JsxText>node);
701701
case SyntaxKind.JsxOpeningElement:
702-
return emitJsxOpeningElement(<JsxOpeningElement>node);
702+
case SyntaxKind.JsxOpeningFragment:
703+
return emitJsxOpeningElementOrFragment(<JsxOpeningElement>node);
703704
case SyntaxKind.JsxClosingElement:
704-
return emitJsxClosingElement(<JsxClosingElement>node);
705+
case SyntaxKind.JsxClosingFragment:
706+
return emitJsxClosingElementOrFragment(<JsxClosingElement>node);
705707
case SyntaxKind.JsxAttribute:
706708
return emitJsxAttribute(<JsxAttribute>node);
707709
case SyntaxKind.JsxAttributes:
@@ -836,6 +838,8 @@ namespace ts {
836838
return emitJsxElement(<JsxElement>node);
837839
case SyntaxKind.JsxSelfClosingElement:
838840
return emitJsxSelfClosingElement(<JsxSelfClosingElement>node);
841+
case SyntaxKind.JsxFragment:
842+
return emitJsxFragment(<JsxFragment>node);
839843

840844
// Transformation nodes
841845
case SyntaxKind.PartiallyEmittedExpression:
@@ -2060,7 +2064,7 @@ namespace ts {
20602064

20612065
function emitJsxElement(node: JsxElement) {
20622066
emit(node.openingElement);
2063-
emitList(node, node.children, ListFormat.JsxElementChildren);
2067+
emitList(node, node.children, ListFormat.JsxElementOrFragmentChildren);
20642068
emit(node.closingElement);
20652069
}
20662070

@@ -2075,24 +2079,36 @@ namespace ts {
20752079
write("/>");
20762080
}
20772081

2078-
function emitJsxOpeningElement(node: JsxOpeningElement) {
2082+
function emitJsxFragment(node: JsxFragment) {
2083+
emit(node.openingFragment);
2084+
emitList(node, node.children, ListFormat.JsxElementOrFragmentChildren);
2085+
emit(node.closingFragment);
2086+
}
2087+
2088+
function emitJsxOpeningElementOrFragment(node: JsxOpeningElement | JsxOpeningFragment) {
20792089
write("<");
2080-
emitJsxTagName(node.tagName);
2081-
writeIfAny(node.attributes.properties, " ");
2082-
// We are checking here so we won't re-enter the emitting pipeline and emit extra sourcemap
2083-
if (node.attributes.properties && node.attributes.properties.length > 0) {
2084-
emit(node.attributes);
2090+
2091+
if (isJsxOpeningElement(node)) {
2092+
emitJsxTagName(node.tagName);
2093+
// We are checking here so we won't re-enter the emitting pipeline and emit extra sourcemap
2094+
if (node.attributes.properties && node.attributes.properties.length > 0) {
2095+
write(" ");
2096+
emit(node.attributes);
2097+
}
20852098
}
2099+
20862100
write(">");
20872101
}
20882102

20892103
function emitJsxText(node: JsxText) {
20902104
writer.writeLiteral(getTextOfNode(node, /*includeTrivia*/ true));
20912105
}
20922106

2093-
function emitJsxClosingElement(node: JsxClosingElement) {
2107+
function emitJsxClosingElementOrFragment(node: JsxClosingElement | JsxClosingFragment) {
20942108
write("</");
2095-
emitJsxTagName(node.tagName);
2109+
if (isJsxClosingElement(node)) {
2110+
emitJsxTagName(node.tagName);
2111+
}
20962112
write(">");
20972113
}
20982114

@@ -2611,12 +2627,6 @@ namespace ts {
26112627
writer.decreaseIndent();
26122628
}
26132629

2614-
function writeIfAny(nodes: NodeArray<Node>, text: string) {
2615-
if (some(nodes)) {
2616-
write(text);
2617-
}
2618-
}
2619-
26202630
function writeToken(token: SyntaxKind, pos: number, contextNode?: Node) {
26212631
return onEmitSourceMapOfToken
26222632
? onEmitSourceMapOfToken(contextNode, token, pos, writeTokenText)
@@ -3176,7 +3186,7 @@ namespace ts {
31763186
EnumMembers = CommaDelimited | Indented | MultiLine,
31773187
CaseBlockClauses = Indented | MultiLine,
31783188
NamedImportsOrExportsElements = CommaDelimited | SpaceBetweenSiblings | AllowTrailingComma | SingleLine | SpaceBetweenBraces,
3179-
JsxElementChildren = SingleLine | NoInterveningComments,
3189+
JsxElementOrFragmentChildren = SingleLine | NoInterveningComments,
31803190
JsxElementAttributes = SingleLine | SpaceBetweenSiblings | NoInterveningComments,
31813191
CaseOrDefaultClauseStatements = Indented | MultiLine | NoTrailingNewLine | OptionalIfEmpty,
31823192
HeritageClauseTypes = CommaDelimited | SpaceBetweenSiblings | SingleLine,

src/compiler/factory.ts

+50-3
Original file line numberDiff line numberDiff line change
@@ -2115,6 +2115,22 @@ namespace ts {
21152115
: node;
21162116
}
21172117

2118+
export function createJsxFragment(openingFragment: JsxOpeningFragment, children: ReadonlyArray<JsxChild>, closingFragment: JsxClosingFragment) {
2119+
const node = <JsxFragment>createSynthesizedNode(SyntaxKind.JsxFragment);
2120+
node.openingFragment = openingFragment;
2121+
node.children = createNodeArray(children);
2122+
node.closingFragment = closingFragment;
2123+
return node;
2124+
}
2125+
2126+
export function updateJsxFragment(node: JsxFragment, openingFragment: JsxOpeningFragment, children: ReadonlyArray<JsxChild>, closingFragment: JsxClosingFragment) {
2127+
return node.openingFragment !== openingFragment
2128+
|| node.children !== children
2129+
|| node.closingFragment !== closingFragment
2130+
? updateNode(createJsxFragment(openingFragment, children, closingFragment), node)
2131+
: node;
2132+
}
2133+
21182134
export function createJsxAttribute(name: Identifier, initializer: StringLiteral | JsxExpression) {
21192135
const node = <JsxAttribute>createSynthesizedNode(SyntaxKind.JsxAttribute);
21202136
node.name = name;
@@ -2951,7 +2967,7 @@ namespace ts {
29512967
);
29522968
}
29532969

2954-
function createReactNamespace(reactNamespace: string, parent: JsxOpeningLikeElement) {
2970+
function createReactNamespace(reactNamespace: string, parent: JsxOpeningLikeElement | JsxOpeningFragment) {
29552971
// To ensure the emit resolver can properly resolve the namespace, we need to
29562972
// treat this identifier as if it were a source tree node by clearing the `Synthesized`
29572973
// flag and setting a parent node.
@@ -2963,7 +2979,7 @@ namespace ts {
29632979
return react;
29642980
}
29652981

2966-
function createJsxFactoryExpressionFromEntityName(jsxFactory: EntityName, parent: JsxOpeningLikeElement): Expression {
2982+
function createJsxFactoryExpressionFromEntityName(jsxFactory: EntityName, parent: JsxOpeningLikeElement | JsxOpeningFragment): Expression {
29672983
if (isQualifiedName(jsxFactory)) {
29682984
const left = createJsxFactoryExpressionFromEntityName(jsxFactory.left, parent);
29692985
const right = createIdentifier(idText(jsxFactory.right));
@@ -2975,7 +2991,7 @@ namespace ts {
29752991
}
29762992
}
29772993

2978-
function createJsxFactoryExpression(jsxFactoryEntity: EntityName, reactNamespace: string, parent: JsxOpeningLikeElement): Expression {
2994+
function createJsxFactoryExpression(jsxFactoryEntity: EntityName, reactNamespace: string, parent: JsxOpeningLikeElement | JsxOpeningFragment): Expression {
29792995
return jsxFactoryEntity ?
29802996
createJsxFactoryExpressionFromEntityName(jsxFactoryEntity, parent) :
29812997
createPropertyAccess(
@@ -3016,6 +3032,37 @@ namespace ts {
30163032
);
30173033
}
30183034

3035+
export function createExpressionForJsxFragment(jsxFactoryEntity: EntityName, reactNamespace: string, children: Expression[], parentElement: JsxOpeningFragment, location: TextRange): LeftHandSideExpression {
3036+
const tagName = createPropertyAccess(
3037+
createReactNamespace(reactNamespace, parentElement),
3038+
"Fragment"
3039+
);
3040+
3041+
const argumentsList = [<Expression>tagName];
3042+
argumentsList.push(createNull());
3043+
3044+
if (children && children.length > 0) {
3045+
if (children.length > 1) {
3046+
for (const child of children) {
3047+
child.startsOnNewLine = true;
3048+
argumentsList.push(child);
3049+
}
3050+
}
3051+
else {
3052+
argumentsList.push(children[0]);
3053+
}
3054+
}
3055+
3056+
return setTextRange(
3057+
createCall(
3058+
createJsxFactoryExpression(jsxFactoryEntity, reactNamespace, parentElement),
3059+
/*typeArguments*/ undefined,
3060+
argumentsList
3061+
),
3062+
location
3063+
);
3064+
}
3065+
30193066
// Helpers
30203067

30213068
export function getHelperName(name: string) {

0 commit comments

Comments
 (0)