From 0f313c672f2929d7d4435eec4ef26c6205610d46 Mon Sep 17 00:00:00 2001 From: Sam Carlberg Date: Thu, 31 Oct 2024 23:34:00 -0400 Subject: [PATCH] [epilogue] Autogenerate nicer data names by default, not just raw element names (#7167) eg "getFoo()" will now be logged as "Foo", or "m_leftMotor" as "Left Motor" It is now a compilation error to reuse the same logged name for multiple elements (since whatever is declared last would overwrite anything logged before it) Do not log record fields (just use the accessors). This also fixes an issue where records could never be logged due to identical member and accessor names Also skips toString, hashCode, and clone methods when generating loggers --- .../epilogue/processor/ElementHandler.java | 45 +++- .../epilogue/processor/LoggerGenerator.java | 145 ++++++++++- .../first/epilogue/processor/StringUtils.java | 57 +++++ .../processor/AnnotationProcessorTest.java | 241 +++++++++++++++--- .../epilogue/processor/StringUtilsTest.java | 20 ++ .../java/edu/wpi/first/epilogue/Logged.java | 35 +++ 6 files changed, 496 insertions(+), 47 deletions(-) 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; }