Skip to content
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
101 changes: 101 additions & 0 deletions src/main/java/org/codehaus/groovy/classgen/EnumConstantInit.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.codehaus.groovy.classgen;

import org.codehaus.groovy.ast.ClassHelper;
import org.codehaus.groovy.ast.ClassNode;
import org.codehaus.groovy.ast.GroovyCodeVisitor;
import org.codehaus.groovy.ast.Parameter;
import org.codehaus.groovy.ast.expr.Expression;
import org.codehaus.groovy.ast.expr.ExpressionTransformer;
import org.codehaus.groovy.classgen.asm.BytecodeHelper;
import org.objectweb.asm.MethodVisitor;

import static org.codehaus.groovy.ast.ClassHelper.int_TYPE;
import static org.codehaus.groovy.ast.tools.GeneralUtils.param;
import static org.codehaus.groovy.ast.tools.GeneralUtils.params;
import static org.objectweb.asm.Opcodes.DUP;
import static org.objectweb.asm.Opcodes.INVOKESPECIAL;
import static org.objectweb.asm.Opcodes.NEW;

/**
* Initializes an enum constant that supplies no arguments of its own by calling the
* {@code (String,int)} constructor of the enum directly.
* <p>
* The {@code $INIT} helper generated by {@link EnumVisitor} spreads an {@code Object[]}
* over the constructors of the enum, which means the meta class selects the constructor
* at run time by reflecting over {@code getDeclaredConstructors()}. Where that reflection
* is unavailable, e.g. in a GraalVM native image for which the enum was not registered,
* the static initializer of the enum fails. When the arguments are known at compile time
* to be exactly the name and the ordinal, the constructor can be selected there instead.
* <p>
* Only the bytecode generator sees the direct call: every other visitor is given the
* {@code $INIT} call and every {@link ExpressionTransformer} rewrites it in place, so the
* call that is emitted if the expected constructor turns out not to be present once all
* transforms have run is the one that would have been emitted without this expression.
*/
final class EnumConstantInit extends BytecodeExpression {

private static final Parameter[] NAME_AND_ORDINAL = params(param(ClassHelper.STRING_TYPE, "name"), param(int_TYPE, "ordinal"));

private final ClassNode enumClass;
private final String name;
private final int ordinal;
private final Expression initCall;

EnumConstantInit(final ClassNode enumClass, final String name, final int ordinal, final Expression initCall) {
super(enumClass.getPlainNodeReference());
this.enumClass = enumClass;
this.name = name;
this.ordinal = ordinal;
this.initCall = initCall;
}

@Override
public String getText() {
return initCall.getText();
}

@Override
public void visit(final GroovyCodeVisitor visitor) {
if (visitor instanceof AsmClassGenerator && enumClass.getDeclaredConstructor(NAME_AND_ORDINAL) != null) {
super.visit(visitor); // i.e. visitBytecodeExpression(this)
} else {
initCall.visit(visitor);
}
}

@Override
public Expression transformExpression(final ExpressionTransformer transformer) {
Expression result = new EnumConstantInit(enumClass, name, ordinal, transformer.transform(initCall));
result.setSourcePosition(this);
result.copyNodeMetaData(this);
return result;
}

@Override
public void visit(final MethodVisitor mv) {
String owner = BytecodeHelper.getClassInternalName(enumClass);
mv.visitTypeInsn(NEW, owner);
mv.visitInsn(DUP);
mv.visitLdcInsn(name);
BytecodeHelper.pushConstant(mv, ordinal);
mv.visitMethodInsn(INVOKESPECIAL, owner, "<init>", "(Ljava/lang/String;I)V", false);
}
}
30 changes: 29 additions & 1 deletion src/main/java/org/codehaus/groovy/classgen/EnumVisitor.java
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ private void addInit(final ClassNode enumClass, final FieldNode minValue, final

// static init
List<FieldNode> fields = enumClass.getFields();
boolean directInit = canInitDirectly(enumClass, fields);
List<Expression> arrayInit = new ArrayList<>();
List<Statement> block = new ArrayList<>();
int index = -1;
Expand Down Expand Up @@ -320,7 +321,9 @@ private void addInit(final ClassNode enumClass, final FieldNode minValue, final
}
}
arrayInit.add(fieldX(field));
block.add(assignS(fieldX(field), callX(enumType, "$INIT", args)));
Expression init = callX(enumType, "$INIT", args);
if (directInit) init = new EnumConstantInit(enumClass, field.getName(), index, init);
block.add(assignS(fieldX(field), init));
}

