diff --git a/transpiler/src/main/java/org/jsweet/transpiler/Java2TypeScriptTranslator.java b/transpiler/src/main/java/org/jsweet/transpiler/Java2TypeScriptTranslator.java index e09ac2fc..8a71c9aa 100644 --- a/transpiler/src/main/java/org/jsweet/transpiler/Java2TypeScriptTranslator.java +++ b/transpiler/src/main/java/org/jsweet/transpiler/Java2TypeScriptTranslator.java @@ -243,7 +243,7 @@ public class Java2TypeScriptTranslator extends AbstractTreePrinter { private List localClasses = new ArrayList<>(); private List generatedMethodNames = new ArrayList<>(); - + // to be accessed in the parent scope private boolean isAnonymousClass = false; // to be accessed in the parent scope @@ -1461,8 +1461,9 @@ public class Java2TypeScriptTranslator extends AbstractTreePrinter { Map signatures = new HashMap<>(); for (MethodSymbol meth : methods) { if (meth.type instanceof MethodType) { - // do not generate default abstract method for already generated methods - if(getScope().generatedMethodNames.contains(meth.name.toString())) { + // do not generate default abstract method for already + // generated methods + if (getScope().generatedMethodNames.contains(meth.name.toString())) { continue; } MethodSymbol s = Util.findMethodDeclarationInType(getContext().types, classdecl.sym, @@ -2809,7 +2810,8 @@ public class Java2TypeScriptTranslator extends AbstractTreePrinter { print("[]"); } } else { - if (context.hasAnnotationType(varDecl.vartype.type.tsym, ANNOTATION_STRING_TYPE)) { + if (context.hasAnnotationType(varDecl.vartype.type.tsym, ANNOTATION_STRING_TYPE) + && !varDecl.vartype.type.tsym.isEnum()) { print("\""); print(context.getAnnotationValue(varDecl.vartype.type.tsym, ANNOTATION_STRING_TYPE, String.class, varDecl.vartype.type.tsym.name.toString()).toString()); diff --git a/transpiler/src/main/java/org/jsweet/transpiler/extension/AddPrefixToNonPublicMembersAdapter.java b/transpiler/src/main/java/org/jsweet/transpiler/extension/AddPrefixToNonPublicMembersAdapter.java index ad7779c8..973304f8 100644 --- a/transpiler/src/main/java/org/jsweet/transpiler/extension/AddPrefixToNonPublicMembersAdapter.java +++ b/transpiler/src/main/java/org/jsweet/transpiler/extension/AddPrefixToNonPublicMembersAdapter.java @@ -1,5 +1,23 @@ package org.jsweet.transpiler.extension; +/* + * JSweet transpiler - http://www.jsweet.org + * Copyright (C) 2015 CINCHEO SAS + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; @@ -8,6 +26,17 @@ import javax.lang.model.element.VariableElement; import org.jsweet.transpiler.util.Util; +/** + * This simple adapter renames non-public members by adding two underscores as a + * prefix. + * + *

+ * Note that this could be dangerous to use for protected fields if wanting to + * access them from subclasses declared in other JSweet projects. + * + * @author Renaud Pawlak + */ + public class AddPrefixToNonPublicMembersAdapter extends PrinterAdapter { public AddPrefixToNonPublicMembersAdapter(PrinterAdapter parentAdapter) { diff --git a/transpiler/src/main/java/org/jsweet/transpiler/extension/BigDecimalAdapter.java b/transpiler/src/main/java/org/jsweet/transpiler/extension/BigDecimalAdapter.java new file mode 100644 index 00000000..0fda80cb --- /dev/null +++ b/transpiler/src/main/java/org/jsweet/transpiler/extension/BigDecimalAdapter.java @@ -0,0 +1,110 @@ +/* + * JSweet transpiler - http://www.jsweet.org + * Copyright (C) 2015 CINCHEO SAS + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +package org.jsweet.transpiler.extension; + +import java.math.BigDecimal; + +import javax.lang.model.element.Element; + +import org.jsweet.transpiler.extension.PrinterAdapter; +import org.jsweet.transpiler.model.MethodInvocationElement; +import org.jsweet.transpiler.model.NewClassElement; + +/** + * This optional adapter tunes the JavaScript generation to map the Java's + * BigDecimal API to the Big JavaScript library. + * + *

+ * Warning: this adapter is not activated by default. See JSweet specifications + * to know how to activate this adapter. + * + *

+ * This extension requires the big.js candy to be available in the JSweet + * classpath: https://github.com/jsweet-candies/candy-bigjs. + * + * @author Renaud Pawlak + */ +public class BigDecimalAdapter extends PrinterAdapter { + + public BigDecimalAdapter(PrinterAdapter parent) { + super(parent); + // all BigDecimal types are mapped to Big + addTypeMapping(BigDecimal.class.getName(), "Big"); + } + + @Override + public boolean substituteNewClass(NewClassElement newClass) { + String className = newClass.getTypeAsElement().toString(); + // map the BigDecimal constructors + if (BigDecimal.class.getName().equals(className)) { + print("new Big(").printArgList(newClass.getArguments()).print(")"); + return true; + } + // delegate to the adapter chain + return super.substituteNewClass(newClass); + } + + @Override + public boolean substituteMethodInvocation(MethodInvocationElement invocation) { + if (invocation.getTargetExpression() != null) { + Element targetType = invocation.getTargetExpression().getTypeAsElement(); + if (BigDecimal.class.getName().equals(targetType.toString())) { + // BigDecimal methods are mapped to their Big.js equivalent + switch (invocation.getMethodName()) { + case "multiply": + printMacroName(invocation.getMethodName()); + print(invocation.getTargetExpression()).print(".times(").printArgList(invocation.getArguments()) + .print(")"); + return true; + case "add": + printMacroName(invocation.getMethodName()); + print(invocation.getTargetExpression()).print(".plus(").printArgList(invocation.getArguments()) + .print(")"); + return true; + case "scale": + printMacroName(invocation.getMethodName()); + // we assume that we always have a scale of 2, which is a + // good default if we deal with currencies... + // to be changed/implemented further + print("2"); + return true; + case "setScale": + printMacroName(invocation.getMethodName()); + print(invocation.getTargetExpression()).print(".round(").print(invocation.getArguments().get(0)) + .print(")"); + return true; + case "compareTo": + printMacroName(invocation.getMethodName()); + print(invocation.getTargetExpression()).print(".cmp(").print(invocation.getArguments().get(0)) + .print(")"); + return true; + case "equals": + printMacroName(invocation.getMethodName()); + print(invocation.getTargetExpression()).print(".eq(").print(invocation.getArguments().get(0)) + .print(")"); + return true; + } + } + + } + // delegate to the adapter chain + return super.substituteMethodInvocation(invocation); + } + +} diff --git a/transpiler/src/main/java/org/jsweet/transpiler/extension/MapAdapter.java b/transpiler/src/main/java/org/jsweet/transpiler/extension/MapAdapter.java new file mode 100644 index 00000000..ce1535b7 --- /dev/null +++ b/transpiler/src/main/java/org/jsweet/transpiler/extension/MapAdapter.java @@ -0,0 +1,119 @@ +/* + * JSweet transpiler - http://www.jsweet.org + * Copyright (C) 2015 CINCHEO SAS + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +package org.jsweet.transpiler.extension; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.Hashtable; +import java.util.Map; +import java.util.TreeMap; + +import javax.lang.model.element.Element; + +import org.jsweet.transpiler.model.MethodInvocationElement; +import org.jsweet.transpiler.model.NewClassElement; + +/** + * This optional adapter tunes the JavaScript generation to use JavaScript ES6 + * maps to implement Java maps. + * + *

+ * Note that this is a partial implementation that shall be extended to support + * more cases and adapted to your own requirements. Additionally, this + * implementation generates untyped JavaScript in order to avoid having to have + * the ES6 API in the compilation path. + * + * @author Renaud Pawlak + */ +public class MapAdapter extends PrinterAdapter { + + static String[] mapTypes = { Map.class.getName(), HashMap.class.getName(), TreeMap.class.getName(), + Hashtable.class.getName() }; + + public MapAdapter(PrinterAdapter parent) { + super(parent); + // rewrite all Java map and compatible map implementations types + // note that we rewrite to 'any' because we don't want to require the + // ES6 API to compile (all subsequent accesses will be untyped) + for (String mapType : mapTypes) { + addTypeMapping(mapType, "any"); + } + } + + @Override + public boolean substituteNewClass(NewClassElement newClass) { + String className = newClass.getTypeAsElement().toString(); + // map the map constructor to the global 'Map' variable (untyped access) + if (Arrays.binarySearch(mapTypes, className) >= 0) { + // this access is browser/node-compatible + print("new (typeof window == 'undefined'?global:window)['Map'](").printArgList(newClass.getArguments()).print(")"); + return true; + } + // delegate to the adapter chain + return super.substituteNewClass(newClass); + } + + @Override + public boolean substituteMethodInvocation(MethodInvocationElement invocation) { + if (invocation.getTargetExpression() != null) { + Element targetType = invocation.getTargetExpression().getTypeAsElement(); + if (Arrays.binarySearch(mapTypes, targetType.toString()) >= 0) { + // Java Map methods are mapped to their JavaScript equivalent + switch (invocation.getMethodName()) { + case "put": + printMacroName(invocation.getMethodName()); + print(invocation.getTargetExpression()).print(".set(").printArgList(invocation.getArguments()) + .print(")"); + return true; + // although 'get' has the same name, we still rewrite it in case + // another extension would provide it's own implementation + case "get": + printMacroName(invocation.getMethodName()); + print(invocation.getTargetExpression()).print(".get(").printArgList(invocation.getArguments()) + .print(")"); + return true; + case "containsKey": + printMacroName(invocation.getMethodName()); + print(invocation.getTargetExpression()).print(".has(").printArgList(invocation.getArguments()) + .print(")"); + return true; + // we use the ES6 'Array.from' method in an untyped way to + // transform the iterator in an array + case "keySet": + printMacroName(invocation.getMethodName()); + print("(Array).from(").print(invocation.getTargetExpression()).print(".keys())"); + return true; + case "values": + printMacroName(invocation.getMethodName()); + print("(Array).from(").print(invocation.getTargetExpression()).print(".values())"); + return true; + // in ES6 maps, 'size' is a property, not a method + case "size": + printMacroName(invocation.getMethodName()); + print(invocation.getTargetExpression()).print(".size"); + return true; + } + } + + } + // delegate to the adapter chain + return super.substituteMethodInvocation(invocation); + } + +} diff --git a/transpiler/src/main/java/org/jsweet/transpiler/extension/StringEnumAdapter.java b/transpiler/src/main/java/org/jsweet/transpiler/extension/StringEnumAdapter.java new file mode 100644 index 00000000..2bd4356b --- /dev/null +++ b/transpiler/src/main/java/org/jsweet/transpiler/extension/StringEnumAdapter.java @@ -0,0 +1,123 @@ +/* + * JSweet transpiler - http://www.jsweet.org + * Copyright (C) 2015 CINCHEO SAS + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + */ +package org.jsweet.transpiler.extension; + +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; + +import org.jsweet.JSweetConfig; +import org.jsweet.transpiler.model.CaseElement; +import org.jsweet.transpiler.model.ExtendedElement; +import org.jsweet.transpiler.model.MethodInvocationElement; +import org.jsweet.transpiler.model.VariableAccessElement; + +/** + * This optional adapter tunes the JavaScript generation to remove enums and + * replace them with strings. It only applies to enums that are annotated with + * @StringType. + * + *

+ * For instance: @StringType enum MyEnum { A, B, C } will be erased + * and all subsequent accesses to the enum will be mapped to simple strings. + * + *

+ * Typically, the method declaration void m(MyEnum e) {...} will be + * mapped to void m(e : string) {...}. And of course, the + * invocation xxx.m(MyEnum.A) will be mapped to + * xxx.m("A"). + * + *

+ * Warning: this adapter is not activated by default. See JSweet specifications + * to know how to activate this adapter. + * + * @author Renaud Pawlak + */ +public class StringEnumAdapter extends PrinterAdapter { + + private boolean isStringEnum(Element element) { + // note: this function could be improved to exclude enums that have + // fields or methods other than the enum constants + return element.getKind() == ElementKind.ENUM && hasAnnotationType(element, JSweetConfig.ANNOTATION_STRING_TYPE); + } + + public StringEnumAdapter(PrinterAdapter parent) { + super(parent); + // eligible enums will be translated to string in JS + addTypeMapping((typeTree, name) -> isStringEnum(typeTree.getTypeAsElement()) ? "string" : null); + + // ignore enum declarations with a programmatic annotation manager + addAnnotationManager(new AnnotationManager() { + @Override + public Action manageAnnotation(Element element, String annotationType) { + // add the @Erased annotation to string enums + return JSweetConfig.ANNOTATION_ERASED.equals(annotationType) && isStringEnum(element) ? Action.ADD + : Action.VOID; + } + }); + + } + + @Override + public boolean substituteMethodInvocation(MethodInvocationElement invocation) { + if (invocation.getTargetExpression() != null) { + Element targetType = invocation.getTargetExpression().getTypeAsElement(); + // enum API must be erased and use plain strings instead + if (isStringEnum(targetType)) { + switch (invocation.getMethodName()) { + case "name": + printMacroName(invocation.getMethodName()); + print(invocation.getTargetExpression()); + return true; + case "valueOf": + printMacroName(invocation.getMethodName()); + print(invocation.getArgument(0)); + return true; + case "equals": + printMacroName(invocation.getMethodName()); + print("(").print(invocation.getTargetExpression()).print(" == ") + .print(invocation.getArguments().get(0)).print(")"); + return true; + } + } + } + return super.substituteMethodInvocation(invocation); + } + + @Override + public boolean substituteVariableAccess(VariableAccessElement variableAccess) { + // accessing an enum field is replaced by a simple string value + // (MyEnum.A => "A") + if (isStringEnum(variableAccess.getTargetElement())) { + print("\"" + variableAccess.getVariableName() + "\""); + return true; + } + return super.substituteVariableAccess(variableAccess); + } + + @Override + public boolean substituteCaseStatementPattern(CaseElement caseStatement, ExtendedElement pattern) { + // map enums to strings in case statements + if (isStringEnum(pattern.getTypeAsElement())) { + print("\"" + pattern + "\""); + return true; + } + return super.substituteCaseStatementPattern(caseStatement, pattern); + } + +} diff --git a/transpiler/src/test/java/org/jsweet/test/transpiler/EnumTests.java b/transpiler/src/test/java/org/jsweet/test/transpiler/EnumTests.java index a57954f0..92093df6 100644 --- a/transpiler/src/test/java/org/jsweet/test/transpiler/EnumTests.java +++ b/transpiler/src/test/java/org/jsweet/test/transpiler/EnumTests.java @@ -22,6 +22,8 @@ import org.jsweet.transpiler.JSweetContext; import org.jsweet.transpiler.JSweetFactory; import org.jsweet.transpiler.ModuleKind; import org.jsweet.transpiler.extension.Java2TypeScriptAdapter; +import org.jsweet.transpiler.extension.PrinterAdapter; +import org.jsweet.transpiler.extension.StringEnumAdapter; import org.junit.Assert; import org.junit.Test; @@ -33,6 +35,7 @@ import source.enums.ComplexEnums; import source.enums.EnumInSamePackage; import source.enums.Enums; import source.enums.ErasedEnum; +import source.enums.StringEnums; import source.enums.other.EnumInOtherPackage; public class EnumTests extends AbstractTest { @@ -87,7 +90,7 @@ public class EnumTests extends AbstractTest { logHandler.assertNoProblems(); }, getSourceFile(ComplexInnerEnums.class)); } - + @Test public void testComplexEnumWithAbstractMethods() { eval((logHandler, r) -> { @@ -137,4 +140,18 @@ public class EnumTests extends AbstractTest { }, getSourceFile(EnumWithStatics.class)); } + @Test + public void testStringEnums() { + createTranspiler(new JSweetFactory() { + @Override + public PrinterAdapter createAdapter(JSweetContext context) { + return new StringEnumAdapter(super.createAdapter(context)); + } + }); + eval((logHandler, r) -> { + logHandler.assertNoProblems(); + }, getSourceFile(StringEnums.class)); + createTranspiler(new JSweetFactory()); + } + } diff --git a/transpiler/src/test/java/org/jsweet/test/transpiler/ExtensionTests.java b/transpiler/src/test/java/org/jsweet/test/transpiler/ExtensionTests.java index b290e274..31186181 100644 --- a/transpiler/src/test/java/org/jsweet/test/transpiler/ExtensionTests.java +++ b/transpiler/src/test/java/org/jsweet/test/transpiler/ExtensionTests.java @@ -12,13 +12,18 @@ import org.jsweet.transpiler.JSweetContext; import org.jsweet.transpiler.JSweetFactory; import org.jsweet.transpiler.ModuleKind; import org.jsweet.transpiler.extension.Java2TypeScriptAdapter; +import org.jsweet.transpiler.extension.MapAdapter; +import org.jsweet.transpiler.extension.PrinterAdapter; import org.jsweet.transpiler.extension.RemoveJavaDependenciesAdapter; +import org.jsweet.transpiler.extension.StringEnumAdapter; import org.junit.AfterClass; import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; +import source.enums.StringEnums; import source.extension.AnnotationTest; +import source.extension.Maps; class TestFactory extends JSweetFactory { @@ -105,4 +110,19 @@ public class ExtensionTests extends AbstractTest { }, getSourceFile(AnnotationTest.class)); } + @Test + public void testMaps() { + createTranspiler(new JSweetFactory() { + @Override + public PrinterAdapter createAdapter(JSweetContext context) { + return new MapAdapter(super.createAdapter(context)); + } + }); + eval((logHandler, r) -> { + logHandler.assertNoProblems(); + }, getSourceFile(Maps.class)); + createTranspiler(new JSweetFactory()); + + } + } \ No newline at end of file diff --git a/transpiler/src/test/java/source/enums/StringEnums.java b/transpiler/src/test/java/source/enums/StringEnums.java new file mode 100644 index 00000000..e5cfa63e --- /dev/null +++ b/transpiler/src/test/java/source/enums/StringEnums.java @@ -0,0 +1,32 @@ +package source.enums; + +import static jsweet.util.Lang.any; + +import jsweet.lang.StringType; + +public class StringEnums { + + public static void main(String[] args) { + m(MyStringEnum.A); + } + + public static void m(MyStringEnum e) { + assert any(e) == "A"; + switch (e) { + case A: + break; + case B: + case C: + default: + assert false; + } + } + +} + +@StringType +enum MyStringEnum { + + A, B, C + +} diff --git a/transpiler/src/test/java/source/extension/Maps.java b/transpiler/src/test/java/source/extension/Maps.java new file mode 100644 index 00000000..293aab68 --- /dev/null +++ b/transpiler/src/test/java/source/extension/Maps.java @@ -0,0 +1,45 @@ +package source.extension; + +import java.util.HashMap; + +import def.js.Array; + +/** + * This test is executed without any Java runtime. + */ +public class Maps { + + static Array trace = new Array<>(); + + static String key1() { + return "1"; + } + + static String key2() { + return "a"; + } + + public static void main(String[] args) { + HashMap m = new HashMap<>(); + + m.put(key1(), "a"); + m.put("2", "b"); + +// for(Entry e : m.entrySet()) { +// +// trace.push("" + e.getKey()); +// trace.push("" + e.getValue()); +// } + + assert 2 == m.size(); + assert "a" == m.get("1"); + + assert m.containsKey("2"); + + assert m.keySet().toString() == "[1, 2]"; + + assert m.values().toString() == "[a, b]"; + + } + +}