[epilogue] Add an annotation-based logging framework for Java programs (#6584)

This commit is contained in:
Sam Carlberg
2024-07-16 20:25:43 -04:00
committed by GitHub
parent 30c7632ab8
commit 59256f0e00
51 changed files with 5147 additions and 7 deletions

View File

@@ -0,0 +1,413 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue.processor;
import com.google.auto.service.AutoService;
import edu.wpi.first.epilogue.CustomLoggerFor;
import edu.wpi.first.epilogue.Logged;
import edu.wpi.first.epilogue.NotLogged;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.AnnotationMirror;
import javax.lang.model.element.AnnotationValue;
import javax.lang.model.element.Element;
import javax.lang.model.element.ElementKind;
import javax.lang.model.element.ExecutableElement;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.NoType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
@SupportedAnnotationTypes({
"edu.wpi.first.epilogue.CustomLoggerFor",
"edu.wpi.first.epilogue.Logged"
})
@SupportedSourceVersion(SourceVersion.RELEASE_17)
@AutoService(Processor.class)
public class AnnotationProcessor extends AbstractProcessor {
private static final String kCustomLoggerFqn = "edu.wpi.first.epilogue.CustomLoggerFor";
private static final String kClassSpecificLoggerFqn =
"edu.wpi.first.epilogue.logging.ClassSpecificLogger";
private static final String kLoggedFqn = "edu.wpi.first.epilogue.Logged";
private EpilogueGenerator m_epiloguerGenerator;
private LoggerGenerator m_loggerGenerator;
private List<ElementHandler> m_handlers;
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
if (annotations.isEmpty()) {
// Nothing to do, don't claim
return false;
}
Map<TypeMirror, DeclaredType> customLoggers = new HashMap<>();
annotations.stream()
.filter(ann -> kCustomLoggerFqn.contentEquals(ann.getQualifiedName()))
.findAny()
.ifPresent(
customLogger -> {
customLoggers.putAll(processCustomLoggers(roundEnv, customLogger));
});
roundEnv.getRootElements().stream()
.filter(
e ->
processingEnv
.getTypeUtils()
.isAssignable(
e.asType(),
processingEnv
.getTypeUtils()
.erasure(
processingEnv
.getElementUtils()
.getTypeElement(kClassSpecificLoggerFqn)
.asType())))
.filter(e -> e.getAnnotation(CustomLoggerFor.class) == null)
.forEach(
e -> {
processingEnv
.getMessager()
.printMessage(
Diagnostic.Kind.ERROR,
"Custom logger classes should have a @CustomLoggerFor annotation",
e);
});
// Handlers are declared in order of priority. If an element could be logged in more than one
// way (eg a class implements both Sendable and StructSerializable), the order of the handlers
// in this list will determine how it gets logged.
m_handlers =
List.of(
new LoggableHandler(processingEnv), // prioritize epilogue logging over Sendable
new ConfiguredLoggerHandler(
processingEnv, customLoggers), // then customized logging configs
new ArrayHandler(processingEnv),
new CollectionHandler(processingEnv),
new EnumHandler(processingEnv),
new MeasureHandler(processingEnv),
new PrimitiveHandler(processingEnv),
new SupplierHandler(processingEnv),
new StructHandler(processingEnv), // prioritize struct over sendable
new SendableHandler(processingEnv));
m_epiloguerGenerator = new EpilogueGenerator(processingEnv, customLoggers);
m_loggerGenerator = new LoggerGenerator(processingEnv, m_handlers);
annotations.stream()
.filter(ann -> kLoggedFqn.contentEquals(ann.getQualifiedName()))
.findAny()
.ifPresent(
epilogue -> {
processEpilogue(roundEnv, epilogue);
});
return false;
}
private boolean validateFields(Set<? extends Element> annotatedElements) {
var fields =
annotatedElements.stream()
.filter(e -> e instanceof VariableElement)
.map(e -> (VariableElement) e)
.toList();
boolean valid = true;
for (VariableElement field : fields) {
// Field is explicitly tagged
// And is not opted out of
if (field.getAnnotation(NotLogged.class) == null && isNotLoggable(field, field.asType())) {
// And is not of a loggable type
processingEnv
.getMessager()
.printMessage(
Diagnostic.Kind.ERROR,
"[EPILOGUE] You have opted in to logging on this field, "
+ "but it is not a loggable data type!",
field);
valid = false;
}
}
return valid;
}
private boolean validateMethods(Set<? extends Element> annotatedElements) {
var methods =
annotatedElements.stream()
.filter(e -> e instanceof ExecutableElement)
.map(e -> (ExecutableElement) e)
.toList();
boolean valid = true;
for (ExecutableElement method : methods) {
// Field is explicitly tagged
if (method.getAnnotation(NotLogged.class) == null) {
// And is not opted out of
if (isNotLoggable(method, method.getReturnType())) {
// And is not of a loggable type
processingEnv
.getMessager()
.printMessage(
Diagnostic.Kind.ERROR,
"[EPILOGUE] You have opted in to logging on this method, "
+ "but it does not return a loggable data type!",
method);
valid = false;
}
if (!method.getModifiers().contains(Modifier.PUBLIC)) {
// Only public methods can be logged
processingEnv
.getMessager()
.printMessage(
Diagnostic.Kind.ERROR, "[EPILOGUE] Logged methods must be public", method);
valid = false;
}
if (method.getModifiers().contains(Modifier.STATIC)) {
processingEnv
.getMessager()
.printMessage(
Diagnostic.Kind.ERROR, "[EPILOGUE] Logged methods cannot be static", method);
valid = false;
}
if (method.getReturnType().getKind() == TypeKind.NONE) {
processingEnv
.getMessager()
.printMessage(
Diagnostic.Kind.ERROR, "[EPILOGUE] Logged methods cannot be void", method);
valid = false;
}
if (!method.getParameters().isEmpty()) {
processingEnv
.getMessager()
.printMessage(
Diagnostic.Kind.ERROR,
"[EPILOGUE] Logged methods cannot accept arguments",
method);
valid = false;
}
}
}
return valid;
}
/**
* Checks if a type is not loggable.
*
* @param type the type to check
*/
private boolean isNotLoggable(Element element, TypeMirror type) {
if (type instanceof NoType) {
// e.g. void, cannot log
return true;
}
boolean loggable = m_handlers.stream().anyMatch(h -> h.isLoggable(element));
if (loggable) {
return false;
}
processingEnv
.getMessager()
.printMessage(
Diagnostic.Kind.NOTE,
"[EPILOGUE] Excluded from logs because " + type + " is not a loggable data type",
element);
return true;
}
@SuppressWarnings("unchecked")
private Map<DeclaredType, DeclaredType> processCustomLoggers(
RoundEnvironment roundEnv, TypeElement customLoggerAnnotation) {
// map logged type to its custom logger, eg
// { Point.class => CustomPointLogger.class }
var customLoggers = new HashMap<DeclaredType, DeclaredType>();
var annotatedElements = roundEnv.getElementsAnnotatedWith(customLoggerAnnotation);
var loggerSuperClass =
processingEnv
.getElementUtils()
.getTypeElement("edu.wpi.first.epilogue.logging.ClassSpecificLogger");
for (Element annotatedElement : annotatedElements) {
List<AnnotationValue> targetTypes = List.of();
for (AnnotationMirror annotationMirror : annotatedElement.getAnnotationMirrors()) {
for (var entry : annotationMirror.getElementValues().entrySet()) {
if ("value".equals(entry.getKey().getSimpleName().toString())) {
targetTypes = (List<AnnotationValue>) entry.getValue().getValue();
}
}
}
boolean hasPublicNoArgConstructor =
annotatedElement.getEnclosedElements().stream()
.anyMatch(
enclosedElement ->
enclosedElement instanceof ExecutableElement exe
&& exe.getKind() == ElementKind.CONSTRUCTOR
&& exe.getModifiers().contains(Modifier.PUBLIC)
&& exe.getParameters().isEmpty());
if (!hasPublicNoArgConstructor) {
processingEnv
.getMessager()
.printMessage(
Diagnostic.Kind.ERROR,
"Logger classes must have a public no-argument constructor",
annotatedElement);
continue;
}
for (AnnotationValue value : targetTypes) {
var targetType = (DeclaredType) value.getValue();
var reflectedTarget = targetType.asElement();
// eg ClassSpecificLogger<MyDataType>
var requiredSuperClass =
processingEnv
.getTypeUtils()
.getDeclaredType(
loggerSuperClass,
processingEnv.getTypeUtils().getWildcardType(null, reflectedTarget.asType()));
if (customLoggers.containsKey(targetType)) {
processingEnv
.getMessager()
.printMessage(
Diagnostic.Kind.ERROR,
"Multiple custom loggers detected for type " + targetType,
annotatedElement);
continue;
}
if (!processingEnv
.getTypeUtils()
.isAssignable(annotatedElement.asType(), requiredSuperClass)) {
processingEnv
.getMessager()
.printMessage(
Diagnostic.Kind.ERROR,
"Not a subclass of ClassSpecificLogger<" + targetType + ">",
annotatedElement);
continue;
}
customLoggers.put(targetType, (DeclaredType) annotatedElement.asType());
}
}
return customLoggers;
}
private void processEpilogue(RoundEnvironment roundEnv, TypeElement epilogueAnnotation) {
var annotatedElements = roundEnv.getElementsAnnotatedWith(epilogueAnnotation);
List<String> loggerClassNames = new ArrayList<>();
var mainRobotClasses = new ArrayList<TypeElement>();
// Used to check for a main robot class
var robotBaseClass =
processingEnv.getElementUtils().getTypeElement("edu.wpi.first.wpilibj.TimedRobot").asType();
boolean validFields = validateFields(annotatedElements);
boolean validMethods = validateMethods(annotatedElements);
if (!(validFields && validMethods)) {
// Generate nothing and bail
return;
}
var classes =
annotatedElements.stream()
.filter(e -> e instanceof TypeElement)
.map(e -> (TypeElement) e)
.toList();
for (TypeElement clazz : classes) {
try {
warnOfNonLoggableElements(clazz);
m_loggerGenerator.writeLoggerFile(clazz);
if (processingEnv.getTypeUtils().isAssignable(clazz.getSuperclass(), robotBaseClass)) {
mainRobotClasses.add(clazz);
}
loggerClassNames.add(StringUtils.loggerClassName(clazz));
} catch (IOException e) {
processingEnv
.getMessager()
.printMessage(
Diagnostic.Kind.ERROR,
"Could not write logger file for " + clazz.getQualifiedName(),
clazz);
e.printStackTrace(System.err);
}
}
// Sort alphabetically
mainRobotClasses.sort(Comparator.comparing(c -> c.getSimpleName().toString()));
m_epiloguerGenerator.writeEpilogueFile(loggerClassNames, mainRobotClasses);
}
private void warnOfNonLoggableElements(TypeElement clazz) {
var config = clazz.getAnnotation(Logged.class);
if (config.strategy() == Logged.Strategy.OPT_IN) {
// field and method validations will have already checked everything
return;
}
for (Element element : clazz.getEnclosedElements()) {
if (element.getAnnotation(NotLogged.class) != null) {
// Explicitly opted out from, don't need to check
continue;
}
if (element.getModifiers().contains(Modifier.STATIC)) {
// static elements are never logged
continue;
}
if (element instanceof VariableElement v) {
// isNotLoggable will internally print a warning message
isNotLoggable(v, v.asType());
}
if (element instanceof ExecutableElement exe
&& exe.getModifiers().contains(Modifier.PUBLIC)
&& exe.getParameters().isEmpty()) {
// isNotLoggable will internally print a warning message
isNotLoggable(exe, exe.getReturnType());
}
}
}
}

