diff --git a/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/AnnotationProcessor.java b/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/AnnotationProcessor.java index 2781c055c8c..351aebe7f19 100644 --- a/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/AnnotationProcessor.java +++ b/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/AnnotationProcessor.java @@ -104,8 +104,8 @@ public boolean process(Set annotations, RoundEnvironment processingEnv, loggedTypes), // prioritize epilogue logging over Sendable new ConfiguredLoggerHandler( processingEnv, customLoggers), // then customized logging configs - new ArrayHandler(processingEnv), - new CollectionHandler(processingEnv), + new ArrayHandler(processingEnv, loggedTypes), + new CollectionHandler(processingEnv, loggedTypes), new EnumHandler(processingEnv), new MeasureHandler(processingEnv), new PrimitiveHandler(processingEnv), diff --git a/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/ArrayHandler.java b/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/ArrayHandler.java index 8c31197e8d8..d1195fbb108 100644 --- a/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/ArrayHandler.java +++ b/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/ArrayHandler.java @@ -4,11 +4,15 @@ package edu.wpi.first.epilogue.processor; +import java.util.Collection; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.Element; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.VariableElement; import javax.lang.model.type.ArrayType; import javax.lang.model.type.PrimitiveType; import javax.lang.model.type.TypeMirror; +import javax.tools.Diagnostic; /** * Arrays of bytes, ints, flats, doubles, booleans, Strings, and struct-serializable objects can be @@ -16,21 +20,24 @@ */ public class ArrayHandler extends ElementHandler { private final StructHandler m_structHandler; + private final LoggableHandler m_loggableHandler; private final TypeMirror m_javaLangString; - protected ArrayHandler(ProcessingEnvironment processingEnv) { + protected ArrayHandler( + ProcessingEnvironment processingEnv, Collection loggedTypes) { super(processingEnv); // use a struct handler for managing struct arrays m_structHandler = new StructHandler(processingEnv); - + m_loggableHandler = new LoggableHandler(processingEnv, loggedTypes); m_javaLangString = lookupTypeElement(processingEnv, "java.lang.String").asType(); } @Override public boolean isLoggable(Element element) { return dataType(element) instanceof ArrayType arr - && isLoggableComponentType(arr.getComponentType()); + && (isLoggableComponentType(arr.getComponentType()) + || isCustomLoggableArray(arr.getComponentType(), element)); } /** @@ -51,6 +58,35 @@ public boolean isLoggableComponentType(TypeMirror type) { || m_processingEnv.getTypeUtils().isAssignable(type, m_javaLangString); } + /** + * Checks to see if an array has a type that either contains a @Logged annotation or has a custom + * logger. Will fail if the array is not final. + * + * @param componentType The component type of the array + * @param arrayElement The element of the array + * @return Whether the array + */ + private boolean isCustomLoggableArray(TypeMirror componentType, Element arrayElement) { + if (m_loggableHandler.isLoggableType(componentType)) { + if (!arrayElement.getModifiers().contains(Modifier.FINAL)) { + String cause = + arrayElement instanceof VariableElement + ? " isn't marked as final." + : " is returned from a method."; + cause += " Arrays with @Logged classes cannot be non-final or be returned from methods."; + m_processingEnv + .getMessager() + .printMessage( + Diagnostic.Kind.NOTE, + "[EPILOGUE] Excluded from logs because array " + arrayElement + cause, + arrayElement); + return false; + } + return true; + } + return false; + } + @Override public String logInvocation(Element element) { var dataType = dataType(element); @@ -67,6 +103,20 @@ public String logInvocation(Element element) { + ", " + m_structHandler.structAccess(componentType) + ")"; + } else if (m_loggableHandler.isLoggableType(componentType)) { + var elementAccess = elementAccess(element); + var logInvocation = + m_loggableHandler.logInvocation(element, componentType, elementAccess + "[i]"); + logInvocation = + logInvocation.replaceAll( + "backend\\.getNested\\(\".*\"\\)", + "backend.getNested(\"%s/\" + i)".formatted(loggedName(element))); + return """ + for (int i = 0; i < %s.length; i++) { + %s; + } + """ + .formatted(elementAccess, logInvocation); } else { // Primitive or string array return "backend.log(\"" + loggedName(element) + "\", " + elementAccess(element) + ")"; diff --git a/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/CollectionHandler.java b/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/CollectionHandler.java index 3fe4abb3aa8..d201aacfee1 100644 --- a/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/CollectionHandler.java +++ b/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/CollectionHandler.java @@ -4,6 +4,7 @@ package edu.wpi.first.epilogue.processor; +import java.util.Collection; import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.Element; import javax.lang.model.type.DeclaredType; @@ -17,9 +18,10 @@ public class CollectionHandler extends ElementHandler { private final TypeMirror m_collectionType; private final StructHandler m_structHandler; - protected CollectionHandler(ProcessingEnvironment processingEnv) { + protected CollectionHandler( + ProcessingEnvironment processingEnv, Collection loggedTypes) { super(processingEnv); - m_arrayHandler = new ArrayHandler(processingEnv); + m_arrayHandler = new ArrayHandler(processingEnv, loggedTypes); m_collectionType = processingEnv.getElementUtils().getTypeElement("java.util.Collection").asType(); m_structHandler = new StructHandler(processingEnv); diff --git a/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/LoggableHandler.java b/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/LoggableHandler.java index ba14022a2c1..bea3ecc96e9 100644 --- a/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/LoggableHandler.java +++ b/epilogue-processor/src/main/java/edu/wpi/first/epilogue/processor/LoggableHandler.java @@ -34,13 +34,28 @@ protected LoggableHandler( @Override public boolean isLoggable(Element element) { - return m_processingEnv.getTypeUtils().asElement(dataType(element)) instanceof TypeElement t + return isLoggableType(dataType(element)); + } + + public boolean isLoggableType(TypeMirror typeMirror) { + return m_processingEnv.getTypeUtils().asElement(typeMirror) instanceof TypeElement t && m_loggedTypes.contains(t); } @Override public String logInvocation(Element element) { - TypeMirror dataType = dataType(element); + return logInvocation(element, dataType(element), elementAccess(element)); + } + + /** + * Creates a log invocation with a custom data type/element access. + * + * @param element The base element of the type to log + * @param dataType The base type of the element + * @param elementAccess A code string that fetches the value when created in java + * @return A log call that logs the element + */ + public String logInvocation(Element element, TypeMirror dataType, String elementAccess) { var declaredType = m_processingEnv .getElementUtils() @@ -61,7 +76,7 @@ public String logInvocation(Element element) { // If there are no known loggable subtypes, return just the single logger call if (size == 1) { - return generateLoggerCall(element, declaredType, elementAccess(element)); + return generateLoggerCall(element, declaredType, elementAccess); } // Otherwise, generate an if-else chain to compare the element with its known loggable subtypes @@ -73,7 +88,7 @@ public String logInvocation(Element element) { StringBuilder builder = new StringBuilder(); // Cache the value in a variable so it's only read once - builder.append("var %s = %s;\n".formatted(varName, elementAccess(element))); + builder.append("var %s = %s;\n".formatted(varName, elementAccess)); for (int i = 0; i < size; i++) { TypeElement type = loggableSubtypes.get(i); @@ -129,9 +144,7 @@ private static String cacheVariableName(Element element) { private Comparator inheritanceComparatorFor(TypeElement declaredType) { Comparator byDistance = Comparator.comparingInt( - inheritor -> { - return inheritanceDistance(inheritor.asType(), declaredType.asType()); - }); + inheritor -> inheritanceDistance(inheritor.asType(), declaredType.asType())); return byDistance .reversed() 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 f211999bfe6..826a91cf3ef 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 @@ -972,6 +972,54 @@ public void update(EpilogueBackend backend, Example object) { assertLoggerGenerates(source, expectedGeneratedSource); } + @Test + void customLoggableArrays() { + String source = + """ + package edu.wpi.first.epilogue; + import java.util.List; + + @Logged + class Example { + final SubLoggable[] x = new SubLoggable[] {}; // logged + SubLoggable[] y; // not logged; not marked final + final List z = List.of(); // not logged; lists cannot be logged + } + + @Logged + class SubLoggable { + double y; + } + """; + + String expectedGeneratedSource = + """ + 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.EpilogueBackend; + + public class ExampleLogger extends ClassSpecificLogger { + public ExampleLogger() { + super(Example.class); + } + + @Override + public void update(EpilogueBackend backend, Example object) { + if (Epilogue.shouldLog(Logged.Importance.DEBUG)) { + for (int i = 0; i < object.x.length; i++) { + Epilogue.subLoggableLogger.tryUpdate(backend.getNested("x/" + i), object.x[i], Epilogue.getConfig().errorHandler); + }; + } + } + } + """; + + assertLoggerGenerates(source, expectedGeneratedSource); + } + @Test void badLogSetup() { String source =