Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions akit/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ dependencies {
implementation("edu.wpi.first.wpiunits:wpiunits-java:$wpilibVersion")
implementation("edu.wpi.first.hal:hal-java:$wpilibVersion")
implementation("org.ejml:ejml-simple:0.43.1")
implementation("org.reflections:reflections:0.9.12")
implementation("org.jetbrains.kotlin:kotlin-reflect:2.0.0")
implementation("com.fasterxml.jackson.core:jackson-annotations:2.15.2")
implementation("com.fasterxml.jackson.core:jackson-core:2.15.2")
implementation("com.fasterxml.jackson.core:jackson-databind:2.15.2")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
import java.util.function.LongSupplier;
import java.util.function.Supplier;
import org.littletonrobotics.junction.mechanism.LoggedMechanism2d;
import org.reflections.Reflections;
import org.reflections.scanners.SubTypesScanner;

/**
* Manages objects and packages for annotation logging of outputs with {@link
Expand Down Expand Up @@ -58,22 +60,60 @@ static void periodic() {
}
}

/**
* Adds Kotlin classes from the package of the given root object. This method scans the package of
* the root object to identify all Kotlin classes and adds them to the appropriate collection. For
* Kotlin object classes, it calls the {@link #addObject(Object)} method, while for other Kotlin
* classes (such as Kotlin files), it calls the {@link #addKotlinFile(Object)} method.
*
* @param root The root object from which the package will be scanned for Kotlin classes.
*/
public static void addKotlinPackage(Object root) {
Set<Class<?>> kotlinClasses = getAllKotlinClassesInPackage(root.getClass().getPackageName());
for (Class<?> clazz : kotlinClasses) {
if (isKotlinObjectClass(clazz)) {
addObject(clazz);
} else {
addKotlinFile(clazz);
}
}
}

/**
* Adds a Kotlin file to the collection of allowed packages and registers it by invoking {@link
* #addObjectImpl(Object, Class)} with the provided root object and its class. Kotlin's files are
* typically represented by classes ending in "Kt" and aren't object classes.
*
* @param root The Kotlin file to be added.
*/
public static void addKotlinFile(Object root) {
allowedPackages.add(root.getClass().getPackageName());
addObjectImpl(root, (Class<?>) root);
}

/**
* Registers a root object, scanning for loggable fields recursively.
*
* @param root The object to scan recursively.
*/
public static void addObject(Object root) {
allowedPackages.add(root.getClass().getPackageName());
addObjectImpl(root);
String packageName = root.getClass().getPackageName();
if (!allowedPackages.contains(packageName)) {
addKotlinPackage(root);
allowedPackages.add(packageName);
}

addObjectImpl(root, root.getClass());
}

/**
* Registers a root object, scanning for loggable fields recursively.
*
* @param root The object to scan recursively.
* @param rootClass The class of the root object, used for retrieving methods and fields. When
* called in a Java context, it should be root.getClass().
*/
private static void addObjectImpl(Object root) {
private static void addObjectImpl(Object root, Class<?> rootClass) {
// Check if package name is valid
String packageName = root.getClass().getPackageName();
boolean packageNameValid = false;
Expand All @@ -90,18 +130,18 @@ private static void addObjectImpl(Object root) {
scannedObjectHashes.add(root.hashCode());

// If array, loop over individual items
if (root.getClass().isArray()) {
if (rootClass.isArray()) {
Object[] rootArray = (Object[]) root;
for (Object item : rootArray) {
if (item != null) {
addObjectImpl(item);
addObjectImpl(item, rootClass);
}
}
return;
}

// Loop over declared methods
getAllMethods(root.getClass())
getAllMethods(rootClass)
.forEach(
(methodAndDeclaringClass) -> {
Method method = methodAndDeclaringClass.method;
Expand Down Expand Up @@ -183,7 +223,7 @@ private static void addObjectImpl(Object root) {
return;
}
if (fieldValue != null) {
addObjectImpl(fieldValue);
addObjectImpl(fieldValue, rootClass);
}
});
}
Expand Down Expand Up @@ -626,4 +666,42 @@ private static void registerField(
}
}
}

/**
* Checks if a given class is a Kotlin object class. This method checks if the class has a field
* named "INSTANCE", which all objects classes in kotlin have since they are singletons
*
* @param clazz The class to check.
* @return true if the class is a Kotlin object class, false otherwise.
*/
private static boolean isKotlinObjectClass(Class<?> clazz) {
try {
return clazz.getDeclaredField("INSTANCE") != null;
} catch (NoSuchFieldException e) {
return false;
}
}

/**
* Retrieves all Kotlin classes in a specified package.
*
* @param packageName The name of the package to scan for Kotlin classes.
* @return A {@link Set} of {@link Class} objects representing all Kotlin object classes or files
* with top level methods & fields in the specified package.
*/
private static Set<Class<?>> getAllKotlinClassesInPackage(String packageName) {
Reflections reflections = new Reflections(packageName, new SubTypesScanner(false));

Set<Class<?>> allClasses = new HashSet<>(reflections.getSubTypesOf(Object.class));
allClasses.addAll(reflections.getSubTypesOf(AutoCloseable.class));

Set<Class<?>> filteredClasses = new HashSet<>();
for (Class<?> clazz : allClasses) {
if (clazz.getSimpleName().endsWith("Kt") || isKotlinObjectClass(clazz)) {
filteredClasses.add(clazz);
}
}

return filteredClasses;
}
}
4 changes: 4 additions & 0 deletions docs/docs/data-flow/recording-outputs/annotation-logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,7 @@ AutoLogOutputManager.addObject(this);
:::warning
The parent class where `@AutoLogOutput` is used must also be instantiated within the first loop cycle and be accessible by a recursive search of the fields of `Robot`. This feature is primarily intended to log outputs from subsystems and other similar classes. For classes that do not fit the criteria above, call `Logger.recordOutput` periodically to record outputs.
:::

:::tip
Kotlin users should use the `addKotlinPackage` and `addKotlinFile` methods. Check the API documentation for details.
:::