View File

@@ -0,0 +1,75 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue.processor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.type.ArrayType;
import javax.lang.model.type.PrimitiveType;
import javax.lang.model.type.TypeMirror;
/**
* Arrays of bytes, ints, flats, doubles, booleans, Strings, and struct-serializable objects can be
* logged. No other array types - including multidimensional arrays - are loggable.
*/
public class ArrayHandler extends ElementHandler {
private final StructHandler m_structHandler;
private final TypeMirror m_javaLangString;
protected ArrayHandler(ProcessingEnvironment processingEnv) {
super(processingEnv);
// use a struct handler for managing struct arrays
m_structHandler = new StructHandler(processingEnv);
m_javaLangString = lookupTypeElement(processingEnv, "java.lang.String").asType();
}
@Override
public boolean isLoggable(Element element) {
return dataType(element) instanceof ArrayType arr
&& isLoggableComponentType(arr.getComponentType());
}
/**
* Checks if an array containing elements of the given type can be logged.
*
* @param type the data type to check
* @return true if an array like {@code type[]} can be logged, false otherwise
*/
public boolean isLoggableComponentType(TypeMirror type) {
if (type instanceof PrimitiveType primitive) {
return switch (primitive.getKind()) {
case BYTE, INT, LONG, FLOAT, DOUBLE, BOOLEAN -> true;
default -> false;
};
}
return m_structHandler.isLoggableType(type)
|| m_processingEnv.getTypeUtils().isAssignable(type, m_javaLangString);
}
@Override
public String logInvocation(Element element) {
var dataType = dataType(element);
// known to be an array type (assuming isLoggable is checked first); this is a safe cast
var componentType = ((ArrayType) dataType).getComponentType();
if (m_structHandler.isLoggableType(componentType)) {
// Struct arrays need to pass in the struct serializer
return "dataLogger.log(\""
+ loggedName(element)
+ "\", "
+ elementAccess(element)
+ ", "
+ m_structHandler.structAccess(componentType)
+ ")";
} else {
// Primitive or string array
return "dataLogger.log(\"" + loggedName(element) + "\", " + elementAccess(element) + ")";
}
}
}

