google / j2cl

Java to Closure JavaScript transpiler
Apache License 2.0
1.23k stars 145 forks source link

Enum.valueOf() support? #90

Open niloc132 opened 4 years ago

niloc132 commented 4 years ago

Enum.valueOf() (and the corresponding Class.getEnumConstants()) isn't supported in j2cl, but there are TODOs in the code suggesting that this might be feasible, and perhaps would be accepted as an external contribution?

https://github.com/google/j2cl/blob/d555a3350bd9e374686138b0c484207a363aefa0/jre/java/java/lang/Enum.java#L56 https://github.com/google/j2cl/blob/d555a3350bd9e374686138b0c484207a363aefa0/jre/java/java/lang/Class.java#L100-L103

Having not yet tried, my hunch was that this might have been left out due to its interaction with @JsEnum https://github.com/google/j2cl/blob/d555a3350bd9e374686138b0c484207a363aefa0/transpiler/java/com/google/j2cl/generator/JavaScriptImplGenerator.java#L597-L601? Otherwise, it looks like the array literal returned by SomeEnum.values() (via com.google.j2cl.frontend.common.EnumMethodsCreator#createValuesMethod) could possibly be passed made into an additional argument for Util.$setClassMetadataForEnum so it is available on the Class itself?

rluble commented 4 years ago

The main issue is not the ability to implement it but rather how to implement it without losing optimization. As with other reflective features, if not done very carefully the result in a big loss of optimizations. In this case it, a naive implementation will result in all enum values for all enums being kept and not optimized away.

To implement this properly might require some restrictions and compiler transformations to ensure optimizability.

niloc132 commented 4 years ago

Thanks Roberto.

Can you outline briefly what your acceptance criteria for a feature like this might look like? Keeping in mind of course that some parts of //transpiler/javatests can't be built in open source (com/google/j2cl/transpiler/readable and com/google/j2cl/transpiler/regression), plus all of //jre/javatests.

At least in my head, it seems plausible that this could be as simple as adding a simple test to com.google.j2cl.transpiler.optimization.EnumOptimizationTest to ensure getUnusedEnumValues still passes for an enum with unreferenced values, even if OtherEnum.valueOf(...) and Enum.valueOf(OtherEnum.class,...) is called, so that we are sure that only this specific enum gets its values array retained through the whole optimization process?

rluble commented 4 years ago

Without restrictions (or a specific global optimizations in jscompiler) a single use of an utility method like

void myMethod(Class c) {
  .....
  x =   Enum.valueOf(c, "...");
 ...
}

would retain all unreferenced values on all enums.

As I before said implementing the functionality is trivial and is not blocking this API. We need to decide on a set of restrictions and/or specific jscompiler optimizations before implementing it. We have already had some discussions around this issue but no concrete design nor plan yet.

niloc132 commented 4 years ago

To confirm, you are saying that if Enum.valueOf was implemented as something like return c.getEnumConstants(), which in turn would be assigned to the Class instance in generated JS (perhaps right after Util.$setClassMetadataForEnum is called), closure-compiler would not notice that the field is never read on most class literals and prune those assignments, and that some kind of pass would be required to enable closure to chase them down and confirm which Class instances could be passed in?

This has come up a few times in open source, so it might be something we're going to pursue, even if we just maintain it in our fork after confirming that some basic usage like I described works.

gkdn commented 4 years ago

It is complicated to tackle this in JsCompiler. Our plan is to support this only for limited cases and do some rewriting on J2CL.

If we take an example use case: EnumSet.allOf which needs to call getEnumConstants.

First EnumSet will be updated to use some annotation like @ClassLiteral: static <E extends Enum<E>> EnumSet<E> allOf(@ClassLiteral Class<E> clazz);

So it means that it could only be called like: EmumSet.allOf(MyEnum.class);

If J2CL sees such definition, it will allow the call for clazz.getEnumConstants() inside that method.

Then J2CL will re-write such methods to look like following instead: EnumSet allOf(Class<E> clazz, E[] clazz_enum_constants);

Then it will replace clazz.getEnumConstants() inside the method with clazz_enum_constants parameter.

And finally the call site will be re-written to look like following: EmumSet.allOf(MyEnum.class, MyEnum.values());

niloc132 commented 4 years ago

Got it, thank you. I imagine this will be an internal-only annotation, akin to the other javaemul-internal annotations?

I get the impression you aren't looking for an external contribution for this - is that accurate, or am I misreading?

gkdn commented 4 years ago

The annotation will be public so Guava and user utilities could use it as well.

We will be happy to have an external contribution. If you are planning to do, I recommend writing a short design doc first to get early feedback.

mP1 commented 4 years ago

Can i make a suggestion that along with an @MagicAnnotation on the actual enum to opt in, a txt file holding fq class names of enums is also supported. This is of course to support 3rd party libs that are not yours and have not had their enums tagged(updated), but you want/need em in j2cl.

xamde commented 2 years ago

If others stumble over this, here is the best current workaround I found.

Given an enum like

    enum Color { red, green, blue, white }

and this helper

    public static <T extends Enum<T>> T getValue( String name, T ... enumConstants) {
        for(T enumConstant : enumConstants) {
            if(enumConstant.name().equals(name))
                return enumConstant;
        }
        return null;
    }

Usage is

        String fromSomewhere = "red";
        Color color = getValue(fromSomewhere, Color.values());
niloc132 commented 2 years ago

Consider instead just using Color.fromString(fromSomewhere) - if you already can reference the enum type directly (instead of its Class<? extends Enum<?>> to pass to Enum.valueOf(type, name) itself), you can just call the static method.

J2CL's tests confirm that this method will be emitted, and will work as expected: https://github.com/google/j2cl/blob/20e8f01c306993947d709894d12acc4279694d5d/transpiler/javatests/com/google/j2cl/readable/java/enums/output_closure/Enum1.impl.java.js.txt#L30-L37