diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 35a62a644d839..ce02d461ed55e 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -3296,6 +3296,9 @@ namespace ts { case SyntaxKind.JsxOpeningElement: case SyntaxKind.JsxText: case SyntaxKind.JsxClosingElement: + case SyntaxKind.JsxFragment: + case SyntaxKind.JsxOpeningFragment: + case SyntaxKind.JsxClosingFragment: case SyntaxKind.JsxAttribute: case SyntaxKind.JsxAttributes: case SyntaxKind.JsxSpreadAttribute: diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 09b5ebc4fcf1b..14951dd5e3d88 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -13481,7 +13481,13 @@ namespace ts { // JSX expression can appear in two position : JSX Element's children or JSX attribute const jsxAttributes = isJsxAttributeLike(node.parent) ? node.parent.parent : - node.parent.openingElement.attributes; // node.parent is JsxElement + isJsxElement(node.parent) ? + node.parent.openingElement.attributes : + undefined; // node.parent is JsxFragment with no attributes + + if (!jsxAttributes) { + return undefined; // don't check children of a fragment + } // When we trying to resolve JsxOpeningLikeElement as a stateless function element, we will already give its attributes a contextual type // which is a type of the parameter of the signature we are trying out. @@ -14061,13 +14067,13 @@ namespace ts { } function checkJsxSelfClosingElement(node: JsxSelfClosingElement): Type { - checkJsxOpeningLikeElement(node); + checkJsxOpeningLikeElementOrOpeningFragment(node); return getJsxGlobalElementType() || anyType; } function checkJsxElement(node: JsxElement): Type { // Check attributes - checkJsxOpeningLikeElement(node.openingElement); + checkJsxOpeningLikeElementOrOpeningFragment(node.openingElement); // Perform resolution on the closing tag so that rename/go to definition/etc work if (isJsxIntrinsicIdentifier(node.closingElement.tagName)) { @@ -14080,6 +14086,16 @@ namespace ts { return getJsxGlobalElementType() || anyType; } + function checkJsxFragment(node: JsxFragment): Type { + checkJsxOpeningLikeElementOrOpeningFragment(node.openingFragment); + + if (compilerOptions.jsx === JsxEmit.React && compilerOptions.jsxFactory) { + error(node, Diagnostics.JSX_fragment_is_not_supported_when_using_jsxFactory); + } + + return getJsxGlobalElementType() || anyType; + } + /** * Returns true iff the JSX element name would be a valid JS identifier, ignoring restrictions about keywords not being identifiers */ @@ -14184,19 +14200,7 @@ namespace ts { const parent = openingLikeElement.parent.kind === SyntaxKind.JsxElement ? openingLikeElement.parent as JsxElement : undefined; // We have to check that openingElement of the parent is the one we are visiting as this may not be true for selfClosingElement if (parent && parent.openingElement === openingLikeElement && parent.children.length > 0) { - const childrenTypes: Type[] = []; - for (const child of (parent as JsxElement).children) { - // In React, JSX text that contains only whitespaces will be ignored so we don't want to type-check that - // because then type of children property will have constituent of string type. - if (child.kind === SyntaxKind.JsxText) { - if (!child.containsOnlyWhiteSpaces) { - childrenTypes.push(stringType); - } - } - else { - childrenTypes.push(checkExpression(child, checkMode)); - } - } + const childrenTypes: Type[] = checkJsxChildren(parent as JsxElement, checkMode); if (!hasSpreadAnyType && jsxChildrenPropertyName && jsxChildrenPropertyName !== "") { // Error if there is a attribute named "children" explicitly specified and children element. @@ -14236,6 +14240,23 @@ namespace ts { } } + function checkJsxChildren(node: JsxElement | JsxFragment, checkMode?: CheckMode) { + const childrenTypes: Type[] = []; + for (const child of node.children) { + // In React, JSX text that contains only whitespaces will be ignored so we don't want to type-check that + // because then type of children property will have constituent of string type. + if (child.kind === SyntaxKind.JsxText) { + if (!child.containsOnlyWhiteSpaces) { + childrenTypes.push(stringType); + } + } + else { + childrenTypes.push(checkExpression(child, checkMode)); + } + } + return childrenTypes; + } + /** * Check attributes property of opening-like element. This function is called during chooseOverload to get call signature of a JSX opening-like element. * (See "checkApplicableSignatureForJsxOpeningLikeElement" for how the function is used) @@ -14743,14 +14764,19 @@ namespace ts { } } - function checkJsxOpeningLikeElement(node: JsxOpeningLikeElement) { - checkGrammarJsxElement(node); + function checkJsxOpeningLikeElementOrOpeningFragment(node: JsxOpeningLikeElement | JsxOpeningFragment) { + const isNodeOpeningLikeElement = isJsxOpeningLikeElement(node); + + if (isNodeOpeningLikeElement) { + checkGrammarJsxElement(node); + } checkJsxPreconditions(node); // The reactNamespace/jsxFactory's root symbol should be marked as 'used' so we don't incorrectly elide its import. // And if there is no reactNamespace/jsxFactory's symbol in scope when targeting React emit, we should issue an error. const reactRefErr = diagnostics && compilerOptions.jsx === JsxEmit.React ? Diagnostics.Cannot_find_name_0 : undefined; const reactNamespace = getJsxNamespace(); - const reactSym = resolveName(node.tagName, reactNamespace, SymbolFlags.Value, reactRefErr, reactNamespace, /*isUse*/ true); + const reactLocation = isNodeOpeningLikeElement ? (node).tagName : node; + const reactSym = resolveName(reactLocation, reactNamespace, SymbolFlags.Value, reactRefErr, reactNamespace, /*isUse*/ true); if (reactSym) { // Mark local symbol as referenced here because it might not have been marked // if jsx emit was not react as there wont be error being emitted @@ -14762,7 +14788,12 @@ namespace ts { } } - checkJsxAttributesAssignableToTagNameAttributes(node); + if (isNodeOpeningLikeElement) { + checkJsxAttributesAssignableToTagNameAttributes(node); + } + else { + checkJsxChildren((node as JsxOpeningFragment).parent); + } } /** @@ -18526,6 +18557,8 @@ namespace ts { return checkJsxElement(node); case SyntaxKind.JsxSelfClosingElement: return checkJsxSelfClosingElement(node); + case SyntaxKind.JsxFragment: + return checkJsxFragment(node); case SyntaxKind.JsxAttributes: return checkJsxAttributes(node, checkMode); case SyntaxKind.JsxOpeningElement: diff --git a/src/compiler/diagnosticMessages.json b/src/compiler/diagnosticMessages.json index 3ad919e1bc0ed..b33da03d66227 100644 --- a/src/compiler/diagnosticMessages.json +++ b/src/compiler/diagnosticMessages.json @@ -3603,6 +3603,18 @@ "category": "Error", "code": 17013 }, + "JSX fragment has no corresponding closing tag.": { + "category": "Error", + "code": 17014 + }, + "Expected corresponding closing tag for JSX fragment.": { + "category": "Error", + "code": 17015 + }, + "JSX fragment is not supported when using --jsxFactory": { + "category": "Error", + "code":17016 + }, "Circularity detected while resolving configuration: {0}": { "category": "Error", diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 9ce220e723f92..8c41eda192adf 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -699,9 +699,11 @@ namespace ts { case SyntaxKind.JsxText: return emitJsxText(node); case SyntaxKind.JsxOpeningElement: - return emitJsxOpeningElement(node); + case SyntaxKind.JsxOpeningFragment: + return emitJsxOpeningElementOrFragment(node); case SyntaxKind.JsxClosingElement: - return emitJsxClosingElement(node); + case SyntaxKind.JsxClosingFragment: + return emitJsxClosingElementOrFragment(node); case SyntaxKind.JsxAttribute: return emitJsxAttribute(node); case SyntaxKind.JsxAttributes: @@ -836,6 +838,8 @@ namespace ts { return emitJsxElement(node); case SyntaxKind.JsxSelfClosingElement: return emitJsxSelfClosingElement(node); + case SyntaxKind.JsxFragment: + return emitJsxFragment(node); // Transformation nodes case SyntaxKind.PartiallyEmittedExpression: @@ -2060,7 +2064,7 @@ namespace ts { function emitJsxElement(node: JsxElement) { emit(node.openingElement); - emitList(node, node.children, ListFormat.JsxElementChildren); + emitList(node, node.children, ListFormat.JsxElementOrFragmentChildren); emit(node.closingElement); } @@ -2075,14 +2079,24 @@ namespace ts { write("/>"); } - function emitJsxOpeningElement(node: JsxOpeningElement) { + function emitJsxFragment(node: JsxFragment) { + emit(node.openingFragment); + emitList(node, node.children, ListFormat.JsxElementOrFragmentChildren); + emit(node.closingFragment); + } + + function emitJsxOpeningElementOrFragment(node: JsxOpeningElement | JsxOpeningFragment) { write("<"); - emitJsxTagName(node.tagName); - writeIfAny(node.attributes.properties, " "); - // We are checking here so we won't re-enter the emitting pipeline and emit extra sourcemap - if (node.attributes.properties && node.attributes.properties.length > 0) { - emit(node.attributes); + + if (isJsxOpeningElement(node)) { + emitJsxTagName(node.tagName); + // We are checking here so we won't re-enter the emitting pipeline and emit extra sourcemap + if (node.attributes.properties && node.attributes.properties.length > 0) { + write(" "); + emit(node.attributes); + } } + write(">"); } @@ -2090,9 +2104,11 @@ namespace ts { writer.writeLiteral(getTextOfNode(node, /*includeTrivia*/ true)); } - function emitJsxClosingElement(node: JsxClosingElement) { + function emitJsxClosingElementOrFragment(node: JsxClosingElement | JsxClosingFragment) { write(""); } @@ -2611,12 +2627,6 @@ namespace ts { writer.decreaseIndent(); } - function writeIfAny(nodes: NodeArray, text: string) { - if (some(nodes)) { - write(text); - } - } - function writeToken(token: SyntaxKind, pos: number, contextNode?: Node) { return onEmitSourceMapOfToken ? onEmitSourceMapOfToken(contextNode, token, pos, writeTokenText) @@ -3176,7 +3186,7 @@ namespace ts { EnumMembers = CommaDelimited | Indented | MultiLine, CaseBlockClauses = Indented | MultiLine, NamedImportsOrExportsElements = CommaDelimited | SpaceBetweenSiblings | AllowTrailingComma | SingleLine | SpaceBetweenBraces, - JsxElementChildren = SingleLine | NoInterveningComments, + JsxElementOrFragmentChildren = SingleLine | NoInterveningComments, JsxElementAttributes = SingleLine | SpaceBetweenSiblings | NoInterveningComments, CaseOrDefaultClauseStatements = Indented | MultiLine | NoTrailingNewLine | OptionalIfEmpty, HeritageClauseTypes = CommaDelimited | SpaceBetweenSiblings | SingleLine, diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index 65c1c92f36652..8d66608c22686 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -2115,6 +2115,22 @@ namespace ts { : node; } + export function createJsxFragment(openingFragment: JsxOpeningFragment, children: ReadonlyArray, closingFragment: JsxClosingFragment) { + const node = createSynthesizedNode(SyntaxKind.JsxFragment); + node.openingFragment = openingFragment; + node.children = createNodeArray(children); + node.closingFragment = closingFragment; + return node; + } + + export function updateJsxFragment(node: JsxFragment, openingFragment: JsxOpeningFragment, children: ReadonlyArray, closingFragment: JsxClosingFragment) { + return node.openingFragment !== openingFragment + || node.children !== children + || node.closingFragment !== closingFragment + ? updateNode(createJsxFragment(openingFragment, children, closingFragment), node) + : node; + } + export function createJsxAttribute(name: Identifier, initializer: StringLiteral | JsxExpression) { const node = createSynthesizedNode(SyntaxKind.JsxAttribute); node.name = name; @@ -2951,7 +2967,7 @@ namespace ts { ); } - function createReactNamespace(reactNamespace: string, parent: JsxOpeningLikeElement) { + function createReactNamespace(reactNamespace: string, parent: JsxOpeningLikeElement | JsxOpeningFragment) { // To ensure the emit resolver can properly resolve the namespace, we need to // treat this identifier as if it were a source tree node by clearing the `Synthesized` // flag and setting a parent node. @@ -2963,7 +2979,7 @@ namespace ts { return react; } - function createJsxFactoryExpressionFromEntityName(jsxFactory: EntityName, parent: JsxOpeningLikeElement): Expression { + function createJsxFactoryExpressionFromEntityName(jsxFactory: EntityName, parent: JsxOpeningLikeElement | JsxOpeningFragment): Expression { if (isQualifiedName(jsxFactory)) { const left = createJsxFactoryExpressionFromEntityName(jsxFactory.left, parent); const right = createIdentifier(idText(jsxFactory.right)); @@ -2975,7 +2991,7 @@ namespace ts { } } - function createJsxFactoryExpression(jsxFactoryEntity: EntityName, reactNamespace: string, parent: JsxOpeningLikeElement): Expression { + function createJsxFactoryExpression(jsxFactoryEntity: EntityName, reactNamespace: string, parent: JsxOpeningLikeElement | JsxOpeningFragment): Expression { return jsxFactoryEntity ? createJsxFactoryExpressionFromEntityName(jsxFactoryEntity, parent) : createPropertyAccess( @@ -3016,6 +3032,37 @@ namespace ts { ); } + export function createExpressionForJsxFragment(jsxFactoryEntity: EntityName, reactNamespace: string, children: Expression[], parentElement: JsxOpeningFragment, location: TextRange): LeftHandSideExpression { + const tagName = createPropertyAccess( + createReactNamespace(reactNamespace, parentElement), + "Fragment" + ); + + const argumentsList = [tagName]; + argumentsList.push(createNull()); + + if (children && children.length > 0) { + if (children.length > 1) { + for (const child of children) { + child.startsOnNewLine = true; + argumentsList.push(child); + } + } + else { + argumentsList.push(children[0]); + } + } + + return setTextRange( + createCall( + createJsxFactoryExpression(jsxFactoryEntity, reactNamespace, parentElement), + /*typeArguments*/ undefined, + argumentsList + ), + location + ); + } + // Helpers export function getHelperName(name: string) { diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index e2bae71e6bf7b..1f37043bfd043 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -377,6 +377,10 @@ namespace ts { return visitNode(cbNode, (node).openingElement) || visitNodes(cbNode, cbNodes, (node).children) || visitNode(cbNode, (node).closingElement); + case SyntaxKind.JsxFragment: + return visitNode(cbNode, (node).openingFragment) || + visitNodes(cbNode, cbNodes, (node).children) || + visitNode(cbNode, (node).closingFragment); case SyntaxKind.JsxSelfClosingElement: case SyntaxKind.JsxOpeningElement: return visitNode(cbNode, (node).tagName) || @@ -1423,6 +1427,11 @@ namespace ts { return tokenIsIdentifierOrKeyword(token()); } + function nextTokenIsIdentifierOrKeywordOrGreaterThan() { + nextToken(); + return tokenIsIdentifierOrKeywordOrGreaterThan(token()); + } + function isHeritageClauseExtendsOrImplementsKeyword(): boolean { if (token() === SyntaxKind.ImplementsKeyword || token() === SyntaxKind.ExtendsKeyword) { @@ -3802,9 +3811,9 @@ namespace ts { node.operand = parseLeftHandSideExpressionOrHigher(); return finishNode(node); } - else if (sourceFile.languageVariant === LanguageVariant.JSX && token() === SyntaxKind.LessThanToken && lookAhead(nextTokenIsIdentifierOrKeyword)) { + else if (sourceFile.languageVariant === LanguageVariant.JSX && token() === SyntaxKind.LessThanToken && lookAhead(nextTokenIsIdentifierOrKeywordOrGreaterThan)) { // JSXElement is part of primaryExpression - return parseJsxElementOrSelfClosingElement(/*inExpressionContext*/ true); + return parseJsxElementOrSelfClosingElementOrFragment(/*inExpressionContext*/ true); } const expression = parseLeftHandSideExpressionOrHigher(); @@ -3959,14 +3968,14 @@ namespace ts { } - function parseJsxElementOrSelfClosingElement(inExpressionContext: boolean): JsxElement | JsxSelfClosingElement { - const opening = parseJsxOpeningOrSelfClosingElement(inExpressionContext); - let result: JsxElement | JsxSelfClosingElement; + function parseJsxElementOrSelfClosingElementOrFragment(inExpressionContext: boolean): JsxElement | JsxSelfClosingElement | JsxFragment { + const opening = parseJsxOpeningOrSelfClosingElementOrOpeningFragment(inExpressionContext); + let result: JsxElement | JsxSelfClosingElement | JsxFragment; if (opening.kind === SyntaxKind.JsxOpeningElement) { const node = createNode(SyntaxKind.JsxElement, opening.pos); node.openingElement = opening; - node.children = parseJsxChildren(node.openingElement.tagName); + node.children = parseJsxChildren(node.openingElement); node.closingElement = parseJsxClosingElement(inExpressionContext); if (!tagNamesAreEquivalent(node.openingElement.tagName, node.closingElement.tagName)) { @@ -3975,6 +3984,14 @@ namespace ts { result = finishNode(node); } + else if (opening.kind === SyntaxKind.JsxOpeningFragment) { + const node = createNode(SyntaxKind.JsxFragment, opening.pos); + node.openingFragment = opening; + node.children = parseJsxChildren(node.openingFragment); + node.closingFragment = parseJsxClosingFragment(inExpressionContext); + + result = finishNode(node); + } else { Debug.assert(opening.kind === SyntaxKind.JsxSelfClosingElement); // Nothing else to do for self-closing elements @@ -3989,7 +4006,7 @@ namespace ts { // Since JSX elements are invalid < operands anyway, this lookahead parse will only occur in error scenarios // of one sort or another. if (inExpressionContext && token() === SyntaxKind.LessThanToken) { - const invalidElement = tryParse(() => parseJsxElementOrSelfClosingElement(/*inExpressionContext*/ true)); + const invalidElement = tryParse(() => parseJsxElementOrSelfClosingElementOrFragment(/*inExpressionContext*/ true)); if (invalidElement) { parseErrorAtCurrentToken(Diagnostics.JSX_expressions_must_have_one_parent_element); const badNode = createNode(SyntaxKind.BinaryExpression, result.pos); @@ -4020,12 +4037,12 @@ namespace ts { case SyntaxKind.OpenBraceToken: return parseJsxExpression(/*inExpressionContext*/ false); case SyntaxKind.LessThanToken: - return parseJsxElementOrSelfClosingElement(/*inExpressionContext*/ false); + return parseJsxElementOrSelfClosingElementOrFragment(/*inExpressionContext*/ false); } Debug.fail("Unknown JSX child kind " + token()); } - function parseJsxChildren(openingTagName: LeftHandSideExpression): NodeArray { + function parseJsxChildren(openingTag: JsxOpeningElement | JsxOpeningFragment): NodeArray { const list = []; const listPos = getNodePos(); const saveParsingContext = parsingContext; @@ -4040,7 +4057,13 @@ namespace ts { else if (token() === SyntaxKind.EndOfFileToken) { // If we hit EOF, issue the error at the tag that lacks the closing element // rather than at the end of the file (which is useless) - parseErrorAtPosition(openingTagName.pos, openingTagName.end - openingTagName.pos, Diagnostics.JSX_element_0_has_no_corresponding_closing_tag, getTextOfNodeFromSourceText(sourceText, openingTagName)); + if (isJsxOpeningFragment(openingTag)) { + parseErrorAtPosition(openingTag.pos, openingTag.end - openingTag.pos, Diagnostics.JSX_fragment_has_no_corresponding_closing_tag); + } + else { + const openingTagName = openingTag.tagName; + parseErrorAtPosition(openingTagName.pos, openingTagName.end - openingTagName.pos, Diagnostics.JSX_element_0_has_no_corresponding_closing_tag, getTextOfNodeFromSourceText(sourceText, openingTagName)); + } break; } else if (token() === SyntaxKind.ConflictMarkerTrivia) { @@ -4063,11 +4086,17 @@ namespace ts { return finishNode(jsxAttributes); } - function parseJsxOpeningOrSelfClosingElement(inExpressionContext: boolean): JsxOpeningElement | JsxSelfClosingElement { + function parseJsxOpeningOrSelfClosingElementOrOpeningFragment(inExpressionContext: boolean): JsxOpeningElement | JsxSelfClosingElement | JsxOpeningFragment { const fullStart = scanner.getStartPos(); parseExpected(SyntaxKind.LessThanToken); + if (token() === SyntaxKind.GreaterThanToken) { + parseExpected(SyntaxKind.GreaterThanToken); + const node: JsxOpeningFragment = createNode(SyntaxKind.JsxOpeningFragment, fullStart); + return finishNode(node); + } + const tagName = parseJsxElementName(); const attributes = parseJsxAttributes(); @@ -4179,6 +4208,23 @@ namespace ts { return finishNode(node); } + function parseJsxClosingFragment(inExpressionContext: boolean): JsxClosingFragment { + const node = createNode(SyntaxKind.JsxClosingFragment); + parseExpected(SyntaxKind.LessThanSlashToken); + if (tokenIsIdentifierOrKeyword(token())) { + const unexpectedTagName = parseJsxElementName(); + parseErrorAtPosition(unexpectedTagName.pos, unexpectedTagName.end - unexpectedTagName.pos, Diagnostics.Expected_corresponding_closing_tag_for_JSX_fragment); + } + if (inExpressionContext) { + parseExpected(SyntaxKind.GreaterThanToken); + } + else { + parseExpected(SyntaxKind.GreaterThanToken, /*diagnostic*/ undefined, /*shouldAdvance*/ false); + scanJsxText(); + } + return finishNode(node); + } + function parseTypeAssertion(): TypeAssertion { const node = createNode(SyntaxKind.TypeAssertionExpression); parseExpected(SyntaxKind.LessThanToken); diff --git a/src/compiler/scanner.ts b/src/compiler/scanner.ts index e9a336b29f388..2e1ca122479c0 100644 --- a/src/compiler/scanner.ts +++ b/src/compiler/scanner.ts @@ -11,6 +11,11 @@ namespace ts { return token >= SyntaxKind.Identifier; } + /* @internal */ + export function tokenIsIdentifierOrKeywordOrGreaterThan(token: SyntaxKind): boolean { + return token === SyntaxKind.GreaterThanToken || tokenIsIdentifierOrKeyword(token); + } + export interface Scanner { getStartPos(): number; getToken(): SyntaxKind; diff --git a/src/compiler/transformers/jsx.ts b/src/compiler/transformers/jsx.ts index bbe05afe87835..2be6cdef0bce9 100644 --- a/src/compiler/transformers/jsx.ts +++ b/src/compiler/transformers/jsx.ts @@ -41,6 +41,9 @@ namespace ts { case SyntaxKind.JsxSelfClosingElement: return visitJsxSelfClosingElement(node, /*isChild*/ false); + case SyntaxKind.JsxFragment: + return visitJsxFragment(node, /*isChild*/ false); + case SyntaxKind.JsxExpression: return visitJsxExpression(node); @@ -63,6 +66,9 @@ namespace ts { case SyntaxKind.JsxSelfClosingElement: return visitJsxSelfClosingElement(node, /*isChild*/ true); + case SyntaxKind.JsxFragment: + return visitJsxFragment(node, /*isChild*/ true); + default: Debug.failBadSyntaxKind(node); return undefined; @@ -77,6 +83,10 @@ namespace ts { return visitJsxOpeningLikeElement(node, /*children*/ undefined, isChild, /*location*/ node); } + function visitJsxFragment(node: JsxFragment, isChild: boolean) { + return visitJsxOpeningFragment(node.openingFragment, node.children, isChild, /*location*/ node); + } + function visitJsxOpeningLikeElement(node: JsxOpeningLikeElement, children: ReadonlyArray, isChild: boolean, location: TextRange) { const tagName = getTagName(node); let objectProperties: Expression; @@ -126,6 +136,22 @@ namespace ts { return element; } + function visitJsxOpeningFragment(node: JsxOpeningFragment, children: ReadonlyArray, isChild: boolean, location: TextRange) { + const element = createExpressionForJsxFragment( + context.getEmitResolver().getJsxFactoryEntity(), + compilerOptions.reactNamespace, + mapDefined(children, transformJsxChildToExpression), + node, + location + ); + + if (isChild) { + startOnNewLine(element); + } + + return element; + } + function transformJsxSpreadAttributeToExpression(node: JsxSpreadAttribute) { return visitNode(node.expression, visitor, isExpression); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 5d1b005860613..c842768c173d8 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -328,6 +328,9 @@ namespace ts { JsxSelfClosingElement, JsxOpeningElement, JsxClosingElement, + JsxFragment, + JsxOpeningFragment, + JsxClosingFragment, JsxAttribute, JsxAttributes, JsxSpreadAttribute, @@ -1621,7 +1624,7 @@ namespace ts { closingElement: JsxClosingElement; } - /// Either the opening tag in a ... pair, or the lone in a self-closing form + /// Either the opening tag in a ... pair or the lone in a self-closing form export type JsxOpeningLikeElement = JsxSelfClosingElement | JsxOpeningElement; export type JsxAttributeLike = JsxAttribute | JsxSpreadAttribute; @@ -1647,6 +1650,26 @@ namespace ts { attributes: JsxAttributes; } + /// A JSX expression of the form <>... + export interface JsxFragment extends PrimaryExpression { + kind: SyntaxKind.JsxFragment; + openingFragment: JsxOpeningFragment; + children: NodeArray; + closingFragment: JsxClosingFragment; + } + + /// The opening element of a <>... JsxFragment + export interface JsxOpeningFragment extends Expression { + kind: SyntaxKind.JsxOpeningFragment; + parent?: JsxFragment; + } + + /// The closing element of a <>... JsxFragment + export interface JsxClosingFragment extends Expression { + kind: SyntaxKind.JsxClosingFragment; + parent?: JsxFragment; + } + export interface JsxAttribute extends ObjectLiteralElement { kind: SyntaxKind.JsxAttribute; parent?: JsxAttributes; @@ -1680,7 +1703,7 @@ namespace ts { parent?: JsxElement; } - export type JsxChild = JsxText | JsxExpression | JsxElement | JsxSelfClosingElement; + export type JsxChild = JsxText | JsxExpression | JsxElement | JsxSelfClosingElement | JsxFragment; export interface Statement extends Node { _statementBrand: any; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index f3197f5501fae..a8864fa97e7dc 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -1262,6 +1262,7 @@ namespace ts { case SyntaxKind.OmittedExpression: case SyntaxKind.JsxElement: case SyntaxKind.JsxSelfClosingElement: + case SyntaxKind.JsxFragment: case SyntaxKind.YieldExpression: case SyntaxKind.AwaitExpression: case SyntaxKind.MetaProperty: @@ -2178,6 +2179,7 @@ namespace ts { case SyntaxKind.ClassExpression: case SyntaxKind.JsxElement: case SyntaxKind.JsxSelfClosingElement: + case SyntaxKind.JsxFragment: case SyntaxKind.RegularExpressionLiteral: case SyntaxKind.NoSubstitutionTemplateLiteral: case SyntaxKind.TemplateExpression: @@ -4802,6 +4804,18 @@ namespace ts { return node.kind === SyntaxKind.JsxClosingElement; } + export function isJsxFragment(node: Node): node is JsxFragment { + return node.kind === SyntaxKind.JsxFragment; + } + + export function isJsxOpeningFragment(node: Node): node is JsxOpeningFragment { + return node.kind === SyntaxKind.JsxOpeningFragment; + } + + export function isJsxClosingFragment(node: Node): node is JsxClosingFragment { + return node.kind === SyntaxKind.JsxClosingFragment; + } + export function isJsxAttribute(node: Node): node is JsxAttribute { return node.kind === SyntaxKind.JsxAttribute; } @@ -5327,6 +5341,7 @@ namespace ts { case SyntaxKind.CallExpression: case SyntaxKind.JsxElement: case SyntaxKind.JsxSelfClosingElement: + case SyntaxKind.JsxFragment: case SyntaxKind.TaggedTemplateExpression: case SyntaxKind.ArrayLiteralExpression: case SyntaxKind.ParenthesizedExpression: @@ -5648,7 +5663,8 @@ namespace ts { return kind === SyntaxKind.JsxElement || kind === SyntaxKind.JsxExpression || kind === SyntaxKind.JsxSelfClosingElement - || kind === SyntaxKind.JsxText; + || kind === SyntaxKind.JsxText + || kind === SyntaxKind.JsxFragment; } /* @internal */ diff --git a/src/compiler/visitor.ts b/src/compiler/visitor.ts index 7d46630e227d4..0428d2d2d2e06 100644 --- a/src/compiler/visitor.ts +++ b/src/compiler/visitor.ts @@ -819,6 +819,12 @@ namespace ts { return updateJsxClosingElement(node, visitNode((node).tagName, visitor, isJsxTagNameExpression)); + case SyntaxKind.JsxFragment: + return updateJsxFragment(node, + visitNode((node).openingFragment, visitor, isJsxOpeningFragment), + nodesVisitor((node).children, visitor, isJsxChild), + visitNode((node).closingFragment, visitor, isJsxClosingFragment)); + case SyntaxKind.JsxAttribute: return updateJsxAttribute(node, visitNode((node).name, visitor, isIdentifier), @@ -1334,6 +1340,12 @@ namespace ts { result = reduceNode((node).closingElement, cbNode, result); break; + case SyntaxKind.JsxFragment: + result = reduceNode((node).openingFragment, cbNode, result); + result = reduceLeft((node).children, cbNode, result); + result = reduceNode((node).closingFragment, cbNode, result); + break; + case SyntaxKind.JsxSelfClosingElement: case SyntaxKind.JsxOpeningElement: result = reduceNode((node).tagName, cbNode, result); diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index ac6a76aa82830..077dc0e92f1c0 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -313,46 +313,49 @@ declare namespace ts { JsxSelfClosingElement = 250, JsxOpeningElement = 251, JsxClosingElement = 252, - JsxAttribute = 253, - JsxAttributes = 254, - JsxSpreadAttribute = 255, - JsxExpression = 256, - CaseClause = 257, - DefaultClause = 258, - HeritageClause = 259, - CatchClause = 260, - PropertyAssignment = 261, - ShorthandPropertyAssignment = 262, - SpreadAssignment = 263, - EnumMember = 264, - SourceFile = 265, - Bundle = 266, - JSDocTypeExpression = 267, - JSDocAllType = 268, - JSDocUnknownType = 269, - JSDocNullableType = 270, - JSDocNonNullableType = 271, - JSDocOptionalType = 272, - JSDocFunctionType = 273, - JSDocVariadicType = 274, - JSDocComment = 275, - JSDocTag = 276, - JSDocAugmentsTag = 277, - JSDocClassTag = 278, - JSDocParameterTag = 279, - JSDocReturnTag = 280, - JSDocTypeTag = 281, - JSDocTemplateTag = 282, - JSDocTypedefTag = 283, - JSDocPropertyTag = 284, - JSDocTypeLiteral = 285, - SyntaxList = 286, - NotEmittedStatement = 287, - PartiallyEmittedExpression = 288, - CommaListExpression = 289, - MergeDeclarationMarker = 290, - EndOfDeclarationMarker = 291, - Count = 292, + JsxFragment = 253, + JsxOpeningFragment = 254, + JsxClosingFragment = 255, + JsxAttribute = 256, + JsxAttributes = 257, + JsxSpreadAttribute = 258, + JsxExpression = 259, + CaseClause = 260, + DefaultClause = 261, + HeritageClause = 262, + CatchClause = 263, + PropertyAssignment = 264, + ShorthandPropertyAssignment = 265, + SpreadAssignment = 266, + EnumMember = 267, + SourceFile = 268, + Bundle = 269, + JSDocTypeExpression = 270, + JSDocAllType = 271, + JSDocUnknownType = 272, + JSDocNullableType = 273, + JSDocNonNullableType = 274, + JSDocOptionalType = 275, + JSDocFunctionType = 276, + JSDocVariadicType = 277, + JSDocComment = 278, + JSDocTag = 279, + JSDocAugmentsTag = 280, + JSDocClassTag = 281, + JSDocParameterTag = 282, + JSDocReturnTag = 283, + JSDocTypeTag = 284, + JSDocTemplateTag = 285, + JSDocTypedefTag = 286, + JSDocPropertyTag = 287, + JSDocTypeLiteral = 288, + SyntaxList = 289, + NotEmittedStatement = 290, + PartiallyEmittedExpression = 291, + CommaListExpression = 292, + MergeDeclarationMarker = 293, + EndOfDeclarationMarker = 294, + Count = 295, FirstAssignment = 58, LastAssignment = 70, FirstCompoundAssignment = 59, @@ -378,10 +381,10 @@ declare namespace ts { FirstBinaryOperator = 27, LastBinaryOperator = 70, FirstNode = 143, - FirstJSDocNode = 267, - LastJSDocNode = 285, - FirstJSDocTagNode = 276, - LastJSDocTagNode = 285, + FirstJSDocNode = 270, + LastJSDocNode = 288, + FirstJSDocTagNode = 279, + LastJSDocTagNode = 288, } enum NodeFlags { None = 0, @@ -1061,6 +1064,20 @@ declare namespace ts { tagName: JsxTagNameExpression; attributes: JsxAttributes; } + interface JsxFragment extends PrimaryExpression { + kind: SyntaxKind.JsxFragment; + openingFragment: JsxOpeningFragment; + children: NodeArray; + closingFragment: JsxClosingFragment; + } + interface JsxOpeningFragment extends Expression { + kind: SyntaxKind.JsxOpeningFragment; + parent?: JsxFragment; + } + interface JsxClosingFragment extends Expression { + kind: SyntaxKind.JsxClosingFragment; + parent?: JsxFragment; + } interface JsxAttribute extends ObjectLiteralElement { kind: SyntaxKind.JsxAttribute; parent?: JsxAttributes; @@ -1088,7 +1105,7 @@ declare namespace ts { containsOnlyWhiteSpaces: boolean; parent?: JsxElement; } - type JsxChild = JsxText | JsxExpression | JsxElement | JsxSelfClosingElement; + type JsxChild = JsxText | JsxExpression | JsxElement | JsxSelfClosingElement | JsxFragment; interface Statement extends Node { _statementBrand: any; } @@ -3001,6 +3018,9 @@ declare namespace ts { function isJsxSelfClosingElement(node: Node): node is JsxSelfClosingElement; function isJsxOpeningElement(node: Node): node is JsxOpeningElement; function isJsxClosingElement(node: Node): node is JsxClosingElement; + function isJsxFragment(node: Node): node is JsxFragment; + function isJsxOpeningFragment(node: Node): node is JsxOpeningFragment; + function isJsxClosingFragment(node: Node): node is JsxClosingFragment; function isJsxAttribute(node: Node): node is JsxAttribute; function isJsxAttributes(node: Node): node is JsxAttributes; function isJsxSpreadAttribute(node: Node): node is JsxSpreadAttribute; @@ -3500,6 +3520,8 @@ declare namespace ts { function updateJsxOpeningElement(node: JsxOpeningElement, tagName: JsxTagNameExpression, attributes: JsxAttributes): JsxOpeningElement; function createJsxClosingElement(tagName: JsxTagNameExpression): JsxClosingElement; function updateJsxClosingElement(node: JsxClosingElement, tagName: JsxTagNameExpression): JsxClosingElement; + function createJsxFragment(openingFragment: JsxOpeningFragment, children: ReadonlyArray, closingFragment: JsxClosingFragment): JsxFragment; + function updateJsxFragment(node: JsxFragment, openingFragment: JsxOpeningFragment, children: ReadonlyArray, closingFragment: JsxClosingFragment): JsxFragment; function createJsxAttribute(name: Identifier, initializer: StringLiteral | JsxExpression): JsxAttribute; function updateJsxAttribute(node: JsxAttribute, name: Identifier, initializer: StringLiteral | JsxExpression): JsxAttribute; function createJsxAttributes(properties: ReadonlyArray): JsxAttributes; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 7fa342e6847ff..140078d707728 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -313,46 +313,49 @@ declare namespace ts { JsxSelfClosingElement = 250, JsxOpeningElement = 251, JsxClosingElement = 252, - JsxAttribute = 253, - JsxAttributes = 254, - JsxSpreadAttribute = 255, - JsxExpression = 256, - CaseClause = 257, - DefaultClause = 258, - HeritageClause = 259, - CatchClause = 260, - PropertyAssignment = 261, - ShorthandPropertyAssignment = 262, - SpreadAssignment = 263, - EnumMember = 264, - SourceFile = 265, - Bundle = 266, - JSDocTypeExpression = 267, - JSDocAllType = 268, - JSDocUnknownType = 269, - JSDocNullableType = 270, - JSDocNonNullableType = 271, - JSDocOptionalType = 272, - JSDocFunctionType = 273, - JSDocVariadicType = 274, - JSDocComment = 275, - JSDocTag = 276, - JSDocAugmentsTag = 277, - JSDocClassTag = 278, - JSDocParameterTag = 279, - JSDocReturnTag = 280, - JSDocTypeTag = 281, - JSDocTemplateTag = 282, - JSDocTypedefTag = 283, - JSDocPropertyTag = 284, - JSDocTypeLiteral = 285, - SyntaxList = 286, - NotEmittedStatement = 287, - PartiallyEmittedExpression = 288, - CommaListExpression = 289, - MergeDeclarationMarker = 290, - EndOfDeclarationMarker = 291, - Count = 292, + JsxFragment = 253, + JsxOpeningFragment = 254, + JsxClosingFragment = 255, + JsxAttribute = 256, + JsxAttributes = 257, + JsxSpreadAttribute = 258, + JsxExpression = 259, + CaseClause = 260, + DefaultClause = 261, + HeritageClause = 262, + CatchClause = 263, + PropertyAssignment = 264, + ShorthandPropertyAssignment = 265, + SpreadAssignment = 266, + EnumMember = 267, + SourceFile = 268, + Bundle = 269, + JSDocTypeExpression = 270, + JSDocAllType = 271, + JSDocUnknownType = 272, + JSDocNullableType = 273, + JSDocNonNullableType = 274, + JSDocOptionalType = 275, + JSDocFunctionType = 276, + JSDocVariadicType = 277, + JSDocComment = 278, + JSDocTag = 279, + JSDocAugmentsTag = 280, + JSDocClassTag = 281, + JSDocParameterTag = 282, + JSDocReturnTag = 283, + JSDocTypeTag = 284, + JSDocTemplateTag = 285, + JSDocTypedefTag = 286, + JSDocPropertyTag = 287, + JSDocTypeLiteral = 288, + SyntaxList = 289, + NotEmittedStatement = 290, + PartiallyEmittedExpression = 291, + CommaListExpression = 292, + MergeDeclarationMarker = 293, + EndOfDeclarationMarker = 294, + Count = 295, FirstAssignment = 58, LastAssignment = 70, FirstCompoundAssignment = 59, @@ -378,10 +381,10 @@ declare namespace ts { FirstBinaryOperator = 27, LastBinaryOperator = 70, FirstNode = 143, - FirstJSDocNode = 267, - LastJSDocNode = 285, - FirstJSDocTagNode = 276, - LastJSDocTagNode = 285, + FirstJSDocNode = 270, + LastJSDocNode = 288, + FirstJSDocTagNode = 279, + LastJSDocTagNode = 288, } enum NodeFlags { None = 0, @@ -1061,6 +1064,20 @@ declare namespace ts { tagName: JsxTagNameExpression; attributes: JsxAttributes; } + interface JsxFragment extends PrimaryExpression { + kind: SyntaxKind.JsxFragment; + openingFragment: JsxOpeningFragment; + children: NodeArray; + closingFragment: JsxClosingFragment; + } + interface JsxOpeningFragment extends Expression { + kind: SyntaxKind.JsxOpeningFragment; + parent?: JsxFragment; + } + interface JsxClosingFragment extends Expression { + kind: SyntaxKind.JsxClosingFragment; + parent?: JsxFragment; + } interface JsxAttribute extends ObjectLiteralElement { kind: SyntaxKind.JsxAttribute; parent?: JsxAttributes; @@ -1088,7 +1105,7 @@ declare namespace ts { containsOnlyWhiteSpaces: boolean; parent?: JsxElement; } - type JsxChild = JsxText | JsxExpression | JsxElement | JsxSelfClosingElement; + type JsxChild = JsxText | JsxExpression | JsxElement | JsxSelfClosingElement | JsxFragment; interface Statement extends Node { _statementBrand: any; } @@ -3056,6 +3073,9 @@ declare namespace ts { function isJsxSelfClosingElement(node: Node): node is JsxSelfClosingElement; function isJsxOpeningElement(node: Node): node is JsxOpeningElement; function isJsxClosingElement(node: Node): node is JsxClosingElement; + function isJsxFragment(node: Node): node is JsxFragment; + function isJsxOpeningFragment(node: Node): node is JsxOpeningFragment; + function isJsxClosingFragment(node: Node): node is JsxClosingFragment; function isJsxAttribute(node: Node): node is JsxAttribute; function isJsxAttributes(node: Node): node is JsxAttributes; function isJsxSpreadAttribute(node: Node): node is JsxSpreadAttribute; @@ -3447,6 +3467,8 @@ declare namespace ts { function updateJsxOpeningElement(node: JsxOpeningElement, tagName: JsxTagNameExpression, attributes: JsxAttributes): JsxOpeningElement; function createJsxClosingElement(tagName: JsxTagNameExpression): JsxClosingElement; function updateJsxClosingElement(node: JsxClosingElement, tagName: JsxTagNameExpression): JsxClosingElement; + function createJsxFragment(openingFragment: JsxOpeningFragment, children: ReadonlyArray, closingFragment: JsxClosingFragment): JsxFragment; + function updateJsxFragment(node: JsxFragment, openingFragment: JsxOpeningFragment, children: ReadonlyArray, closingFragment: JsxClosingFragment): JsxFragment; function createJsxAttribute(name: Identifier, initializer: StringLiteral | JsxExpression): JsxAttribute; function updateJsxAttribute(node: JsxAttribute, name: Identifier, initializer: StringLiteral | JsxExpression): JsxAttribute; function createJsxAttributes(properties: ReadonlyArray): JsxAttributes; diff --git a/tests/baselines/reference/checkJsxChildrenProperty14.errors.txt b/tests/baselines/reference/checkJsxChildrenProperty14.errors.txt new file mode 100644 index 0000000000000..ecae349e19e63 --- /dev/null +++ b/tests/baselines/reference/checkJsxChildrenProperty14.errors.txt @@ -0,0 +1,56 @@ +tests/cases/conformance/jsx/file.tsx(42,27): error TS2322: Type '{ a: 10; b: "hi"; children: Element[]; }' is not assignable to type 'IntrinsicAttributes & SingleChildProp'. + Type '{ a: 10; b: "hi"; children: Element[]; }' is not assignable to type 'SingleChildProp'. + Types of property 'children' are incompatible. + Type 'Element[]' is not assignable to type 'Element'. + Property 'type' is missing in type 'Element[]'. + + +==== tests/cases/conformance/jsx/file.tsx (1 errors) ==== + import React = require('react'); + + interface Prop { + a: number, + b: string, + children: JSX.Element | JSX.Element[]; + } + + class Button extends React.Component { + render() { + return (
My Button
) + } + } + + function AnotherButton(p: any) { + return

Just Another Button

; + } + + function Comp(p: Prop) { + return
{p.b}
; + } + + // OK + let k1 = <>