Skip to content

Commit

Permalink
GH-2596: As a developer I need React jsx files using the new JSX tran…
Browse files Browse the repository at this point in the history
…sform (#2602)

* rename folder

* add transpile test

* add tests

* related changes

* main change

* adjust tests

* fix fragment support, import specifier

* fix side effect on target

* fix react test imports

* add validation and test

* use spread instead of Object.assign

* revert unintended change

* fix missing value for boolean prop

* fix test expectation

* fix treatment of wrapped cancellation exception
  • Loading branch information
mmews-n4 authored Feb 14, 2024
1 parent 5e99990 commit 5a79f6f
Show file tree
Hide file tree
Showing 28 changed files with 676 additions and 190 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,27 @@
*/
package org.eclipse.n4js.transpiler.es.transform;

import static org.eclipse.n4js.tooling.react.ReactHelper.REACT_ELEMENT_PROPERTY_CHILDREN_NAME;
import static org.eclipse.n4js.tooling.react.ReactHelper.REACT_ELEMENT_PROPERTY_KEY_NAME;
import static org.eclipse.n4js.tooling.react.ReactHelper.REACT_JSX_RUNTIME_NAME;
import static org.eclipse.n4js.tooling.react.ReactHelper.REACT_JSX_TRANSFORM_NAME;
import static org.eclipse.n4js.transpiler.TranspilerBuilderBlocks._ArrLit;
import static org.eclipse.n4js.transpiler.TranspilerBuilderBlocks._CallExpr;
import static org.eclipse.n4js.transpiler.TranspilerBuilderBlocks._NULL;
import static org.eclipse.n4js.transpiler.TranspilerBuilderBlocks._ObjLit;
import static org.eclipse.n4js.transpiler.TranspilerBuilderBlocks._PropertyAccessExpr;
import static org.eclipse.n4js.transpiler.TranspilerBuilderBlocks._PropertyNameValuePair;
import static org.eclipse.n4js.transpiler.TranspilerBuilderBlocks._PropertySpread;
import static org.eclipse.n4js.transpiler.TranspilerBuilderBlocks._StringLiteral;
import static org.eclipse.n4js.transpiler.TranspilerBuilderBlocks._TRUE;
import static org.eclipse.xtext.xbase.lib.IterableExtensions.filter;
import static org.eclipse.xtext.xbase.lib.IterableExtensions.map;
import static org.eclipse.xtext.xbase.lib.IterableExtensions.toList;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.eclipse.emf.common.util.EList;
import org.eclipse.n4js.n4JS.Expression;
import org.eclipse.n4js.n4JS.ImportDeclaration;
import org.eclipse.n4js.n4JS.JSXAbstractElement;
Expand All @@ -35,18 +42,17 @@
import org.eclipse.n4js.n4JS.JSXPropertyAttribute;
import org.eclipse.n4js.n4JS.JSXSpreadAttribute;
import org.eclipse.n4js.n4JS.NamespaceImportSpecifier;
import org.eclipse.n4js.n4JS.ObjectLiteral;
import org.eclipse.n4js.n4JS.ParameterizedCallExpression;
import org.eclipse.n4js.n4JS.PropertyAssignment;
import org.eclipse.n4js.n4JS.PropertyNameValuePair;
import org.eclipse.n4js.tooling.react.ReactHelper;
import org.eclipse.n4js.transpiler.Transformation;
import org.eclipse.n4js.transpiler.im.IdentifierRef_IM;
import org.eclipse.n4js.transpiler.im.ImFactory;
import org.eclipse.n4js.transpiler.im.Script_IM;
import org.eclipse.n4js.transpiler.im.SymbolTableEntry;
import org.eclipse.n4js.transpiler.im.SymbolTableEntryInternal;
import org.eclipse.n4js.transpiler.im.SymbolTableEntryOriginal;
import org.eclipse.n4js.ts.types.IdentifiableElement;
import org.eclipse.n4js.ts.types.TFunction;
import org.eclipse.n4js.ts.types.TModule;
import org.eclipse.n4js.utils.ResourceType;
import org.eclipse.xtext.EcoreUtil2;
Expand All @@ -69,9 +75,10 @@
* </pre>
*/
public class JSXTransformation extends Transformation {
/** Alias for React transform */
public static final String JSX_ALIAS = "$" + REACT_JSX_TRANSFORM_NAME;

private SymbolTableEntryOriginal steForJsxBackendNamespace;
private SymbolTableEntryOriginal steForJsxBackendElementFactoryFunction;
private SymbolTableEntryOriginal steForJsxBackendFragmentComponent;

@Inject
Expand Down Expand Up @@ -124,24 +131,29 @@ public void transform() {
return; // this transformation is not applicable
}

// Transform JSXFragments and JSXElements
// note: we are passing 'true' to #collectNodes(), i.e. we are searching for nested elements
List<JSXAbstractElement> jsxAbstractElements = collectNodes(getState().im, JSXAbstractElement.class, true);
if (jsxAbstractElements.isEmpty()) {
// Nothing to transform
return;
}

steForJsxBackendNamespace = prepareImportOfJsxBackend();
steForJsxBackendElementFactoryFunction = prepareElementFactoryFunction();
createImportOfJsx();
steForJsxBackendNamespace = createImportOfJsxBackend(); // will be removed if obsolete
steForJsxBackendFragmentComponent = prepareFragmentComponent();

// note: we are passing 'true' to #collectNodes(), i.e. we are searching for nested elements
// Transform JSXFragments and JSXElements
for (JSXAbstractElement jsxElem : jsxAbstractElements) {
transformJSXAbstractElement(jsxElem);
}
}

private SymbolTableEntryOriginal prepareImportOfJsxBackend() {
private void createImportOfJsx() {
ImportDeclaration impDecl = addNamedImport(REACT_JSX_TRANSFORM_NAME, JSX_ALIAS, REACT_JSX_RUNTIME_NAME);
impDecl.getImportSpecifiers().forEach(is -> is.setFlaggedUsedInCode(true));
}

private SymbolTableEntryOriginal createImportOfJsxBackend() {
TModule jsxBackendModule = reactHelper.getJsxBackendModule(getState().resource);
if (jsxBackendModule == null) {
throw new RuntimeException("cannot locate JSX backend for N4JSX resource " + getState().resource.getURI());
Expand Down Expand Up @@ -169,15 +181,6 @@ private SymbolTableEntryOriginal prepareImportOfJsxBackend() {
return addNamespaceImport(jsxBackendModule, reactHelper.getJsxBackendNamespaceName());
}

private SymbolTableEntryOriginal prepareElementFactoryFunction() {
TFunction elementFactoryFunction = reactHelper.getJsxBackendElementFactoryFunction(getState().resource);
if (elementFactoryFunction == null) {
throw new RuntimeException("cannot locate element factory function of JSX backend for N4JSX resource "
+ getState().resource.getURI());
}
return getSymbolTableEntryOriginal(elementFactoryFunction, true);
}

private SymbolTableEntryOriginal prepareFragmentComponent() {
IdentifiableElement fragmentComponent = reactHelper.getJsxBackendFragmentComponent(getState().resource);
if (fragmentComponent == null) {
Expand All @@ -202,90 +205,95 @@ private ParameterizedCallExpression convertJSXAbstractElement(JSXAbstractElement
if (elem instanceof JSXElement) {
JSXElement jsxElem = (JSXElement) elem;
args.add(getTagNameFromElement(jsxElem));
args.add(convertJSXAttributes(jsxElem.getJsxAttributes()));
} else {
args.add(convertJSXAttributes(jsxElem.getJsxAttributes(), elem.getJsxChildren()));
Expression keysValue = findKeysAttribute(jsxElem.getJsxAttributes());
if (keysValue != null) {
args.add(keysValue);
}
} else if (elem instanceof JSXFragment) {
args.add(_PropertyAccessExpr(steForJsxBackendNamespace, steForJsxBackendFragmentComponent));
args.add(_NULL());
args.add(convertJSXAttributes(Collections.emptyList(), elem.getJsxChildren()));
}
args.addAll(toList(map(elem.getJsxChildren(), child -> convertJSXChild(child))));

return _CallExpr(
_PropertyAccessExpr(steForJsxBackendNamespace, steForJsxBackendElementFactoryFunction),
args.toArray(new Expression[0]));
IdentifierRef_IM idRef = ImFactory.eINSTANCE.createIdentifierRef_IM();
idRef.setIdAsText(JSX_ALIAS);
SymbolTableEntryInternal ste = getSymbolTableEntryInternal(idRef.getIdAsText(), true);
idRef.setId_IM(ste);
return _CallExpr(idRef, args.toArray(new Expression[0]));
}

private Expression convertJSXChild(JSXChild child) {
if (child instanceof JSXElement) {
return convertJSXAbstractElement((JSXElement) child);
}
if (child instanceof JSXFragment) {
return convertJSXAbstractElement((JSXFragment) child);
}
if (child instanceof JSXExpression) {
return ((JSXExpression) child).getExpression();
private Expression findKeysAttribute(EList<JSXAttribute> jsxAttributes) {
for (JSXAttribute attr : jsxAttributes) {
if (attr instanceof JSXPropertyAttribute) {
JSXPropertyAttribute pa = (JSXPropertyAttribute) attr;
// https://github.com/reactjs/rfcs/blob/createlement-rfc/text/0000-create-element-changes.md#motivation
// notes that the key property will not be extracted from attributes
// at some time in the future
if (REACT_ELEMENT_PROPERTY_KEY_NAME.equals(pa.getPropertyAsText())) {
return pa.getJsxAttributeValue();
}
}
}
return null;
}

// Generate Object.assign({}, {foo, bar: "Hi"}, spr)
private Expression convertJSXAttributes(List<JSXAttribute> attrs) {
if (attrs.isEmpty()) {
return _NULL();
} else if (attrs.size() == 1 && attrs.get(0) instanceof JSXSpreadAttribute) {
// Generate {foo:foo, ...spread, bar: "Hi", children: []}
private Expression convertJSXAttributes(List<JSXAttribute> attrs, List<JSXChild> children) {
if (children.isEmpty() && attrs.isEmpty()) {
return _ObjLit();
}
if (children.isEmpty() && attrs.size() == 1 && attrs.get(0) instanceof JSXSpreadAttribute) {
// Special case: if only a single spread operator is passed, we pass it directly, e.g. spr instead of
// cloning with Object.assign.
// cloning.
return ((JSXSpreadAttribute) attrs.get(0)).getExpression();
} else {
}

List<PropertyAssignment> pas = new ArrayList<>();

List<Integer> spreadIndices = new ArrayList<>();
for (int idx = 0; idx < attrs.size(); idx++) {
if (attrs.get(idx) instanceof JSXSpreadAttribute) {
spreadIndices.add(idx);
for (JSXAttribute attr : attrs) {
if (attr instanceof JSXSpreadAttribute) {
JSXSpreadAttribute sAttr = (JSXSpreadAttribute) attr;
pas.add(_PropertySpread(sAttr.getExpression()));
} else if (attr instanceof JSXPropertyAttribute) {
JSXPropertyAttribute pAttr = (JSXPropertyAttribute) attr;
if (!children.isEmpty() && REACT_ELEMENT_PROPERTY_CHILDREN_NAME.equals(pAttr.getPropertyAsText())) {
continue;
}
}
// GHOLD-413: We have to make sure that the only properties locating next to each other are combined.
// Moreover, the order of properties as well as spread operators must be preserved!
List<PropertyNameValuePair> props = new ArrayList<>();
if (attrs.get(0) instanceof JSXSpreadAttribute) {
// The first attribute is a spread object, the target must be {}.
} else {
// Otherwise, the target is of the form {foo: true, bar: "Hi"}
int firstSpreadIndex = (!spreadIndices.isEmpty()) ? spreadIndices.get(0) : attrs.size();
for (int i = 0; i < firstSpreadIndex; i++) {
props.add(convertJSXAttribute((JSXPropertyAttribute) attrs.get(i)));
if (REACT_ELEMENT_PROPERTY_KEY_NAME.equals(pAttr.getPropertyAsText())) {
continue;
}
pas.add(_PropertyNameValuePair(
getNameFromPropertyAttribute(pAttr),
getValueExpressionFromPropertyAttribute(pAttr)));
}
ObjectLiteral target = _ObjLit(props.toArray(new PropertyNameValuePair[0]));

List<Expression> parameters = new ArrayList<>();
parameters.add(target);
}

for (int i = 0; i < spreadIndices.size(); i++) {
int curSpreadIdx = spreadIndices.get(i);
// Spread expression passed is used directly
parameters.add(((JSXSpreadAttribute) attrs.get(curSpreadIdx)).getExpression());
// Combine properties between spread intervals
int nextSpreadIdx = (i < spreadIndices.size() - 1) ? spreadIndices.get(i + 1)
: attrs.size();
List<JSXAttribute> propsBetweenTwoSpreads = attrs.subList(curSpreadIdx + 1, nextSpreadIdx);
if (!propsBetweenTwoSpreads.isEmpty()) {
List<PropertyAssignment> props2 = new ArrayList<>();
for (JSXAttribute attr : propsBetweenTwoSpreads) {
props2.add(convertJSXAttribute((JSXPropertyAttribute) attr));
}
parameters.add(_ObjLit(props2.toArray(new PropertyAssignment[0])));
}
if (!children.isEmpty()) {
Expression childrenValue;
if (children.size() == 1) {
childrenValue = convertJSXChild(children.get(0));
} else {
childrenValue = _ArrLit(
toList(map(children, child -> convertJSXChild(child))).toArray(new Expression[0]));
}

return _CallExpr(_PropertyAccessExpr(steFor_Object(), steFor_Object_assign()),
parameters.toArray(new Expression[0]));
// this will cause any other custom property children to be overwritten
pas.add(_PropertyNameValuePair(REACT_ELEMENT_PROPERTY_CHILDREN_NAME, childrenValue));
}

return _ObjLit(pas.toArray(new PropertyAssignment[0]));
}

private PropertyNameValuePair convertJSXAttribute(JSXPropertyAttribute attr) {
return _PropertyNameValuePair(
getNameFromPropertyAttribute(attr),
getValueExpressionFromPropertyAttribute(attr));
private Expression convertJSXChild(JSXChild child) {
if (child instanceof JSXElement) {
return convertJSXAbstractElement((JSXElement) child);
}
if (child instanceof JSXFragment) {
return convertJSXAbstractElement((JSXFragment) child);
}
if (child instanceof JSXExpression) {
return ((JSXExpression) child).getExpression();
}
return null;
}

private Expression getTagNameFromElement(JSXElement elem) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
import java.util.Map;
import java.util.Objects;

import org.apache.log4j.Logger;
import org.eclipse.n4js.n4JS.ImportDeclaration;
import org.eclipse.n4js.n4JS.ModuleSpecifierForm;
import org.eclipse.n4js.packagejson.projectDescription.ProjectType;
import org.eclipse.n4js.tooling.react.ReactHelper;
import org.eclipse.n4js.transpiler.Transformation;
import org.eclipse.n4js.ts.types.TModule;
import org.eclipse.n4js.utils.DeclMergingUtils;
Expand All @@ -39,6 +41,7 @@
* For details, see {@link #computeModuleSpecifierForOutputCode(ImportDeclaration)}.
*/
public class ModuleSpecifierTransformation extends Transformation {
private final static Logger LOGGER = Logger.getLogger(ModuleSpecifierTransformation.class);

@Inject
private WorkspaceAccess workspaceAccess;
Expand Down Expand Up @@ -121,6 +124,17 @@ private void transformImportDecl(ImportDeclaration importDeclIM) {
private String computeModuleSpecifierForOutputCode(ImportDeclaration importDeclIM) {
TModule targetModule = getState().info.getImportedModule(importDeclIM);

if (targetModule == null) {
if (ReactHelper.REACT_JSX_RUNTIME_NAME.equals(importDeclIM.getModuleSpecifierAsText())) {
// expected to happen since this import was added by JSXTransformation
return importDeclIM.getModuleSpecifierAsText();
}
// fallback, should not happen
LOGGER.error("targetModule is null at import declaration with module specifier: "
+ importDeclIM.getModuleSpecifierAsText());
return importDeclIM.getModuleSpecifierAsText();
}

if (URIUtils.isVirtualResourceURI(targetModule.eResource().getURI())
&& !DeclMergingUtils.isModuleAugmentation(targetModule)) {
// SPECIAL CASE #1a
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,10 @@ private boolean isUsed(ImportSpecifier importSpec) {
// add new usages of an existing import)
// -> therefore simply return false (i.e. unused)
return false;
} else if (importSpec instanceof NamedImportSpecifier
&& JSXTransformation.JSX_ALIAS.equals(((NamedImportSpecifier) importSpec).getAlias())) {
// special case for import that is added in JSXTransformation
return true;
} else {
// for performance reasons, we do not require the flaggedUsedInCode to be set to false if usages are removed
// -> therefore have to check for references now
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@
import org.eclipse.n4js.n4JS.PropertyNameKind;
import org.eclipse.n4js.n4JS.PropertyNameOwner;
import org.eclipse.n4js.n4JS.PropertyNameValuePair;
import org.eclipse.n4js.n4JS.PropertySpread;
import org.eclipse.n4js.n4JS.RelationalExpression;
import org.eclipse.n4js.n4JS.RelationalOperator;
import org.eclipse.n4js.n4JS.ReturnStatement;
Expand Down Expand Up @@ -474,6 +475,12 @@ public static PropertyNameValuePair _PropertyNameValuePair(LiteralOrComputedProp
return result;
}

public static PropertySpread _PropertySpread(Expression value) {
PropertySpread result = N4JSFactory.eINSTANCE.createPropertySpread();
result.setExpression(value);
return result;
}

public static PropertyGetterDeclaration _PropertyGetterDecl(String name, Statement... stmnts) {
PropertyGetterDeclaration result = N4JSFactory.eINSTANCE.createPropertyGetterDeclaration();
result.setDeclaredName(_LiteralOrComputedPropertyName(name));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import org.eclipse.n4js.n4JS.FormalParameter;
import org.eclipse.n4js.n4JS.FunctionDeclaration;
import org.eclipse.n4js.n4JS.FunctionExpression;
import org.eclipse.n4js.n4JS.ImportDeclaration;
import org.eclipse.n4js.n4JS.ImportSpecifier;
import org.eclipse.n4js.n4JS.N4ClassDeclaration;
import org.eclipse.n4js.n4JS.N4EnumDeclaration;
Expand Down Expand Up @@ -108,6 +109,12 @@ public void addNamedImport(SymbolTableEntryOriginal steOfElementToImport, String
TranspilerStateOperations.addNamedImport(state, steOfElementToImport, aliasOrNull);
}

/** See {@link TranspilerStateOperations#addNamedImport(TranspilerState, String, String, String)}. */
public ImportDeclaration addNamedImport(String elementNameToImport, String aliasOrNull,
String moduleSpecifierName) {
return TranspilerStateOperations.addNamedImport(state, elementNameToImport, aliasOrNull, moduleSpecifierName);
}

/** See {@link TranspilerStateOperations#addEmptyImport(TranspilerState, String)}. */
public void addEmptyImport(String moduleSpecifier) {
TranspilerStateOperations.addEmptyImport(state, moduleSpecifier);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,32 @@ public static void addNamedImport(TranspilerState state, SymbolTableEntryOrigina
state.info.setImportedModule(importDecl, moduleOfOriginalTarget);
}

/**
* Creates a new named import for the given STE and adds it to the intermediate model of the given transpiler state.
* Note that this method does not perform a binding to an existing target module.
* <p>
* IMPORTANT: this method does not check if the given element name or alias is unique (i.e. does not avoid name
* clashes!).
*/
public static ImportDeclaration addNamedImport(TranspilerState state, String elementNameToImport,
String aliasOrNull, String moduleSpecifierName) {

// 1) create import declaration & specifier
NamedImportSpecifier importSpec = _NamedImportSpecifier(elementNameToImport, aliasOrNull, true);
ImportDeclaration importDecl = _ImportDecl(importSpec);
importDecl.setModuleSpecifierAsText(moduleSpecifierName);

// 2) add import to intermediate model
EList<ScriptElement> scriptElements = state.im.getScriptElements();
if (scriptElements.isEmpty()) {
scriptElements.add(importDecl);
} else {
insertBefore(scriptElements.get(0), importDecl);
}

return importDecl;
}

/**
* Adds an "empty" import to the intermediate model, i.e. an import of the form:
*
Expand Down
Loading

0 comments on commit 5a79f6f

Please sign in to comment.