View File

@@ -0,0 +1,57 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue.processor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
/**
* Collections of strings and structs are loggable. Collections of boxed primitive types are not.
*/
public class CollectionHandler extends ElementHandler {
private final ArrayHandler m_arrayHandler;
private final TypeMirror m_collectionType;
private final StructHandler m_structHandler;
protected CollectionHandler(ProcessingEnvironment processingEnv) {
super(processingEnv);
m_arrayHandler = new ArrayHandler(processingEnv);
m_collectionType =
processingEnv.getElementUtils().getTypeElement("java.util.Collection").asType();
m_structHandler = new StructHandler(processingEnv);
}
@Override
public boolean isLoggable(Element element) {
var dataType = dataType(element);
return m_processingEnv
.getTypeUtils()
.isAssignable(dataType, m_processingEnv.getTypeUtils().erasure(m_collectionType))
&& dataType instanceof DeclaredType decl
&& decl.getTypeArguments().size() == 1
&& m_arrayHandler.isLoggableComponentType(decl.getTypeArguments().get(0));
}
@Override
public String logInvocation(Element element) {
var dataType = dataType(element);
var componentType = ((DeclaredType) dataType).getTypeArguments().get(0);
if (m_structHandler.isLoggableType(componentType)) {
return "dataLogger.log(\""
+ loggedName(element)
+ "\", "
+ elementAccess(element)
+ ", "
+ m_structHandler.structAccess(componentType)
+ ")";
} else {
return "dataLogger.log(\"" + loggedName(element) + "\", " + elementAccess(element) + ")";
}
}
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue.processor;
import java.util.Map;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
public class ConfiguredLoggerHandler extends ElementHandler {
private final Map<TypeMirror, DeclaredType> m_customLoggers;
protected ConfiguredLoggerHandler(
ProcessingEnvironment processingEnv, Map<TypeMirror, DeclaredType> customLoggers) {
super(processingEnv);
this.m_customLoggers = customLoggers;
}
@Override
public boolean isLoggable(Element element) {
return m_customLoggers.containsKey(dataType(element));
}
@Override
public String logInvocation(Element element) {
var dataType = dataType(element);
var loggerType = m_customLoggers.get(dataType);
return "Epilogue."
+ StringUtils.lowerCamelCase(loggerType.asElement().getSimpleName())
+ ".tryUpdate(dataLogger.getSubLogger(\""
+ loggedName(element)
+ "\"), "
+ elementAccess(element)
+ ", Epilogue.getConfig().errorHandler)";
}
}

View File

@@ -0,0 +1,136 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue.processor;
import edu.wpi.first.epilogue.Logged;
import edu.wpi.first.epilogue.logging.ClassSpecificLogger;
import edu.wpi.first.epilogue.logging.DataLogger;
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.TypeElement;
import javax.lang.model.element.VariableElement;
import javax.lang.model.type.TypeMirror;
/**
* Handles logging of fields or methods. An element that passes the {@link #isLoggable(Element)}
* check guarantees that {@link #logInvocation(Element)} will generate a code snippet that will log
* that element. Some subclasses may return {@code null} for the invocation to signal that the
* element should not be logged, but still be considered loggable for the purposes of error
* messaging during the compilation phase.
*/
public abstract class ElementHandler {
protected final ProcessingEnvironment m_processingEnv;
/**
* Instantiates the handler.
*
* @param processingEnv the processing environment, used to look up type information
*/
protected ElementHandler(ProcessingEnvironment processingEnv) {
this.m_processingEnv = processingEnv;
}
protected static TypeElement lookupTypeElement(ProcessingEnvironment processingEnv, String name) {
return processingEnv.getElementUtils().getTypeElement(name);
}
/**
* Gets the type of data that would be logged by a field or method.
*
* @param element the field or method element to check
* @return the logged datatype
*/
protected TypeMirror dataType(Element element) {
if (element instanceof VariableElement field) {
return field.asType();
} else if (element instanceof ExecutableElement method) {
return method.getReturnType();
} else {
throw new IllegalStateException("Unexpected" + element.getClass().getName());
}
}
/**
* Gets the name of a field or method as it would appear in logs.
*
* @param element the field or method element to check
* @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);
if (config != null && !config.name().isBlank()) {
return config.name();
} else {
return elementName;
}
}
/**
* Generates the code snippet to use to access a field or method on a logged object. Private
* fields are accessed via {@link java.lang.invoke.VarHandle VarHandles} and private methods are
* accessed via {@link java.lang.invoke.MethodHandle MethodHandles} (note that this requires the
* logger file to generate those fields). Because the generated logger files are in the same
* package as the logged type, package-private, protected, and public fields and methods are
* always accessible using normal field reads and method calls. Values returned by {@code
* VarHandle} and {@code MethodHandle} invocations will be cast to the appropriate data type.
*
* @param element the element to generate the access for
* @return the generated access snippet
*/
public String elementAccess(Element element) {
if (element instanceof VariableElement field) {
return fieldAccess(field);
} else if (element instanceof ExecutableElement method) {
return methodAccess(method);
} else {
throw new IllegalStateException("Unexpected" + element.getClass().getName());
}
}
private static String fieldAccess(VariableElement field) {
if (field.getModifiers().contains(Modifier.PRIVATE)) {
// (com.example.Foo) $fooField.get(object)
return "(" + field.asType() + ") $" + field.getSimpleName() + ".get(object)";
} else {
// object.fooField
return "object." + field.getSimpleName();
}
}
private static String methodAccess(ExecutableElement method) {
if (method.getModifiers().contains(Modifier.PRIVATE)) {
// (com.example.Foo) _getFoo.invoke(object)
// NOTE: Currently, only public methods are logged, so this branch will not be used
return "(" + method.getReturnType() + ") _" + method.getSimpleName() + ".invoke(object)";
} else {
// object.getFoo()
return "object." + method.getSimpleName() + "()";
}
}
/**
* Checks if a field or method can be logged by this handler.
*
* @param element the field or method element to check
* @return true if the element can be logged, false if not
*/
public abstract boolean isLoggable(Element element);
/**
* Generates a code snippet to place in a generated logger file to log the value of a field or
* method. Log invocations are placed in a generated implementation of {@link
* ClassSpecificLogger#update(DataLogger, Object)}, with access to the data logger and logged
* object passed to the method call.
*
* @param element the field or method element to generate the logger call for
* @return the generated log invocation
*/
public abstract String logInvocation(Element element);
}

View File

@@ -0,0 +1,33 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue.processor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.type.TypeMirror;
public class EnumHandler extends ElementHandler {
private final TypeMirror m_javaLangEnum;
protected EnumHandler(ProcessingEnvironment processingEnv) {
super(processingEnv);
// raw type java.lang.Enum
m_javaLangEnum =
processingEnv
.getTypeUtils()
.erasure(processingEnv.getElementUtils().getTypeElement("java.lang.Enum").asType());
}
@Override
public boolean isLoggable(Element element) {
return m_processingEnv.getTypeUtils().isAssignable(dataType(element), m_javaLangEnum);
}
@Override
public String logInvocation(Element element) {
return "dataLogger.log(\"" + loggedName(element) + "\", " + elementAccess(element) + ")";
}
}

View File

@@ -0,0 +1,172 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue.processor;
import edu.wpi.first.epilogue.EpilogueConfiguration;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
/**
* Generates the {@code Epilogue} file used as the main entry point to logging with Epilogue in a
* robot program. {@code Epilogue} has instances of every generated logger class, a {@link
* EpilogueConfiguration config} object, and (if the main robot class inherits from {@link
* edu.wpi.first.wpilibj.TimedRobot TimedRobot}) a {@code bind()} method to automatically add a
* periodic logging call to the robot.
*/
public class EpilogueGenerator {
private final ProcessingEnvironment m_processingEnv;
private final Map<TypeMirror, DeclaredType> m_customLoggers;
public EpilogueGenerator(
ProcessingEnvironment processingEnv, Map<TypeMirror, DeclaredType> customLoggers) {
this.m_processingEnv = processingEnv;
this.m_customLoggers = customLoggers;
}
/**
* Creates the Epilogue file, which is the main entry point for users to set up and interact with
* the generated loggers.
*
* @param loggerClassNames the names of the generated logger classes. Each of these will be
* instantiated in a public static field on the Epilogue class.
* @param mainRobotClasses the main robot classes. May be empty. Used to generate a {@code bind()}
* method to add a callback hook to a TimedRobot to log itself.
*/
@SuppressWarnings("checkstyle:LineLength") // Source code templates exceed the line length limit
public void writeEpilogueFile(
List<String> loggerClassNames, Collection<TypeElement> mainRobotClasses) {
try {
var centralStore =
m_processingEnv.getFiler().createSourceFile("edu.wpi.first.epilogue.Epilogue");
try (var out = new PrintWriter(centralStore.openOutputStream())) {
out.println("package edu.wpi.first.epilogue;");
out.println();
loggerClassNames.stream()
.sorted()
.forEach(
name -> {
if (!name.contains(".")) {
// Logger is in the global namespace, don't need to import
return;
}
out.println("import " + name + ";");
});
m_customLoggers.values().stream()
.distinct()
.forEach(
loggerType -> {
var name = loggerType.asElement().toString();
if (!name.contains(".")) {
// Logger is in the global namespace, don't need to import
return;
}
out.println("import " + name + ";");
});
out.println();
out.println("public final class Epilogue {");
out.println(
" private static final EpilogueConfiguration config = new EpilogueConfiguration();");
out.println();
loggerClassNames.forEach(
name -> {
String simple = StringUtils.simpleName(name);
// public static final FooLogger fooLogger = new FooLogger();
out.print(" public static final ");
out.print(simple);
out.print(" ");
out.print(StringUtils.lowerCamelCase(simple));
out.print(" = new ");
out.print(simple);
out.println("();");
});
m_customLoggers.values().stream()
.distinct()
.forEach(
loggerType -> {
var loggerTypeName = loggerType.asElement().getSimpleName();
out.println(
" public static final "
+ loggerTypeName
+ " "
+ StringUtils.lowerCamelCase(loggerTypeName)
+ " = new "
+ loggerTypeName
+ "();");
});
out.println();
out.println(
"""
public static void configure(java.util.function.Consumer<EpilogueConfiguration> configurator) {
configurator.accept(config);
}
public static EpilogueConfiguration getConfig() {
return config;
}
""");
out.println(
"""
/**
* Checks if data associated with a given importance level should be logged.
*/
public static boolean shouldLog(Logged.Importance importance) {
return importance.compareTo(config.minimumImportance) >= 0;
}
"""
.stripTrailing());
// Only generate a binding if the robot class is a TimedRobot
if (!mainRobotClasses.isEmpty()) {
for (TypeElement mainRobotClass : mainRobotClasses) {
String robotClassName = mainRobotClass.getQualifiedName().toString();
out.println();
out.print(
"""
/**
* Binds Epilogue updates to a timed robot's update period. Log calls will be made at the
* same update rate as the robot's loop function, but will be offset by a full phase
* (for example, a 20ms update rate but 10ms offset from the main loop invocation) to
* help avoid high CPU loads. However, this does mean that any logged data that reads
* directly from sensors will be slightly different from data used in the main robot
* loop.
*/
""");
out.println(" public static void bind(" + robotClassName + " robot) {");
out.println(" robot.addPeriodic(() -> {");
out.println(" long start = System.nanoTime();");
out.println(
" "
+ StringUtils.loggerFieldName(mainRobotClass)
+ ".tryUpdate(config.dataLogger.getSubLogger(config.root), robot, config.errorHandler);");
out.println(
" edu.wpi.first.networktables.NetworkTableInstance.getDefault().getEntry(\"Epilogue/Stats/Last Run\").setDouble((System.nanoTime() - start) / 1e6);");
out.println(" }, robot.getPeriod(), robot.getPeriod() / 2);");
out.println(" }");
}
}
out.println("}");
}
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@@ -0,0 +1,43 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue.processor;
import edu.wpi.first.epilogue.Logged;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeMirror;
/** Handles logging for types annotated with the {@link Logged @Logged} annotation. */
public class LoggableHandler extends ElementHandler {
protected LoggableHandler(ProcessingEnvironment processingEnv) {
super(processingEnv);
}
@Override
public boolean isLoggable(Element element) {
var dataType = dataType(element);
return dataType.getAnnotation(Logged.class) != null
|| dataType instanceof DeclaredType decl
&& decl.asElement().getAnnotation(Logged.class) != null;
}
@Override
public String logInvocation(Element element) {
TypeMirror dataType = dataType(element);
var reflectedType =
m_processingEnv
.getElementUtils()
.getTypeElement(m_processingEnv.getTypeUtils().erasure(dataType).toString());
return "Epilogue."
+ StringUtils.loggerFieldName(reflectedType)
+ ".tryUpdate(dataLogger.getSubLogger(\""
+ loggedName(element)
+ "\"), "
+ elementAccess(element)
+ ", Epilogue.getConfig().errorHandler)";
}
}

View File

@@ -0,0 +1,245 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue.processor;
import static java.util.stream.Collectors.groupingBy;
import static java.util.stream.Collectors.toList;
import edu.wpi.first.epilogue.Logged;
import edu.wpi.first.epilogue.NotLogged;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.EnumMap;
import java.util.List;
import java.util.function.Predicate;
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.TypeElement;
import javax.lang.model.element.VariableElement;
/** Generates logger class files for {@link Logged @Logged}-annotated classes. */
public class LoggerGenerator {
private final ProcessingEnvironment m_processingEnv;
private final List<ElementHandler> m_handlers;
public LoggerGenerator(ProcessingEnvironment processingEnv, List<ElementHandler> handlers) {
this.m_processingEnv = processingEnv;
this.m_handlers = handlers;
}
private static boolean isNotSkipped(Element e) {
return e.getAnnotation(NotLogged.class) == null;
}
/**
* 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
* implement the {@code update()} method to populate a data log with information from an instance
* of the data type.
*
* @param clazz the data type that the logger should support.
* @throws IOException if the file could not be written
*/
public void writeLoggerFile(TypeElement clazz) throws IOException {
var config = clazz.getAnnotation(Logged.class);
boolean requireExplicitOptIn = config.strategy() == Logged.Strategy.OPT_IN;
Predicate<Element> notSkipped = LoggerGenerator::isNotSkipped;
Predicate<Element> 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();
var methodsToLog =
clazz.getEnclosedElements().stream()
.filter(e -> e instanceof ExecutableElement)
.map(e -> (ExecutableElement) e)
.filter(notSkipped)
.filter(optedIn)
.filter(e -> !e.getModifiers().contains(Modifier.STATIC))
.filter(e -> e.getModifiers().contains(Modifier.PUBLIC))
.filter(e -> e.getParameters().isEmpty())
.filter(e -> e.getReceiverType() != null)
.filter(this::isLoggable)
.toList();
writeLoggerFile(clazz.getQualifiedName().toString(), config, fieldsToLog, methodsToLog);
}
private void writeLoggerFile(
String className,
Logged classConfig,
List<VariableElement> loggableFields,
List<ExecutableElement> loggableMethods)
throws IOException {
String packageName = null;
int lastDot = className.lastIndexOf('.');
if (lastDot > 0) {
packageName = className.substring(0, lastDot);
}
String simpleClassName = StringUtils.simpleName(className);
String loggerClassName = className + "Logger";
String loggerSimpleClassName = loggerClassName.substring(lastDot + 1);
// Use the name on the class config to set the generated logger names
// This helps to avoid naming conflicts
if (!classConfig.name().isBlank()) {
loggerSimpleClassName =
StringUtils.capitalize(classConfig.name().replaceAll(" ", "")) + "Logger";
if (lastDot > 0) {
loggerClassName = packageName + "." + loggerSimpleClassName;
} else {
loggerClassName = loggerSimpleClassName;
}
}
var loggerFile = m_processingEnv.getFiler().createSourceFile(loggerClassName);
var privateFields =
loggableFields.stream().filter(e -> e.getModifiers().contains(Modifier.PRIVATE)).toList();
boolean requiresVarHandles = !privateFields.isEmpty();
try (var out = new PrintWriter(loggerFile.openWriter())) {
if (packageName != null) {
// package com.example;
out.println("package " + packageName + ";");
out.println();
}
out.println("import edu.wpi.first.epilogue.Logged;");
out.println("import edu.wpi.first.epilogue.Epilogue;");
out.println("import edu.wpi.first.epilogue.logging.ClassSpecificLogger;");
out.println("import edu.wpi.first.epilogue.logging.DataLogger;");
if (requiresVarHandles) {
out.println("import java.lang.invoke.MethodHandles;");
out.println("import java.lang.invoke.VarHandle;");
}
out.println();
// public class FooLogger implements ClassSpecificLogger<Foo> {
out.println(
"public class "
+ loggerSimpleClassName
+ " extends ClassSpecificLogger<"
+ simpleClassName
+ "> {");
if (requiresVarHandles) {
for (var privateField : privateFields) {
// This field needs a VarHandle to access.
// Cache it in the class to avoid lookups
out.println(" private static final VarHandle $" + privateField.getSimpleName() + ";");
}
out.println();
var clazz = simpleClassName + ".class";
out.println(" static {");
out.println(" try {");
out.println(
" var lookup = MethodHandles.privateLookupIn("
+ clazz
+ ", MethodHandles.lookup());");
for (var privateField : privateFields) {
var fieldName = privateField.getSimpleName();
out.println(
" $"
+ fieldName
+ " = lookup.findVarHandle("
+ clazz
+ ", \""
+ fieldName
+ "\", "
+ m_processingEnv.getTypeUtils().erasure(privateField.asType())
+ ".class);");
}
out.println(" } catch (ReflectiveOperationException e) {");
out.println(
" throw new RuntimeException("
+ "\"[EPILOGUE] Could not load private fields for logging!\", e);");
out.println(" }");
out.println(" }");
out.println();
}
out.println(" public " + loggerSimpleClassName + "() {");
out.println(" super(" + simpleClassName + ".class);");
out.println(" }");
out.println();
// @Override
// public void update(DataLogger dataLogger, Foo object) {
out.println(" @Override");
out.println(" public void update(DataLogger dataLogger, " + simpleClassName + " object) {");
// Build a map of importance levels to the fields logged at those levels
// e.g. { DEBUG: [fieldA, fieldB], INFO: [fieldC], CRITICAL: [fieldD, fieldE, fieldF] }
var loggedElementsByImportance =
Stream.concat(loggableFields.stream(), loggableMethods.stream())
.collect(
groupingBy(
element -> {
var config = element.getAnnotation(Logged.class);
if (config == null) {
// No configuration on this element, fall back to the class-level
// configuration
return classConfig.importance();
} else {
return config.importance();
}
},
() ->
new EnumMap<>(Logged.Importance.class), // EnumMap for consistent ordering
toList()));
loggedElementsByImportance.forEach(
(importance, elements) -> {
out.println(
" if (Epilogue.shouldLog(Logged.Importance." + importance.name() + ")) {");
for (var loggableElement : elements) {
// findFirst for prioritization
var handler =
m_handlers.stream().filter(h -> h.isLoggable(loggableElement)).findFirst();
handler.ifPresent(
h -> {
// May be null if the handler consumes the element but does not actually want it
// to be logged. For example, the sendable handler consumes all sendable types
// but does not log commands or subsystems, to prevent excessive warnings about
// unloggable commands.
var logInvocation = h.logInvocation(loggableElement);
if (logInvocation != null) {
out.println(logInvocation.indent(6).stripTrailing() + ";");
}
});
}
out.println(" }");
});
out.println(" }");
out.println("}");
}
}
private boolean isLoggable(Element element) {
return m_handlers.stream().anyMatch(h -> h.isLoggable(element));
}
}

View File

@@ -0,0 +1,37 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue.processor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.type.TypeMirror;
public class MeasureHandler extends ElementHandler {
private final TypeMirror m_measure;
protected MeasureHandler(ProcessingEnvironment processingEnv) {
super(processingEnv);
m_measure =
processingEnv
.getTypeUtils()
.erasure(
processingEnv
.getElementUtils()
.getTypeElement("edu.wpi.first.units.Measure")
.asType());
}
@Override
public boolean isLoggable(Element element) {
return m_processingEnv.getTypeUtils().isAssignable(dataType(element), m_measure);
}
@Override
public String logInvocation(Element element) {
// DataLogger has builtin support for logging measures
return "dataLogger.log(\"" + loggedName(element) + "\", " + elementAccess(element) + ")";
}
}

View File

@@ -0,0 +1,41 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue.processor;
import static javax.lang.model.type.TypeKind.BOOLEAN;
import static javax.lang.model.type.TypeKind.BYTE;
import static javax.lang.model.type.TypeKind.CHAR;
import static javax.lang.model.type.TypeKind.DOUBLE;
import static javax.lang.model.type.TypeKind.FLOAT;
import static javax.lang.model.type.TypeKind.INT;
import static javax.lang.model.type.TypeKind.LONG;
import static javax.lang.model.type.TypeKind.SHORT;
import java.util.Set;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.type.TypeMirror;
public class PrimitiveHandler extends ElementHandler {
private final TypeMirror m_javaLangString;
protected PrimitiveHandler(ProcessingEnvironment processingEnv) {
super(processingEnv);
m_javaLangString = processingEnv.getElementUtils().getTypeElement("java.lang.String").asType();
}
@Override
public boolean isLoggable(Element element) {
return m_processingEnv.getTypeUtils().isAssignable(dataType(element), m_javaLangString)
|| Set.of(BYTE, CHAR, SHORT, INT, LONG, FLOAT, DOUBLE, BOOLEAN)
.contains(dataType(element).getKind());
}
@Override
public String logInvocation(Element element) {
return "dataLogger.log(\"" + loggedName(element) + "\", " + elementAccess(element) + ")";
}
}

View File

@@ -0,0 +1,56 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue.processor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.type.TypeMirror;
public class SendableHandler extends ElementHandler {
private final TypeMirror m_sendableType;
private final TypeMirror m_commandType;
private final TypeMirror m_subsystemType;
protected SendableHandler(ProcessingEnvironment processingEnv) {
super(processingEnv);
m_sendableType =
lookupTypeElement(processingEnv, "edu.wpi.first.util.sendable.Sendable").asType();
m_commandType =
lookupTypeElement(processingEnv, "edu.wpi.first.wpilibj2.command.Command").asType();
m_subsystemType =
lookupTypeElement(processingEnv, "edu.wpi.first.wpilibj2.command.SubsystemBase").asType();
}
@Override
public boolean isLoggable(Element element) {
var dataType = dataType(element);
// Accept any sendable type. However, the log invocation will return null
// for sendable types that should not be logged (commands, subsystems)
return m_processingEnv.getTypeUtils().isAssignable(dataType, m_sendableType);
}
@Override
public String logInvocation(Element element) {
var dataType = dataType(element);
if (m_processingEnv.getTypeUtils().isAssignable(dataType, m_commandType)
|| m_processingEnv.getTypeUtils().isAssignable(dataType, m_subsystemType)) {
// Do not log commands or subsystems via their sendable implementations
// We accept all sendable objects to prevent them from being reported as not loggable,
// but their sendable implementations do not include helpful information.
// Users are free to provide custom logging implementations for commands, and tag their
// subsystems with @Logged to log their contents automatically
return null;
}
return "logSendable(dataLogger.getSubLogger(\""
+ loggedName(element)
+ "\"), "
+ elementAccess(element)
+ ")";
}
}

View File

@@ -0,0 +1,110 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue.processor;
import edu.wpi.first.epilogue.Logged;
import javax.lang.model.element.TypeElement;
public final class StringUtils {
private StringUtils() {
throw new UnsupportedOperationException("This is a utility class!");
}
/**
* Gets the simple name of a fully qualified class.
*
* @param fqn the fully qualified class name
* @return the simple class name, without a package specifier
*/
public static String simpleName(String fqn) {
return fqn.substring(fqn.lastIndexOf('.') + 1);
}
/**
* Capitalizes a string. The first character will be capitalized, and the rest of the string will
* be left as is.
*
* @param str the string to capitalize
* @return the capitalized string
*/
public static String capitalize(CharSequence str) {
return Character.toUpperCase(str.charAt(0)) + str.subSequence(1, str.length()).toString();
}
/**
* Converts a string to a lower camel-cased version. This requires the input string to only
* consist of alphanumeric characters, without underscores, spaces, or any other special
* character.
*
* @param str the string to convert
* @return the lower camel-case version of the string
*/
public static String lowerCamelCase(CharSequence str) {
StringBuilder builder = new StringBuilder(str.length());
int i = 0;
for (;
i < str.length()
&& (i == 0
|| i == str.length() - 1
|| Character.isUpperCase(str.charAt(i))
&& Character.isUpperCase(str.charAt(i + 1)));
i++) {
builder.append(Character.toLowerCase(str.charAt(i)));
}
builder.append(str.subSequence(i, str.length()));
return builder.toString();
}
/**
* Gets the name of the field used to hold a logger for data of the given type.
*
* @param clazz the data type that the logger supports
* @return the logger field name
*/
public static String loggerFieldName(TypeElement clazz) {
return lowerCamelCase(simpleName(loggerClassName(clazz)));
}
/**
* Gets the name of the generated class used to log data of the given type. This will be
* fully-qualified class name, such as {@code "frc.robot.MyRobotLogger"}.
*
* @param clazz the data type that the logger supports
* @return the logger class name
*/
public static String loggerClassName(TypeElement clazz) {
var config = clazz.getAnnotation(Logged.class);
var className = clazz.getQualifiedName().toString();
String packageName;
int lastDot = className.lastIndexOf('.');
if (lastDot <= 0) {
packageName = null;
} else {
packageName = className.substring(0, lastDot);
}
String loggerClassName;
// Use the name on the class config to set the generated logger names
// This helps to avoid naming conflicts
if (config.name().isBlank()) {
loggerClassName = className + "Logger";
} else {
String cleaned = config.name().replaceAll(" ", "");
var loggerSimpleClassName = StringUtils.capitalize(cleaned) + "Logger";
if (packageName != null) {
loggerClassName = packageName + "." + loggerSimpleClassName;
} else {
loggerClassName = loggerSimpleClassName;
}
}
return loggerClassName;
}
}

View File

@@ -0,0 +1,50 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue.processor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types;
public class StructHandler extends ElementHandler {
private final TypeMirror m_serializable;
private final Types m_typeUtils;
protected StructHandler(ProcessingEnvironment processingEnv) {
super(processingEnv);
m_serializable =
processingEnv
.getElementUtils()
.getTypeElement("edu.wpi.first.util.struct.StructSerializable")
.asType();
m_typeUtils = processingEnv.getTypeUtils();
}
@Override
public boolean isLoggable(Element element) {
return m_typeUtils.isAssignable(dataType(element), m_serializable);
}
public boolean isLoggableType(TypeMirror type) {
return m_typeUtils.isAssignable(type, m_serializable);
}
public String structAccess(TypeMirror serializableType) {
var className = m_typeUtils.erasure(serializableType).toString();
return className + ".struct";
}
@Override
public String logInvocation(Element element) {
return "dataLogger.log(\""
+ loggedName(element)
+ "\", "
+ elementAccess(element)
+ ", "
+ structAccess(dataType(element))
+ ")";
}
}

View File

@@ -0,0 +1,67 @@
// Copyright (c) FIRST and other WPILib contributors.
// Open Source Software; you can modify and/or share it under the terms of
// the WPILib BSD license file in the root directory of this project.
package edu.wpi.first.epilogue.processor;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.type.TypeMirror;
public class SupplierHandler extends ElementHandler {
private final TypeMirror m_booleanSupplier;
private final TypeMirror m_intSupplier;
private final TypeMirror m_longSupplier;
private final TypeMirror m_doubleSupplier;
protected SupplierHandler(ProcessingEnvironment processingEnv) {
super(processingEnv);
m_booleanSupplier =
processingEnv
.getElementUtils()
.getTypeElement("java.util.function.BooleanSupplier")
.asType();
m_intSupplier =
processingEnv.getElementUtils().getTypeElement("java.util.function.IntSupplier").asType();
m_longSupplier =
processingEnv.getElementUtils().getTypeElement("java.util.function.LongSupplier").asType();
m_doubleSupplier =
processingEnv
.getElementUtils()
.getTypeElement("java.util.function.DoubleSupplier")
.asType();
}
@Override
public boolean isLoggable(Element element) {
return m_processingEnv.getTypeUtils().isAssignable(dataType(element), m_booleanSupplier)
|| m_processingEnv.getTypeUtils().isAssignable(dataType(element), m_intSupplier)
|| m_processingEnv.getTypeUtils().isAssignable(dataType(element), m_longSupplier)
|| m_processingEnv.getTypeUtils().isAssignable(dataType(element), m_doubleSupplier);
}
@Override
public String logInvocation(Element element) {
return "dataLogger.log(\"" + loggedName(element) + "\", " + elementAccess(element) + ")";
}
@Override
public String elementAccess(Element element) {
var typeUtils = m_processingEnv.getTypeUtils();
var dataType = dataType(element);
String base = super.elementAccess(element);
if (typeUtils.isAssignable(dataType, m_booleanSupplier)) {
return base + ".getAsBoolean()";
} else if (typeUtils.isAssignable(dataType, m_intSupplier)) {
return base + ".getAsInt()";
} else if (typeUtils.isAssignable(dataType, m_longSupplier)) {
return base + ".getAsLong()";
} else if (typeUtils.isAssignable(dataType, m_doubleSupplier)) {
return base + ".getAsDouble()";
} else {
throw new IllegalArgumentException("Element type is unsupported: " + dataType);
}
}
}