diff --git a/src/java.compiler/share/classes/javax/lang/model/util/Elements.java b/src/java.compiler/share/classes/javax/lang/model/util/Elements.java index d3326ba7018f5..2e2d7477a5763 100644 --- a/src/java.compiler/share/classes/javax/lang/model/util/Elements.java +++ b/src/java.compiler/share/classes/javax/lang/model/util/Elements.java @@ -758,6 +758,29 @@ default boolean isAutomaticModule(ModuleElement module) { return false; } + /** + * {@return the class body of an {@code enum} constant if the + * argument is an {@code enum} constant declared with an optional + * class body, {@code null} otherwise} + * + * @implSpec + * The default implementation of this method throws {@code + * UnsupportedOperationException} if the argument is an {@code + * enum} constant and throws an {@code IllegalArgumentException} + * if it is not. + * + * @param enumConstant an enum constant + * @throws IllegalArgumentException if the argument is not an {@code enum} constant + * @jls 8.9.1 Enum Constants + * @since 22 + */ + default TypeElement getEnumConstantBody(VariableElement enumConstant) { + switch(enumConstant.getKind()) { + case ENUM_CONSTANT -> throw new UnsupportedOperationException(); + default -> throw new IllegalArgumentException("Argument not an enum constant"); + } + } + /** * Returns the record component for the given accessor. Returns * {@code null} if the given method is not a record component diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/model/JavacElements.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/model/JavacElements.java index 0c0bb4a695958..bdd25c066e52d 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/model/JavacElements.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/model/JavacElements.java @@ -71,6 +71,7 @@ import static com.sun.tools.javac.code.Kinds.Kind.*; import static com.sun.tools.javac.code.Scope.LookupKind.NON_RECURSIVE; import static com.sun.tools.javac.code.TypeTag.CLASS; +import com.sun.tools.javac.comp.Attr; import com.sun.tools.javac.comp.Modules; import com.sun.tools.javac.comp.Resolve; import com.sun.tools.javac.comp.Resolve.RecoveryLoadClass; @@ -93,6 +94,7 @@ public class JavacElements implements Elements { private final Names names; private final Types types; private final Enter enter; + private final Attr attr; private final Resolve resolve; private final JavacTaskImpl javacTaskImpl; private final Log log; @@ -114,6 +116,7 @@ protected JavacElements(Context context) { names = Names.instance(context); types = Types.instance(context); enter = Enter.instance(context); + attr = Attr.instance(context); resolve = Resolve.instance(context); JavacTask t = context.get(JavacTask.class); javacTaskImpl = t instanceof JavacTaskImpl taskImpl ? taskImpl : null; @@ -725,6 +728,37 @@ public boolean isAutomaticModule(ModuleElement module) { return (msym.flags() & Flags.AUTOMATIC_MODULE) != 0; } + @Override @DefinedBy(Api.LANGUAGE_MODEL) + public TypeElement getEnumConstantBody(VariableElement enumConstant) { + if (enumConstant.getKind() == ElementKind.ENUM_CONSTANT) { + JCTree enumBodyTree = getTreeAlt(enumConstant); + JCTree enclosingEnumTree = getTreeAlt(enumConstant.getEnclosingElement()); + + if (enumBodyTree instanceof JCVariableDecl decl + && enclosingEnumTree instanceof JCClassDecl clazz + && decl.init instanceof JCNewClass nc + && nc.def != null) { + if ((clazz.sym.flags_field & Flags.UNATTRIBUTED) != 0) { + attr.attribClass(clazz.pos(), clazz.sym); + } + return nc.def.sym; // ClassSymbol for enum constant body + } else { + return null; + } + } else { + throw new IllegalArgumentException("Argument not an enum constant"); + } + } + + private JCTree getTreeAlt(Element e) { + Symbol sym = cast(Symbol.class, e); + Env<AttrContext> enterEnv = getEnterEnv(sym); + if (enterEnv == null) + return null; + JCTree tree = TreeInfo.declarationFor(sym, enterEnv.tree); + return tree; + } + @Override @DefinedBy(Api.LANGUAGE_MODEL) public boolean isCompactConstructor(ExecutableElement e) { return (((MethodSymbol)e).flags() & Flags.COMPACT_RECORD_CONSTRUCTOR) != 0; diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/processing/PrintingProcessor.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/processing/PrintingProcessor.java index 29d3418bbfa56..ee49a5b344d9c 100644 --- a/src/jdk.compiler/share/classes/com/sun/tools/javac/processing/PrintingProcessor.java +++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/processing/PrintingProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2005, 2021, Oracle and/or its affiliates. All rights reserved. + * Copyright (c) 2005, 2023, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it @@ -184,6 +184,16 @@ public PrintingElementVisitor visitType(TypeElement e, Boolean p) { NestingKind nestingKind = e.getNestingKind(); if (NestingKind.ANONYMOUS == nestingKind) { + // Print nothing for an anonymous class used for an + // enum constant body. + TypeMirror supertype = e.getSuperclass(); + if (supertype.getKind() != TypeKind.NONE) { + TypeElement superClass = (TypeElement)(((DeclaredType)supertype).asElement()); + if (superClass.getKind() == ENUM) { + return this; + } + } + // Print out an anonymous class in the style of a // class instance creation expression rather than a // class declaration. @@ -693,6 +703,12 @@ private void printInterfaces(TypeElement e) { } private void printPermittedSubclasses(TypeElement e) { + if (e.getKind() == ENUM) { + // any permitted classes on an enum are anonymous + // classes for enum bodies, elide. + return; + } + List<? extends TypeMirror> subtypes = e.getPermittedSubclasses(); if (!subtypes.isEmpty()) { // could remove this check with more complicated joining call writer.print(" permits "); diff --git a/test/langtools/tools/javac/processing/model/util/elements/TestGetEnumConstantBody.java b/test/langtools/tools/javac/processing/model/util/elements/TestGetEnumConstantBody.java new file mode 100644 index 0000000000000..0cc526fc386fd --- /dev/null +++ b/test/langtools/tools/javac/processing/model/util/elements/TestGetEnumConstantBody.java @@ -0,0 +1,266 @@ +/* + * Copyright (c) 2023, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code 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 + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8312418 + * @summary Test Elements.getEnumConstantBody + * @library /tools/javac/lib + * @build JavacTestingAbstractProcessor TestGetEnumConstantBody + * @compile -processor TestGetEnumConstantBody -XDshould.stop-at=FLOW TestGetEnumConstantBody.java + */ + +import java.io.IOException; +import java.io.Writer; +import java.util.*; +import java.util.function.*; +import javax.annotation.processing.*; +import javax.lang.model.element.*; +import javax.lang.model.util.*; +import javax.lang.model.type.*; + +/** + * Test basic workings of Elements.getEnumConstantBody + */ +public class TestGetEnumConstantBody extends JavacTestingAbstractProcessor { + private Elements vacuousElements = new VacuousElements(); + private Set<Element> allElements = new HashSet<>(); + private int round; + + public boolean process(Set<? extends TypeElement> annotations, + RoundEnvironment roundEnv) { + + allElements.addAll(roundEnv.getRootElements()); + + // In the innermost loop, examine the fields defined by the the nested classes + for (TypeElement typeRoot : ElementFilter.typesIn(allElements) ) { + if (typeRoot.getQualifiedName().contentEquals("Gen")) { + continue; + } + + boolean elementSeen = false; + + for (TypeElement typeElt : ElementFilter.typesIn(typeRoot.getEnclosedElements()) ) { + System.out.println("Testing type " + typeElt); + + for (VariableElement field : ElementFilter.fieldsIn(typeElt.getEnclosedElements()) ) { + elementSeen = true; + System.out.println(field); + switch (field.getKind()) { + case FIELD -> expectIAE(field); + case ENUM_CONSTANT -> testEnumConstant(field, typeElt); + default -> throw new RuntimeException("Unexpected field kind seen"); + } + } + } + + if (!elementSeen) { + throw new RuntimeException("No elements seen."); + } + } + switch (round++) { + case 0: + try (Writer w = processingEnv.getFiler().createSourceFile("Cleaned").openWriter()) { + w.write(""" + class Enclosing { + enum Cleaned { + @TestGetEnumConstantBody.ExpectedBinaryName("Enclosing$Cleaned$2") + A(new Object() {}) { + void test(Gen g) { + g.run(); + } + }, + B, + @TestGetEnumConstantBody.ExpectedBinaryName("Enclosing$Cleaned$4") + C(new Object() {}) { + }; + + private Cleaned() {} + + private Cleaned(Object o) {} + } + } + """); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + break; + case 1: + try (Writer w = processingEnv.getFiler().createSourceFile("Gen").openWriter()) { + w.write(""" + public class Gen { + public void run() {} + } + """); + } catch (IOException ex) { + throw new RuntimeException(ex); + } + break; + } + return true; + } + + private String computeExpectedBinaryName(VariableElement e) { + ExpectedBinaryName ebn = e.getAnnotation(ExpectedBinaryName.class); + return (ebn == null) ? null : ebn.value(); + } + + private void expectIAE(VariableElement variable) { + expectException0(() -> elements.getEnumConstantBody(variable), + "Expected exception not thrown"); + + expectException0(() -> vacuousElements.getEnumConstantBody(variable), + "Expected vacuous exception not thrown"); + } + + private void expectException0(Supplier<TypeElement> supplier, String message) { + try { + var typeElement = supplier.get(); + messager.printError(message, typeElement); + } catch (IllegalArgumentException iae) { + ; // Expected + } + } + + void expectUOE(VariableElement field) { + try { + var result = vacuousElements.getEnumConstantBody(field); + messager.printError("Unexpected non-exceptional result returned", field); + + } catch(UnsupportedOperationException uoe) { + ; // Expected + } + } + + private void testEnumConstant(VariableElement field, + TypeElement enclosingClass) { + String expectedBinaryName = computeExpectedBinaryName(field); + boolean expectEnumConstantBody = expectedBinaryName != null; + + System.out.println("\tTesting enum constant " + field + " expected " + expectEnumConstantBody); + expectUOE(field); + + TypeElement enumConstantBody = elements.getEnumConstantBody(field); + + if (Objects.nonNull(enumConstantBody) != expectEnumConstantBody) { + messager.printError("Unexpected body value", field); + } + + if (enumConstantBody != null) { + testEnumConstantBody(enumConstantBody, expectedBinaryName, enclosingClass); + } + + System.out.println("\t constant body " + enumConstantBody); + } + + /* + * From JLS 8.9.1: + * + * "The optional class body of an enum constant implicitly + * declares an anonymous class (15.9.5) that (i) is a direct + * subclass of the immediately enclosing enum class (8.1.4), and + * (ii) is final (8.1.1.2). The class body is governed by the + * usual rules of anonymous classes; in particular it cannot + * contain any constructors. Instance methods declared in these + * class bodies may be invoked outside the enclosing enum class + * only if they override accessible methods in the enclosing enum + * class (8.4.8)." + */ + private void testEnumConstantBody(TypeElement enumConstBody, String expectedBinaryName, TypeElement enumClass) { + if (enumConstBody.getNestingKind() != NestingKind.ANONYMOUS) { + messager.printError("Class body not an anonymous class", enumConstBody); + } + + // Get the TypeElement for the direct superclass. + TypeElement superClass = + (TypeElement)(((DeclaredType)enumConstBody.getSuperclass()).asElement()); + + if (!superClass.equals(enumClass)) { + messager.printError("Class body is not a direct subclass of the enum", enumConstBody); + } + + if (!enumConstBody.getModifiers().contains(Modifier.FINAL)) { + messager.printError("Modifier final missing on class body", enumConstBody); + } + + if (!elements.getBinaryName(enumConstBody).contentEquals(expectedBinaryName)) { + messager.printError("Unexpected binary name, expected: " + expectedBinaryName + + ", but was: " + elements.getBinaryName(enumConstBody), enumConstBody); + } + + return; + } + + + @interface ExpectedBinaryName { + String value(); + } + + // Nested classes hosting a variety of different kinds of fields. + + private static enum Body { + @ExpectedBinaryName("TestGetEnumConstantBody$Body$1") + GOLGI(true) { + public boolean isOrganelle() {return true;} + }, + + @ExpectedBinaryName("TestGetEnumConstantBody$Body$2") + HEAVENLY(true) { + public boolean isCelestial() {return true;} + }; + + private Body(boolean predicate) { + this.predicate = predicate; + } + + private boolean predicate; + + public static int field = 42; + + public void method() {return;} + } + + private static enum MetaSyntaxVar { + FOO("foo"), + BAR("bar"); + + private String lower; + private MetaSyntaxVar(String lower) { + this.lower = lower; + } + + int BAZ = 0; + float QUUX = 0.1f; + } + + // Instance and static fields. + public static class FieldHolder { + public static final int f1 = 1; + public static final String s = "s"; + + private Object data; + public FieldHolder(Object data) { + this.data = data; + } + } +}