Skip to content

fix(46305): Keep spread syntax instead of generating Object.assign for ReactJSX and es2018+ #46317

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Oct 14, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
132 changes: 50 additions & 82 deletions src/compiler/transformers/jsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,59 +200,27 @@ namespace ts {
}

function convertJsxChildrenToChildrenPropObject(children: readonly JsxChild[]) {
const prop = convertJsxChildrenToChildrenPropAssignment(children);
return prop && factory.createObjectLiteralExpression([prop]);
}

function convertJsxChildrenToChildrenPropAssignment(children: readonly JsxChild[]) {
const nonWhitespaceChildren = getSemanticJsxChildren(children);
if (length(nonWhitespaceChildren) === 1) {
const result = transformJsxChildToExpression(nonWhitespaceChildren[0]);
return result && factory.createObjectLiteralExpression([
factory.createPropertyAssignment("children", result)
]);
return result && factory.createPropertyAssignment("children", result);
}
const result = mapDefined(children, transformJsxChildToExpression);
return !result.length ? undefined : factory.createObjectLiteralExpression([
factory.createPropertyAssignment("children", factory.createArrayLiteralExpression(result))
]);
return length(result) ? factory.createPropertyAssignment("children", factory.createArrayLiteralExpression(result)) : undefined;
}

function visitJsxOpeningLikeElementJSX(node: JsxOpeningLikeElement, children: readonly JsxChild[] | undefined, isChild: boolean, location: TextRange) {
const tagName = getTagName(node);
let objectProperties: Expression;
const childrenProp = children && children.length ? convertJsxChildrenToChildrenPropAssignment(children) : undefined;
const keyAttr = find(node.attributes.properties, p => !!p.name && isIdentifier(p.name) && p.name.escapedText === "key") as JsxAttribute | undefined;
const attrs = keyAttr ? filter(node.attributes.properties, p => p !== keyAttr) : node.attributes.properties;

let segments: Expression[] = [];
if (attrs.length) {
// Map spans of JsxAttribute nodes into object literals and spans
// of JsxSpreadAttribute nodes into expressions.
segments = flatten(
spanMap(attrs, isJsxSpreadAttribute, (attrs, isSpread) => isSpread
? map(attrs, transformJsxSpreadAttributeToExpression)
: factory.createObjectLiteralExpression(map(attrs, transformJsxAttributeToObjectLiteralElement))
)
);

if (isJsxSpreadAttribute(attrs[0])) {
// We must always emit at least one object literal before a spread
// argument.factory.createObjectLiteral
segments.unshift(factory.createObjectLiteralExpression());
}
}
if (children && children.length) {
const result = convertJsxChildrenToChildrenPropObject(children);
if (result) {
segments.push(result);
}
}

if (segments.length === 0) {
objectProperties = factory.createObjectLiteralExpression([]);
// When there are no attributes, React wants {}
}
else {
// Either emit one big object literal (no spread attribs), or
// a call to the __assign helper.
objectProperties = singleOrUndefined(segments) || emitHelpers().createAssignHelper(segments);
}

const objectProperties = length(attrs) ? transformJsxAttributesToObjectProps(attrs, childrenProp) :
factory.createObjectLiteralExpression(childrenProp ? [childrenProp] : emptyArray); // When there are no attributes, React wants {}
return visitJsxOpeningLikeElementOrFragmentJSX(tagName, objectProperties, keyAttr, length(getSemanticJsxChildren(children || emptyArray)), isChild, location);
}

