Skip to content

Commit

Permalink
Optimize the transformed output of JSXSpreadAttributes containing a…
Browse files Browse the repository at this point in the history
…n `ObjectLiteralExpression` (#49100)
  • Loading branch information
Andarist authored Mar 21, 2023
1 parent 01de788 commit 3f90887
Show file tree
Hide file tree
Showing 21 changed files with 1,319 additions and 40 deletions.
79 changes: 57 additions & 22 deletions src/compiler/transformers/jsx.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,12 @@ import {
isJsxSelfClosingElement,
isJsxSpreadAttribute,
isLineBreak,
isObjectLiteralExpression,
isPropertyAssignment,
isSourceFile,
isSpreadAssignment,
isStringDoubleQuoted,
isStringLiteral,
isWhiteSpaceSingleLine,
JsxAttribute,
JsxAttributeValue,
Expand All @@ -55,6 +59,8 @@ import {
mapDefined,
Node,
NodeFlags,
ObjectLiteralElementLike,
ObjectLiteralExpression,
PropertyAssignment,
ScriptTarget,
setIdentifierGeneratedImportReference,
Expand All @@ -63,7 +69,6 @@ import {
singleOrUndefined,
SourceFile,
spanMap,
SpreadAssignment,
startOnNewLine,
Statement,
StringLiteral,
Expand Down Expand Up @@ -243,13 +248,18 @@ export function transformJsx(context: TransformationContext): (x: SourceFile | B
}
}

function hasProto(obj: ObjectLiteralExpression) {
return obj.properties.some(p => isPropertyAssignment(p) &&
(isIdentifier(p.name) && idText(p.name) === "__proto__" || isStringLiteral(p.name) && p.name.text === "__proto__"));
}

/**
* The react jsx/jsxs transform falls back to `createElement` when an explicit `key` argument comes after a spread
*/
function hasKeyAfterPropsSpread(node: JsxOpeningLikeElement) {
let spread = false;
for (const elem of node.attributes.properties) {
if (isJsxSpreadAttribute(elem)) {
if (isJsxSpreadAttribute(elem) && (!isObjectLiteralExpression(elem.expression) || elem.expression.properties.some(isSpreadAssignment))) {
spread = true;
}
else if (spread && isJsxAttribute(elem) && elem.name.escapedText === "key") {
Expand Down Expand Up @@ -427,7 +437,10 @@ export function transformJsx(context: TransformationContext): (x: SourceFile | B
return element;
}

function transformJsxSpreadAttributeToSpreadAssignment(node: JsxSpreadAttribute) {
function transformJsxSpreadAttributeToProps(node: JsxSpreadAttribute) {
if (isObjectLiteralExpression(node.expression) && !hasProto(node.expression)) {
return node.expression.properties;
}
return factory.createSpreadAssignment(Debug.checkDefined(visitNode(node.expression, visitor, isExpression)));
}

Expand All @@ -438,39 +451,61 @@ export function transformJsx(context: TransformationContext): (x: SourceFile | B
}

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))));
const props = flatten(spanMap(attrs, isJsxSpreadAttribute, (attrs, isSpread) =>
flatten(map(attrs, attr => isSpread ? transformJsxSpreadAttributeToProps(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 as JsxSpreadAttribute[], transformJsxSpreadAttributeToExpression)
: factory.createObjectLiteralExpression(map(attrs as JsxAttribute[], transformJsxAttributeToObjectLiteralElement))
)
);

if (isJsxSpreadAttribute(attrs[0])) {
// We must always emit at least one object literal before a spread
// argument.factory.createObjectLiteral
expressions.unshift(factory.createObjectLiteralExpression());
const expressions: Expression[] = [];
let properties: ObjectLiteralElementLike[] = [];

for (const attr of attrs) {
if (isJsxSpreadAttribute(attr)) {
// as an optimization we try to flatten the first level of spread inline object
// as if its props would be passed as JSX attributes
if (isObjectLiteralExpression(attr.expression) && !hasProto(attr.expression)) {
for (const prop of attr.expression.properties) {
if (isSpreadAssignment(prop)) {
finishObjectLiteralIfNeeded();
expressions.push(prop.expression);
continue;
}
properties.push(prop);
}
continue;
}
finishObjectLiteralIfNeeded();
expressions.push(attr.expression);
continue;
}
properties.push(transformJsxAttributeToObjectLiteralElement(attr));
}

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

finishObjectLiteralIfNeeded();

if (expressions.length && !isObjectLiteralExpression(expressions[0])) {
// We must always emit at least one object literal before a spread attribute
// as the JSX always factory expects a fresh object, so we need to make a copy here
// we also avoid mutating an external reference by doing this (first expression is used as assign's target)
expressions.unshift(factory.createObjectLiteralExpression());
}

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

function transformJsxSpreadAttributeToExpression(node: JsxSpreadAttribute) {
return Debug.checkDefined(visitNode(node.expression, visitor, isExpression));
function finishObjectLiteralIfNeeded() {
if (properties.length) {
expressions.push(factory.createObjectLiteralExpression(properties));
properties = [];
}
}
}

function transformJsxAttributeToObjectLiteralElement(node: JsxAttribute) {
Expand Down
43 changes: 40 additions & 3 deletions tests/baselines/reference/tsxEmitSpreadAttribute(target=es2015).js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,28 @@ export function T5(a: any, b: any, c: any, d: any) {
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 className={"T7"} { ...{ __proto__: null, dir: 'rtl' } }>T7</div>;
}

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

declare const __proto__: string;

export function T9(a: any, b: any, c: any, d: any) {
return <div className={"T9"} { ...{ [__proto__]: null } }>T9</div>;
}

export function T10(a: any, b: any, c: any, d: any) {
return <div className={"T10"} { ...{ ["__proto__"]: null } }>T10</div>;
}

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


//// [test.js]
Expand All @@ -37,11 +59,26 @@ export function T3(a, b) {
return React.createElement("div", Object.assign({}, a, { className: "T3" }, b), "T3");
}
export function T4(a, b) {
return React.createElement("div", Object.assign({ className: "T4" }, Object.assign(Object.assign({}, a), b)), "T4");
return React.createElement("div", Object.assign({ className: "T4" }, a, b), "T4");
}
export function T5(a, b, c, d) {
return React.createElement("div", Object.assign({ className: "T5" }, Object.assign(Object.assign(Object.assign({}, a), b), { c, d })), "T5");
return React.createElement("div", Object.assign({ className: "T5" }, a, b, { c, d }), "T5");
}
export function T6(a, b, c, d) {
return React.createElement("div", Object.assign({ className: "T6" }, Object.assign(Object.assign(Object.assign({}, a), b), Object.assign(Object.assign({}, c), d))), "T6");
return React.createElement("div", Object.assign({ className: "T6" }, a, b, Object.assign(Object.assign({}, c), d)), "T6");
}
export function T7(a, b, c, d) {
return React.createElement("div", Object.assign({ className: "T7" }, { __proto__: null, dir: 'rtl' }), "T7");
}
export function T8(a, b, c, d) {
return React.createElement("div", Object.assign({ className: "T8" }, { "__proto__": null }), "T8");
}
export function T9(a, b, c, d) {
return React.createElement("div", { className: "T9", [__proto__]: null }, "T9");
}
export function T10(a, b, c, d) {
return React.createElement("div", { className: "T10", ["__proto__"]: null }, "T10");
}
export function T11(a, b, c, d) {
return React.createElement("div", { className: "T11", __proto__ }, "T11");
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,69 @@ export function T6(a: any, b: any, c: any, d: any) {
>d : Symbol(d, Decl(test.tsx, 22, 42))
}

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 className={"T7"} { ...{ __proto__: null, dir: 'rtl' } }>T7</div>;
>className : Symbol(className, Decl(test.tsx, 27, 15))
>__proto__ : Symbol(__proto__, Decl(test.tsx, 27, 39))
>dir : Symbol(dir, Decl(test.tsx, 27, 56))
}

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

return <div className={"T8"} { ...{ "__proto__": null } }>T8</div>;
>className : Symbol(className, Decl(test.tsx, 31, 15))
>"__proto__" : Symbol("__proto__", Decl(test.tsx, 31, 39))
}

declare const __proto__: string;
>__proto__ : Symbol(__proto__, Decl(test.tsx, 34, 13))

export function T9(a: any, b: any, c: any, d: any) {
>T9 : Symbol(T9, Decl(test.tsx, 34, 32))
>a : Symbol(a, Decl(test.tsx, 36, 19))
>b : Symbol(b, Decl(test.tsx, 36, 26))
>c : Symbol(c, Decl(test.tsx, 36, 34))
>d : Symbol(d, Decl(test.tsx, 36, 42))

return <div className={"T9"} { ...{ [__proto__]: null } }>T9</div>;
>className : Symbol(className, Decl(test.tsx, 37, 15))
>[__proto__] : Symbol([__proto__], Decl(test.tsx, 37, 39))
>__proto__ : Symbol(__proto__, Decl(test.tsx, 34, 13))
}

export function T10(a: any, b: any, c: any, d: any) {
>T10 : Symbol(T10, Decl(test.tsx, 38, 1))
>a : Symbol(a, Decl(test.tsx, 40, 20))
>b : Symbol(b, Decl(test.tsx, 40, 27))
>c : Symbol(c, Decl(test.tsx, 40, 35))
>d : Symbol(d, Decl(test.tsx, 40, 43))

return <div className={"T10"} { ...{ ["__proto__"]: null } }>T10</div>;
>className : Symbol(className, Decl(test.tsx, 41, 15))
>["__proto__"] : Symbol(["__proto__"], Decl(test.tsx, 41, 40))
>"__proto__" : Symbol(["__proto__"], Decl(test.tsx, 41, 40))
}

export function T11(a: any, b: any, c: any, d: any) {
>T11 : Symbol(T11, Decl(test.tsx, 42, 1))
>a : Symbol(a, Decl(test.tsx, 44, 20))
>b : Symbol(b, Decl(test.tsx, 44, 27))
>c : Symbol(c, Decl(test.tsx, 44, 35))
>d : Symbol(d, Decl(test.tsx, 44, 43))

return <div className={"T11"} { ...{ __proto__ } }>T11</div>;
>className : Symbol(className, Decl(test.tsx, 45, 15))
>__proto__ : Symbol(__proto__, Decl(test.tsx, 45, 40))
}

Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,95 @@ export function T6(a: any, b: any, c: any, d: any) {
>div : any
}

export function T7(a: any, b: any, c: any, d: any) {
>T7 : (a: any, b: any, c: any, d: any) => any
>a : any
>b : any
>c : any
>d : any

return <div className={"T7"} { ...{ __proto__: null, dir: 'rtl' } }>T7</div>;
><div className={"T7"} { ...{ __proto__: null, dir: 'rtl' } }>T7</div> : error
>div : any
>className : string
>"T7" : "T7"
>{ __proto__: null, dir: 'rtl' } : { __proto__: null; dir: string; }
>__proto__ : null
>dir : string
>'rtl' : "rtl"
>div : any
}

export function T8(a: any, b: any, c: any, d: any) {
>T8 : (a: any, b: any, c: any, d: any) => any
>a : any
>b : any
>c : any
>d : any

return <div className={"T8"} { ...{ "__proto__": null } }>T8</div>;
><div className={"T8"} { ...{ "__proto__": null } }>T8</div> : error
>div : any
>className : string
>"T8" : "T8"
>{ "__proto__": null } : { __proto__: null; }
>"__proto__" : null
>div : any
}

declare const __proto__: string;
>__proto__ : string

export function T9(a: any, b: any, c: any, d: any) {
>T9 : (a: any, b: any, c: any, d: any) => any
>a : any
>b : any
>c : any
>d : any

return <div className={"T9"} { ...{ [__proto__]: null } }>T9</div>;
><div className={"T9"} { ...{ [__proto__]: null } }>T9</div> : error
>div : any
>className : string
>"T9" : "T9"
>{ [__proto__]: null } : { [x: string]: null; }
>[__proto__] : null
>__proto__ : string
>div : any
}

export function T10(a: any, b: any, c: any, d: any) {
>T10 : (a: any, b: any, c: any, d: any) => any
>a : any
>b : any
>c : any
>d : any

return <div className={"T10"} { ...{ ["__proto__"]: null } }>T10</div>;
><div className={"T10"} { ...{ ["__proto__"]: null } }>T10</div> : error
>div : any
>className : string
>"T10" : "T10"
>{ ["__proto__"]: null } : { __proto__: null; }
>["__proto__"] : null
>"__proto__" : "__proto__"
>div : any
}

export function T11(a: any, b: any, c: any, d: any) {
>T11 : (a: any, b: any, c: any, d: any) => any
>a : any
>b : any
>c : any
>d : any

return <div className={"T11"} { ...{ __proto__ } }>T11</div>;
><div className={"T11"} { ...{ __proto__ } }>T11</div> : error
>div : any
>className : string
>"T11" : "T11"
>{ __proto__ } : { __proto__: string; }
>__proto__ : string
>div : any
}

Loading

0 comments on commit 3f90887

Please sign in to comment.