if (!isAIC) {
Expand All @@ -338,6 +341,31 @@ private void addInit(final ClassNode enumClass, final FieldNode minValue, final
enumClass.addStaticInitializerStatements(block, true);
}

/**
* Determines whether the constants of the given enum can be initialized with a direct
* constructor call rather than with a call to the synthetic {@code $INIT} helper.
* <p>
* This is only the case when every constant is a plain identifier and so supplies no
* arguments of its own; the constructor arguments are then known to be exactly the
* compiler-supplied name and ordinal. Constants declared with arguments, with named
* arguments or with a class body keep the {@code $INIT} path, as do enums whose own
* constructor cannot accept just the name and the ordinal.
*
* @param enumClass the enum being completed
* @param fields the fields of {@code enumClass}, before any initial value is cleared
* @return {@code true} if a direct constructor call may be attempted
*/
private static boolean canInitDirectly(final ClassNode enumClass, final List<FieldNode> fields) {
// an abstract enum or one that is extended has constants with a body, i.e. subclasses
if (isAnonymousInnerClass(enumClass) || enumClass.isAbstract() || !isNotExtended(enumClass)) return false;
// GROOVY-10811: a declared constructor must be callable with no user-supplied argument
if (!enumClass.getDeclaredConstructors().isEmpty() && !hasNoArgConstructor(enumClass)) return false;
for (FieldNode field : fields) {
if (field.isEnum() && field.getInitialExpression() != null) return false;
}
return true;
}

private void addError(final AnnotatedNode an, final String msg) {
getSourceUnit().getErrorCollector().addErrorAndContinue(
new SyntaxErrorMessage(
Expand Down
52 changes: 52 additions & 0 deletions src/test/groovy/gls/enums/EnumTest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -959,6 +959,58 @@ final class EnumTest extends CompilableTestSupport {
}
'''
}

// constants that supply no arguments of their own are created with a direct
// constructor call instead of through the synthetic $INIT helper
@Test
void testConstantsWithoutArguments() {
assert Weekday.values()*.name() == ['MON', 'TUE', 'WED']
assert Weekday.values()*.ordinal() == [0, 1, 2]
assert Weekday.valueOf('TUE') == Weekday.TUE
assert Weekday.TUE.declaringClass == Weekday
assert Weekday.MIN_VALUE == Weekday.MON
assert Weekday.MAX_VALUE == Weekday.WED
assert Weekday.MON.next() == Weekday.TUE
assert Weekday.MON.previous() == Weekday.WED
assert Weekday.TUE in (Weekday.MON..Weekday.WED)
assert EnumSet.allOf(Weekday).size() == 3
assert Weekday.MON.compareTo(Weekday.WED) < 0
}

@Test
void testConstantsWithoutArgumentsAreSerializable() {
def buffer = new ByteArrayOutputStream()
new ObjectOutputStream(buffer).writeObject(Weekday.TUE)
def restored = new ObjectInputStream(new ByteArrayInputStream(buffer.toByteArray())).readObject()
assert restored.is(Weekday.TUE)
}

@Test
void testConstantsWithoutArgumentsRunTheDeclaredConstructor() {
assert Tagged.values()*.tag == ['tagged', 'tagged']
}

@Test
void testConstantsWithoutArgumentsWhenConstructorHasDefaults() {
assert Defaulted.values()*.label == ['none', 'none']
}
}

enum Weekday {
MON, TUE, WED
}

enum Tagged {
ALPHA, BETA
private final String tag
Tagged() { tag = 'tagged' }
String getTag() { tag }
}

enum Defaulted {
ONE, TWO
final String label
Defaulted(String label = 'none') { this.label = label }
}

enum UsCoin {
Expand Down
Loading
Loading