diff --git a/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/ElementHandler.java b/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/ElementHandler.java index 1903f6a4b9..894225046f 100644 --- a/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/ElementHandler.java +++ b/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/ElementHandler.java @@ -61,15 +61,46 @@ public abstract class ElementHandler { * @return the name specified in the {@link Logged @Logged} annotation on the element, if present; * otherwise, the field or method's name with no modifications */ - public String loggedName(Element element) { - var elementName = element.getSimpleName().toString(); - var config = element.getAnnotation(Logged.class); + public static String loggedName(Element element) { + var elementConfig = element.getAnnotation(Logged.class); - if (config != null && !config.name().isBlank()) { - return config.name(); - } else { - return elementName; + // Use the name provided on the logged element, if one is present + if (elementConfig != null && !elementConfig.name().isBlank()) { + return elementConfig.name(); } + + var config = elementConfig; + + if (config == null) { + // Look up the parent class configuration + // We assume one is present, since logged elements should only be found if the enclosing class + // is @Logged itself + Logged parentConfig = null; + for (var parent = element.getEnclosingElement(); + parent != null; + parent = parent.getEnclosingElement()) { + parentConfig = parent.getAnnotation(Logged.class); + if (parentConfig != null) { + break; + } + } + + config = parentConfig; + } + + if (config == null) { + // Uh oh + throw new IllegalStateException( + "Could not generate a name for element " + + element + + " without a @Logged annotation AND without being contained within a class with a @Logged annotation!\n\nOpen an issue at https://github.com/wpilibsuite/allwpilib/issues and include a copy of the file that caused this error."); + } + + var elementName = element.getSimpleName().toString(); + return switch (config.defaultNaming()) { + case USE_CODE_NAME -> elementName; + case USE_HUMAN_NAME -> StringUtils.toHumanName(elementName); + }; } /** diff --git a/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/LoggerGenerator.java b/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/LoggerGenerator.java index 4c7392217b..3cf64a7562 100644 --- a/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/LoggerGenerator.java +++ b/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/LoggerGenerator.java @@ -7,23 +7,36 @@ package edu.wpi.first.epilogue.processor; import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.toList; +import com.sun.source.tree.IdentifierTree; +import com.sun.source.tree.ReturnTree; +import com.sun.source.util.SimpleTreeVisitor; +import com.sun.source.util.Trees; import edu.wpi.first.epilogue.Logged; import edu.wpi.first.epilogue.NotLogged; import java.io.IOException; import java.io.PrintWriter; +import java.util.ArrayList; import java.util.EnumMap; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.function.Predicate; +import java.util.stream.Collectors; import java.util.stream.Stream; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.Element; import javax.lang.model.element.ExecutableElement; import javax.lang.model.element.Modifier; +import javax.lang.model.element.Name; import javax.lang.model.element.TypeElement; import javax.lang.model.element.VariableElement; +import javax.tools.Diagnostic; /** Generates logger class files for {@link Logged @Logged}-annotated classes. */ public class LoggerGenerator { + public static final Predicate kIsBuiltInJavaMethod = + LoggerGenerator::isBuiltInJavaMethod; private final ProcessingEnvironment m_processingEnv; private final List m_handlers; @@ -36,6 +49,19 @@ public class LoggerGenerator { return e.getAnnotation(NotLogged.class) == null; } + /** + * Checks if a method is a method declared in java.lang.Object that should not be logged. + * + * @param e the method to check + * @return true if the method is toString, hashCode, or clone; false otherwise + */ + private static boolean isBuiltInJavaMethod(ExecutableElement e) { + Name name = e.getSimpleName(); + return name.contentEquals("toString") + || name.contentEquals("hashCode") + || name.contentEquals("clone"); + } + /** * Generates the logger class used to handle data objects of the given type. The generated logger * class will subclass from {@link edu.wpi.first.epilogue.logging.ClassSpecificLogger} and @@ -53,17 +79,23 @@ public class LoggerGenerator { Predicate optedIn = e -> !requireExplicitOptIn || e.getAnnotation(Logged.class) != null; - var fieldsToLog = - clazz.getEnclosedElements().stream() - .filter(e -> e instanceof VariableElement) - .map(e -> (VariableElement) e) - .filter(notSkipped) - .filter(optedIn) - .filter(e -> !e.getModifiers().contains(Modifier.STATIC)) - .filter(this::isLoggable) - .toList(); + List fieldsToLog; + if (Objects.equals(clazz.getSuperclass().toString(), "java.lang.Record")) { + // Do not log record members - just use the accessor methods + fieldsToLog = List.of(); + } else { + fieldsToLog = + clazz.getEnclosedElements().stream() + .filter(e -> e instanceof VariableElement) + .map(e -> (VariableElement) e) + .filter(notSkipped) + .filter(optedIn) + .filter(e -> !e.getModifiers().contains(Modifier.STATIC)) + .filter(this::isLoggable) + .toList(); + } - var methodsToLog = + List methodsToLog = clazz.getEnclosedElements().stream() .filter(e -> e instanceof ExecutableElement) .map(e -> (ExecutableElement) e) @@ -73,9 +105,51 @@ public class LoggerGenerator { .filter(e -> e.getModifiers().contains(Modifier.PUBLIC)) .filter(e -> e.getParameters().isEmpty()) .filter(e -> e.getReceiverType() != null) + .filter(kIsBuiltInJavaMethod.negate()) .filter(this::isLoggable) + .filter(e -> !isSimpleGetterMethodForLoggedField(e, fieldsToLog)) .toList(); + // Validate no name collisions + Map> usedNames = + Stream.concat(fieldsToLog.stream(), methodsToLog.stream()) + .reduce( + new HashMap<>(), + (map, element) -> { + String name = ElementHandler.loggedName(element); + map.computeIfAbsent(name, _k -> new ArrayList<>()).add(element); + + return map; + }, + (left, right) -> { + left.putAll(right); + return left; + }); + + usedNames.forEach( + (name, elements) -> { + if (elements.size() > 1) { + // Collisions! + for (Element conflictingElement : elements) { + String conflicts = + elements.stream() + .filter(e -> !e.equals(conflictingElement)) + .map(e -> e.getEnclosingElement().getSimpleName() + "." + e) + .collect(Collectors.joining(", ")); + + m_processingEnv + .getMessager() + .printMessage( + Diagnostic.Kind.ERROR, + "[EPILOGUE] Conflicting name detected: \"" + + name + + "\" is also used by " + + conflicts, + conflictingElement); + } + } + }); + writeLoggerFile(clazz.getQualifiedName().toString(), config, fieldsToLog, methodsToLog); } @@ -242,4 +316,55 @@ public class LoggerGenerator { private boolean isLoggable(Element element) { return m_handlers.stream().anyMatch(h -> h.isLoggable(element)); } + + /** + * Checks if a method is a simple "getter" method for a field in the given list. Here, we define + * "getter" as a method with a single return statement that references the name of a field, with + * no other expressions. `getX() { return x; }` would be considered a "getter" method, while + * `getX() { return x.clone(); }` would not be. Note that the method name is irrelevant; only the + * method body is checked. + * + * @param ex the method to check + * @param fieldsToLog the fields that will already be logged + * @return true if the method is a simple "getter" method, false otherwise + */ + private boolean isSimpleGetterMethodForLoggedField( + ExecutableElement ex, List fieldsToLog) { + var trees = Trees.instance(m_processingEnv); + var methodTree = trees.getTree(ex); + + if (methodTree == null) { + // probably a record's synthetic reader method + return false; + } + + if (methodTree.getBody() == null) { + // Abstract or native method, can't be determined to be a getter + return false; + } + + var statements = methodTree.getBody().getStatements(); + if (statements.size() != 1) { + // More complex than a simple `return m_field` statement + return false; + } + + var statement = statements.get(0); + if (!(statement instanceof ReturnTree ret)) { + // Shouldn't get here, since we've already filtered for methods that return a value + // and with a single statement - that one statement should be the return + return false; + } + + var returnExpression = ret.getExpression(); + return returnExpression.accept( + new SimpleTreeVisitor(false) { + @Override + public Boolean visitIdentifier(IdentifierTree identifier, Void unused) { + return fieldsToLog.stream() + .anyMatch(v -> v.getSimpleName().contentEquals(identifier.getName())); + } + }, + null); + } } diff --git a/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/StringUtils.java b/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/StringUtils.java index 6cc43e53d7..7fc624b12a 100644 --- a/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/StringUtils.java +++ b/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/StringUtils.java @@ -5,6 +5,9 @@ package edu.wpi.first.epilogue.processor; import edu.wpi.first.epilogue.Logged; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; import javax.lang.model.element.TypeElement; public final class StringUtils { @@ -59,6 +62,33 @@ public final class StringUtils { return builder.toString(); } + /** + * Splits a camel-cased string like "fooBar" into individual words like ["foo", "Bar"]. + * + * @param camelCasedString the camel-cased string to split + * @return the individual words in the input + */ + public static List splitToWords(CharSequence camelCasedString) { + // Implementation from https://stackoverflow.com/a/2560017, refactored for readability + + // Uppercase letter not followed by the first letter of the next word + // This allows for splitting "IOLayer" into "IO" and "Layer" + String penultimateUppercaseLetter = "(?<=[A-Z])(?=[A-Z][a-z])"; + + // Any character that's NOT an uppercase letter, immediately followed by an uppercase letter + // This allows for splitting "fooBar" into "foo" and "Bar", or "123Bang" into "123" and "Bang" + String lastNonUppercaseLetter = "(?<=[^A-Z])(?=[A-Z])"; + + // The final letter in a sequence, followed by a non-alpha character like a number or underscore + // This allows for splitting "foo123" into "foo" and "123" + String finalLetter = "(?<=[A-Za-z])(?=[^A-Za-z])"; + + String regex = + String.format("%s|%s|%s", penultimateUppercaseLetter, lastNonUppercaseLetter, finalLetter); + + return Arrays.asList(camelCasedString.toString().split(regex)); + } + /** * Gets the name of the field used to hold a logger for data of the given type. * @@ -107,4 +137,31 @@ public final class StringUtils { return loggerClassName; } + + /** + * Converts a camelCase element name to separate words, removing common field and method name + * prefixes like "m_" and "get". + * + * @param elementName the camelcased element name + * @return the name split into separate words and sanitized + */ + public static String toHumanName(String elementName) { + // Delete common field prefixes (k_name, m_name, s_name) + var sanitizedName = elementName.replaceFirst("^[msk]_", ""); + + // Drop leading "k" prefix from fields + // (though normally these should be static, and thus not logged) + if (sanitizedName.matches("^k[A-Z].*$")) { + sanitizedName = sanitizedName.substring(1); + } + + // Drop leading "get" from accessor methods + if (sanitizedName.matches("^get[A-Z].*$")) { + sanitizedName = sanitizedName.substring(3); + } + + return splitToWords(sanitizedName).stream() + .map(StringUtils::capitalize) + .collect(Collectors.joining(" ")); + } } diff --git a/epilogue-processor/src/test/java/edu/wpi/first/epilogue/processor/AnnotationProcessorTest.java b/epilogue-processor/src/test/java/edu/wpi/first/epilogue/processor/AnnotationProcessorTest.java index 38cd90e9cf..979026f351 100644 --- a/epilogue-processor/src/test/java/edu/wpi/first/epilogue/processor/AnnotationProcessorTest.java +++ b/epilogue-processor/src/test/java/edu/wpi/first/epilogue/processor/AnnotationProcessorTest.java @@ -301,9 +301,9 @@ class AnnotationProcessorTest { byte[] arr1; // Should be logged byte[][] arr2; // Should not be logged - public byte getX() { return x; } - public byte[] getArr1() { return arr1; } - public byte[][] getArr2() { return arr2; } + public byte getX() { return 0; } + public byte[] getArr1() { return null; } + public byte[][] getArr2() { return null; } } """; @@ -348,9 +348,9 @@ class AnnotationProcessorTest { char[] arr1; // Should not be logged char[][] arr2; // Should not be logged - public char getX() { return x; } - public char[] getArr1() { return arr1; } - public char[][] getArr2() { return arr2; } + public char getX() { return 'x'; } + public char[] getArr1() { return null; } + public char[][] getArr2() { return null; } } """; @@ -393,9 +393,9 @@ class AnnotationProcessorTest { short[] arr1; // Should not be logged short[][] arr2; // Should not be logged - public short getX() { return x; } - public short[] getArr1() { return arr1; } - public short[][] getArr2() { return arr2; } + public short getX() { return 0; } + public short[] getArr1() { return null; } + public short[][] getArr2() { return null; } } """; @@ -438,9 +438,9 @@ class AnnotationProcessorTest { int[] arr1; // Should be logged int[][] arr2; // Should not be logged - public int getX() { return x; } - public int[] getArr1() { return arr1; } - public int[][] getArr2() { return arr2; } + public int getX() { return 0; } + public int[] getArr1() { return null; } + public int[][] getArr2() { return null; } } """; @@ -485,9 +485,9 @@ class AnnotationProcessorTest { long[] arr1; // Should be logged long[][] arr2; // Should not be logged - public long getX() { return x; } - public long[] getArr1() { return arr1; } - public long[][] getArr2() { return arr2; } + public long getX() { return 0; } + public long[] getArr1() { return null; } + public long[][] getArr2() { return null; } } """; @@ -532,9 +532,9 @@ class AnnotationProcessorTest { float[] arr1; // Should be logged float[][] arr2; // Should not be logged - public float getX() { return x; } - public float[] getArr1() { return arr1; } - public float[][] getArr2() { return arr2; } + public float getX() { return 0; } + public float[] getArr1() { return null; } + public float[][] getArr2() { return null; } } """; @@ -582,9 +582,9 @@ class AnnotationProcessorTest { double[][] arr2; // Should not be logged List list; // Should not be logged - public double getX() { return x; } - public double[] getArr1() { return arr1; } - public double[][] getArr2() { return arr2; } + public double getX() { return 0; } + public double[] getArr1() { return null; } + public double[][] getArr2() { return null; } } """; @@ -631,9 +631,9 @@ class AnnotationProcessorTest { boolean[][] arr2; // Should not be logged List list; // Should not be logged - public boolean getX() { return x; } - public boolean[] getArr1() { return arr1; } - public boolean[][] getArr2() { return arr2; } + public boolean getX() { return false; } + public boolean[] getArr1() { return null; } + public boolean[][] getArr2() { return null; } } """; @@ -681,9 +681,9 @@ class AnnotationProcessorTest { String[][] arr2; // Should not be logged List list; // Should be logged - public String getX() { return x; } - public String[] getArr1() { return arr1; } - public String[][] getArr2() { return arr2; } + public String getX() { return null; } + public String[] getArr1() { return null; } + public String[][] getArr2() { return null; } } """; @@ -740,9 +740,9 @@ class AnnotationProcessorTest { Structable[][] arr2; // Should not be logged List list; // Should be logged - public Structable getX() { return x; } - public Structable[] getArr1() { return arr1; } - public Structable[][] getArr2() { return arr2; } + public Structable getX() { return null; } + public Structable[] getArr1() { return null; } + public Structable[][] getArr2() { return null; } } """; @@ -1138,6 +1138,187 @@ class AnnotationProcessorTest { message); } + @Test + void loggingRecords() { + String source = + """ + package edu.wpi.first.epilogue; + + @Logged + record Example(double x, double y) { } + """; + + String expectedRootLogger = + """ + package edu.wpi.first.epilogue; + + import edu.wpi.first.epilogue.Logged; + import edu.wpi.first.epilogue.Epilogue; + import edu.wpi.first.epilogue.logging.ClassSpecificLogger; + import edu.wpi.first.epilogue.logging.DataLogger; + + public class ExampleLogger extends ClassSpecificLogger { + public ExampleLogger() { + super(Example.class); + } + + @Override + public void update(DataLogger dataLogger, Example object) { + if (Epilogue.shouldLog(Logged.Importance.DEBUG)) { + dataLogger.log("x", object.x()); + dataLogger.log("y", object.y()); + } + } + } + """; + + assertLoggerGenerates(source, expectedRootLogger); + } + + @Test + void errorsOnFieldNameConflicts() { + String source = + """ + package edu.wpi.first.epilogue; + + @Logged + class Example { + @Logged(name = "Custom Name") double x; + @Logged(name = "Custom Name") double y; + @Logged(name = "Custom Name") double z; + } + """; + + Compilation compilation = + javac() + .withProcessors(new AnnotationProcessor()) + .compile(JavaFileObjects.forSourceString("edu.wpi.first.epilogue.Example", source)); + + assertThat(compilation).failed(); + assertThat(compilation).hadErrorCount(3); + + List> errors = compilation.errors(); + assertAll( + () -> + assertCompilationError( + "[EPILOGUE] Conflicting name detected: \"Custom Name\" is also used by Example.y, Example.z", + 5, + 40, + errors.get(0)), + () -> + assertCompilationError( + "[EPILOGUE] Conflicting name detected: \"Custom Name\" is also used by Example.x, Example.z", + 6, + 40, + errors.get(1)), + () -> + assertCompilationError( + "[EPILOGUE] Conflicting name detected: \"Custom Name\" is also used by Example.x, Example.y", + 7, + 40, + errors.get(2))); + } + + @Test + void doesNotErrorOnGetterMethod() { + String source = + """ + package edu.wpi.first.epilogue; + + @Logged + class Example { + double x; + public double x() { return x; } + public double getX() { return x; } + public double aTotallyArbitraryNameForAnAccessorMethod() { return x; } + public double withANoOpTransform() { return x + 0; } + public double withTemp() { var temp = x; return temp; } + } + """; + + String expectedRootLogger = + """ + package edu.wpi.first.epilogue; + + import edu.wpi.first.epilogue.Logged; + import edu.wpi.first.epilogue.Epilogue; + import edu.wpi.first.epilogue.logging.ClassSpecificLogger; + import edu.wpi.first.epilogue.logging.DataLogger; + + public class ExampleLogger extends ClassSpecificLogger { + public ExampleLogger() { + super(Example.class); + } + + @Override + public void update(DataLogger dataLogger, Example object) { + if (Epilogue.shouldLog(Logged.Importance.DEBUG)) { + dataLogger.log("x", object.x); + dataLogger.log("withANoOpTransform", object.withANoOpTransform()); + dataLogger.log("withTemp", object.withTemp()); + } + } + } + """; + + assertLoggerGenerates(source, expectedRootLogger); + } + + @Test + void configuredDefaultNaming() { + String source = + """ + package edu.wpi.first.epilogue; + + @Logged(defaultNaming = Logged.Naming.USE_HUMAN_NAME) + class Example { + double m_memberPrefix; + double kConstantPrefix; + double k_otherConstantPrefix; + double s_otherPrefix; + + public double getTheGetterMethod() { + return 0; + } + + @Logged(defaultNaming = Logged.Naming.USE_CODE_NAME) + public double optedOut() { + return 0; + } + } + """; + + String expectedRootLogger = + """ + package edu.wpi.first.epilogue; + + import edu.wpi.first.epilogue.Logged; + import edu.wpi.first.epilogue.Epilogue; + import edu.wpi.first.epilogue.logging.ClassSpecificLogger; + import edu.wpi.first.epilogue.logging.DataLogger; + + public class ExampleLogger extends ClassSpecificLogger { + public ExampleLogger() { + super(Example.class); + } + + @Override + public void update(DataLogger dataLogger, Example object) { + if (Epilogue.shouldLog(Logged.Importance.DEBUG)) { + dataLogger.log("Member Prefix", object.m_memberPrefix); + dataLogger.log("Constant Prefix", object.kConstantPrefix); + dataLogger.log("Other Constant Prefix", object.k_otherConstantPrefix); + dataLogger.log("Other Prefix", object.s_otherPrefix); + dataLogger.log("The Getter Method", object.getTheGetterMethod()); + dataLogger.log("optedOut", object.optedOut()); + } + } + } + """; + + assertLoggerGenerates(source, expectedRootLogger); + } + private void assertCompilationError( String message, long lineNumber, long col, Diagnostic diagnostic) { assertAll( diff --git a/epilogue-processor/src/test/java/edu/wpi/first/epilogue/processor/StringUtilsTest.java b/epilogue-processor/src/test/java/edu/wpi/first/epilogue/processor/StringUtilsTest.java index 4032260fff..8703e9a0a7 100644 --- a/epilogue-processor/src/test/java/edu/wpi/first/epilogue/processor/StringUtilsTest.java +++ b/epilogue-processor/src/test/java/edu/wpi/first/epilogue/processor/StringUtilsTest.java @@ -4,8 +4,10 @@ package edu.wpi.first.epilogue.processor; +import static org.junit.jupiter.api.Assertions.assertAll; import static org.junit.jupiter.api.Assertions.assertEquals; +import java.util.List; import org.junit.jupiter.api.Test; class StringUtilsTest { @@ -16,4 +18,22 @@ class StringUtilsTest { assertEquals("fooBar", StringUtils.lowerCamelCase("FooBar")); assertEquals("allcaps", StringUtils.lowerCamelCase("ALLCAPS")); } + + @Test + void splitToWords() { + assertAll( + () -> assertEquals(List.of("IO", "Logger"), StringUtils.splitToWords("IOLogger")), + () -> assertEquals(List.of("LED", "Subsystem"), StringUtils.splitToWords("LEDSubsystem")), + () -> assertEquals(List.of("Foo", "Bar"), StringUtils.splitToWords("FooBar")), + () -> assertEquals(List.of("ALLCAPS"), StringUtils.splitToWords("ALLCAPS")), + () -> + assertEquals(List.of("k", "First", "Second"), StringUtils.splitToWords("kFirstSecond")), + () -> + assertEquals( + List.of("there", "Is", "A", "Number", "123", "In", "Here", "VERSION", "456"), + StringUtils.splitToWords("thereIsANumber123InHereVERSION456")), + () -> + assertEquals( + List.of("get", "First", "Second"), StringUtils.splitToWords("getFirstSecond"))); + } } diff --git a/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/Logged.java b/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/Logged.java index 94762fc03d..2d76c9bfbe 100644 --- a/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/Logged.java +++ b/epilogue-runtime/src/main/java/edu/wpi/first/epilogue/Logged.java @@ -89,4 +89,39 @@ public @interface Logged { * @return the importance of the annotated element */ Importance importance() default Importance.DEBUG; + + /** + * Different behaviors for how Epilogue will generate the names of logged data points. This only + * applies to automatically generated names; any specific name provided with {@link #name()} will + * take precedence over an automatically generated name. + */ + enum Naming { + /** + * Sets the default naming strategy to use the name of the element as it appears in source code. + * For example, a field {@code double m_x} would be labeled as {@code "m_x"} by default, and a + * {@code getX()} accessor would be labeled as {@code "getX"}. + */ + USE_CODE_NAME, + + /** + * Sets the default naming strategy to use a human-readable name based on the name of the name + * of the element as it appears in source code. For example, a field {@code double m_x} would be + * labeled as {@code "X"} by default, and a {@code getX()} accessor would also be labeled as + * {@code "X"}. Because logged names must be unique, this configuration would fail to compile + * and require either one of the fields to be excluded from logs (which, for simple accessors, + * would be ideal to avoid duplicate data), or to rename one or both elements so the logged data + * fields would have unique names. + */ + USE_HUMAN_NAME + } + + /** + * The default naming behavior to use. Defaults to {@link Naming#USE_CODE_NAME}, which uses the + * raw code name directly in logs. Any configuration of the {@link #name()} attribute on logged + * fields and methods will take precedence over an automatically generated name. + * + * @return the naming strategy for and annotated field or method, or the default naming strategy + * for all logged fields and methods in an annotated class + */ + Naming defaultNaming() default Naming.USE_CODE_NAME; }