Expand Down Expand Up @@ -285,47 +253,9 @@ namespace ts {

function visitJsxOpeningLikeElementCreateElement(node: JsxOpeningLikeElement, children: readonly JsxChild[] | undefined, isChild: boolean, location: TextRange) {
const tagName = getTagName(node);
let objectProperties: Expression | undefined;
const attrs = node.attributes.properties;
if (attrs.length === 0) {
objectProperties = factory.createNull();
// When there are no attributes, React wants "null"
}
else {
const target = getEmitScriptTarget(compilerOptions);
if (target && target >= ScriptTarget.ES2018) {
objectProperties = factory.createObjectLiteralExpression(
flatten<SpreadAssignment | PropertyAssignment>(
spanMap(attrs, isJsxSpreadAttribute, (attrs, isSpread) =>
isSpread ? map(attrs, transformJsxSpreadAttributeToSpreadAssignment) : map(attrs, transformJsxAttributeToObjectLiteralElement)
)
)
);
}
else {
// Map spans of JsxAttribute nodes into object literals and spans
// of JsxSpreadAttribute nodes into expressions.
const segments = flatten<Expression | ObjectLiteralExpression>(
spanMap(attrs, isJsxSpreadAttribute, (attrs, isSpread) => isSpread
? map(attrs, transformJsxSpreadAttributeToExpression)
: factory.createObjectLiteralExpression(map(attrs, transformJsxAttributeToObjectLiteralElement))
)
);

if (isJsxSpreadAttribute(attrs[0])) {
// We must always emit at least one object literal before a spread
// argument.factory.createObjectLiteral
segments.unshift(factory.createObjectLiteralExpression());
}

// Either emit one big object literal (no spread attribs), or
// a call to the __assign helper.
objectProperties = singleOrUndefined(segments);
if (!objectProperties) {
objectProperties = emitHelpers().createAssignHelper(segments);
}
}
}
const objectProperties = length(attrs) ? transformJsxAttributesToObjectProps(attrs) :
factory.createNull(); // When there are no attributes, React wants "null"

const callee = currentFileState.importSpecifier === undefined
? createJsxFactoryExpression(
Expand Down Expand Up @@ -392,6 +322,44 @@ namespace ts {
return factory.createSpreadAssignment(visitNode(node.expression, visitor, isExpression));
}

function transformJsxAttributesToObjectProps(attrs: readonly(JsxSpreadAttribute | JsxAttribute)[], children?: PropertyAssignment) {
const target = getEmitScriptTarget(compilerOptions);
return target && target >= ScriptTarget.ES2018 ? factory.createObjectLiteralExpression(transformJsxAttributesToProps(attrs, children)) :
transformJsxAttributesToExpression(attrs, children);
}

function transformJsxAttributesToProps(attrs: readonly(JsxSpreadAttribute | JsxAttribute)[], children?: PropertyAssignment) {
const props = flatten<SpreadAssignment | PropertyAssignment>(spanMap(attrs, isJsxSpreadAttribute, (attrs, isSpread) =>
map(attrs, attr => isSpread ? transformJsxSpreadAttributeToSpreadAssignment(attr as JsxSpreadAttribute) : transformJsxAttributeToObjectLiteralElement(attr as JsxAttribute))));
if (children) {
props.push(children);
}
return props;
}

function transformJsxAttributesToExpression(attrs: readonly(JsxSpreadAttribute | JsxAttribute)[], children?: PropertyAssignment) {
// Map spans of JsxAttribute nodes into object literals and spans
// of JsxSpreadAttribute nodes into expressions.
const expressions = flatten(
spanMap(attrs, isJsxSpreadAttribute, (attrs, isSpread) => isSpread
? map(attrs, transformJsxSpreadAttributeToExpression)
: factory.createObjectLiteralExpression(map(attrs, transformJsxAttributeToObjectLiteralElement))
)
);

if (isJsxSpreadAttribute(attrs[0])) {
// We must always emit at least one object literal before a spread
// argument.factory.createObjectLiteral
expressions.unshift(factory.createObjectLiteralExpression());
}

if (children) {
expressions.push(factory.createObjectLiteralExpression([children]));
}

return singleOrUndefined(expressions) || emitHelpers().createAssignHelper(expressions);
}

function transformJsxSpreadAttributeToExpression(node: JsxSpreadAttribute) {
return visitNode(node.expression, visitor, isExpression);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
//// [test.tsx]
/// <reference path="/.lib/react16.d.ts" />

export function T1(a: any) {
return <div className={"T1"} { ...a }>T1</div>;
}

export function T2(a: any, b: any) {
return <div className={"T2"} { ...a } { ...b }>T2</div>;
}

export function T3(a: any, b: any) {
return <div { ...a } className={"T3"} { ...b }>T3</div>;
}

export function T4(a: any, b: any) {
return <div className={"T4"} { ...{ ...a, ...b } }>T4</div>;
}

export function T5(a: any, b: any, c: any, d: any) {
return <div className={"T5"} { ...{ ...a, ...b, ...{ c, d } } }>T5</div>;
}

export function T6(a: any, b: any, c: any, d: any) {
return <div className={"T6"} { ...{ ...a, ...b, ...{ ...c, ...d } } }>T6</div>;
}

export function T7(a: any, b: any, c: any, d: any) {
return <div>T7</div>;
}


//// [test.js]
import { jsx as _jsx } from "react/jsx-runtime";
/// <reference path="react16.d.ts" />
export function T1(a) {
return _jsx("div", Object.assign({ className: "T1" }, a, { children: "T1" }), void 0);
}
export function T2(a, b) {
return _jsx("div", Object.assign({ className: "T2" }, a, b, { children: "T2" }), void 0);
}
export function T3(a, b) {
return _jsx("div", Object.assign({}, a, { className: "T3" }, b, { children: "T3" }), void 0);
}
export function T4(a, b) {
return _jsx("div", Object.assign({ className: "T4" }, Object.assign(Object.assign({}, a), b), { children: "T4" }), void 0);
}
export function T5(a, b, c, d) {
return _jsx("div", Object.assign({ className: "T5" }, Object.assign(Object.assign(Object.assign({}, a), b), { c, d }), { children: "T5" }), void 0);
}
export function T6(a, b, c, d) {
return _jsx("div", Object.assign({ className: "T6" }, Object.assign(Object.assign(Object.assign({}, a), b), Object.assign(Object.assign({}, c), d)), { children: "T6" }), void 0);
}
export function T7(a, b, c, d) {
return _jsx("div", { children: "T7" }, void 0);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
=== tests/cases/conformance/jsx/test.tsx ===
/// <reference path="react16.d.ts" />

export function T1(a: any) {
>T1 : Symbol(T1, Decl(test.tsx, 0, 0))
>a : Symbol(a, Decl(test.tsx, 2, 19))

return <div className={"T1"} { ...a }>T1</div>;
>div : Symbol(JSX.IntrinsicElements.div, Decl(react16.d.ts, 2546, 114))
>className : Symbol(className, Decl(test.tsx, 3, 15))
>a : Symbol(a, Decl(test.tsx, 2, 19))
>div : Symbol(JSX.IntrinsicElements.div, Decl(react16.d.ts, 2546, 114))
}

export function T2(a: any, b: any) {
>T2 : Symbol(T2, Decl(test.tsx, 4, 1))
>a : Symbol(a, Decl(test.tsx, 6, 19))
>b : Symbol(b, Decl(test.tsx, 6, 26))

return <div className={"T2"} { ...a } { ...b }>T2</div>;
>div : Symbol(JSX.IntrinsicElements.div, Decl(react16.d.ts, 2546, 114))
>className : Symbol(className, Decl(test.tsx, 7, 15))
>a : Symbol(a, Decl(test.tsx, 6, 19))
>b : Symbol(b, Decl(test.tsx, 6, 26))
>div : Symbol(JSX.IntrinsicElements.div, Decl(react16.d.ts, 2546, 114))
}

export function T3(a: any, b: any) {
>T3 : Symbol(T3, Decl(test.tsx, 8, 1))
>a : Symbol(a, Decl(test.tsx, 10, 19))
>b : Symbol(b, Decl(test.tsx, 10, 26))

return <div { ...a } className={"T3"} { ...b }>T3</div>;
>div : Symbol(JSX.IntrinsicElements.div, Decl(react16.d.ts, 2546, 114))
>a : Symbol(a, Decl(test.tsx, 10, 19))
>className : Symbol(className, Decl(test.tsx, 11, 24))
>b : Symbol(b, Decl(test.tsx, 10, 26))
>div : Symbol(JSX.IntrinsicElements.div, Decl(react16.d.ts, 2546, 114))
}

export function T4(a: any, b: any) {
>T4 : Symbol(T4, Decl(test.tsx, 12, 1))
>a : Symbol(a, Decl(test.tsx, 14, 19))
>b : Symbol(b, Decl(test.tsx, 14, 26))

return <div className={"T4"} { ...{ ...a, ...b } }>T4</div>;
>div : Symbol(JSX.IntrinsicElements.div, Decl(react16.d.ts, 2546, 114))
>className : Symbol(className, Decl(test.tsx, 15, 15))
>a : Symbol(a, Decl(test.tsx, 14, 19))
>b : Symbol(b, Decl(test.tsx, 14, 26))
>div : Symbol(JSX.IntrinsicElements.div, Decl(react16.d.ts, 2546, 114))
}

export function T5(a: any, b: any, c: any, d: any) {
>T5 : Symbol(T5, Decl(test.tsx, 16, 1))
>a : Symbol(a, Decl(test.tsx, 18, 19))
>b : Symbol(b, Decl(test.tsx, 18, 26))
>c : Symbol(c, Decl(test.tsx, 18, 34))
>d : Symbol(d, Decl(test.tsx, 18, 42))

return <div className={"T5"} { ...{ ...a, ...b, ...{ c, d } } }>T5</div>;
>div : Symbol(JSX.IntrinsicElements.div, Decl(react16.d.ts, 2546, 114))
>className : Symbol(className, Decl(test.tsx, 19, 15))
>a : Symbol(a, Decl(test.tsx, 18, 19))
>b : Symbol(b, Decl(test.tsx, 18, 26))
>c : Symbol(c, Decl(test.tsx, 19, 56))
>d : Symbol(d, Decl(test.tsx, 19, 59))
>div : Symbol(JSX.IntrinsicElements.div, Decl(react16.d.ts, 2546, 114))
}

export function T6(a: any, b: any, c: any, d: any) {
>T6 : Symbol(T6, Decl(test.tsx, 20, 1))
>a : Symbol(a, Decl(test.tsx, 22, 19))
>b : Symbol(b, Decl(test.tsx, 22, 26))
>c : Symbol(c, Decl(test.tsx, 22, 34))
>d : Symbol(d, Decl(test.tsx, 22, 42))

return <div className={"T6"} { ...{ ...a, ...b, ...{ ...c, ...d } } }>T6</div>;
>div : Symbol(JSX.IntrinsicElements.div, Decl(react16.d.ts, 2546, 114))
>className : Symbol(className, Decl(test.tsx, 23, 15))
>a : Symbol(a, Decl(test.tsx, 22, 19))
>b : Symbol(b, Decl(test.tsx, 22, 26))
>c : Symbol(c, Decl(test.tsx, 22, 34))
>d : Symbol(d, Decl(test.tsx, 22, 42))
>div : Symbol(JSX.IntrinsicElements.div, Decl(react16.d.ts, 2546, 114))
}

export function T7(a: any, b: any, c: any, d: any) {
>T7 : Symbol(T7, Decl(test.tsx, 24, 1))
>a : Symbol(a, Decl(test.tsx, 26, 19))
>b : Symbol(b, Decl(test.tsx, 26, 26))
>c : Symbol(c, Decl(test.tsx, 26, 34))
>d : Symbol(d, Decl(test.tsx, 26, 42))

return <div>T7</div>;
>div : Symbol(JSX.IntrinsicElements.div, Decl(react16.d.ts, 2546, 114))
>div : Symbol(JSX.IntrinsicElements.div, Decl(react16.d.ts, 2546, 114))
}

Loading