mirror of
https://github.com/wpilibsuite/allwpilib
synced 2026-06-19 00:41:43 +00:00
[javac] Add @MaxLength annotation for limiting lengths of string parameters (#8493)
Useful for eg OpModes, where names have a maximum length Also includes validations for values in opmode annotations like `@Autonomous(name = "...")`; name, group, and description all have maximum allowable lengths
This commit is contained in:
@@ -0,0 +1,185 @@
|
||||
// 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 org.wpilib.javacplugin;
|
||||
|
||||
import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.source.tree.AssignmentTree;
|
||||
import com.sun.source.tree.CompilationUnitTree;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.LiteralTree;
|
||||
import com.sun.source.tree.MethodInvocationTree;
|
||||
import com.sun.source.tree.Tree.Kind;
|
||||
import com.sun.source.tree.UnaryTree;
|
||||
import com.sun.source.util.JavacTask;
|
||||
import com.sun.source.util.TaskEvent;
|
||||
import com.sun.source.util.TaskListener;
|
||||
import com.sun.source.util.TreePath;
|
||||
import com.sun.source.util.TreeScanner;
|
||||
import com.sun.source.util.Trees;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.ExecutableElement;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.element.VariableElement;
|
||||
import javax.tools.Diagnostic;
|
||||
import org.wpilib.annotation.MaxLength;
|
||||
|
||||
public class MaxLengthDetector implements TaskListener {
|
||||
private final JavacTask m_task;
|
||||
private final Set<CompilationUnitTree> m_visitedCUs = new HashSet<>();
|
||||
|
||||
public MaxLengthDetector(JavacTask task) {
|
||||
m_task = task;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finished(TaskEvent e) {
|
||||
// We override `finished` instead of `started` because we want to run after the
|
||||
// ANALYZE attribution phase has completed and assigned types to elements in the AST
|
||||
// Track the visited CUs to avoid re-processing the same CU multiple times when we call
|
||||
// `Trees.getElement()` on a tree path.
|
||||
if (e.getKind() == TaskEvent.Kind.ANALYZE && m_visitedCUs.add(e.getCompilationUnit())) {
|
||||
e.getCompilationUnit().accept(new Scanner(e.getCompilationUnit()), null);
|
||||
}
|
||||
}
|
||||
|
||||
private final class Scanner extends TreeScanner<Void, Void> {
|
||||
private final CompilationUnitTree m_root;
|
||||
private final Trees m_trees;
|
||||
|
||||
Scanner(CompilationUnitTree compilationUnit) {
|
||||
m_root = compilationUnit;
|
||||
m_trees = Trees.instance(m_task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitMethodInvocation(MethodInvocationTree node, Void unused) {
|
||||
// Find the invoked method to determine parameter annotations
|
||||
TreePath selectPath = m_trees.getPath(m_root, node.getMethodSelect());
|
||||
Element element = selectPath != null ? m_trees.getElement(selectPath) : null;
|
||||
List<? extends ExpressionTree> args = node.getArguments();
|
||||
|
||||
if (!(element instanceof ExecutableElement method)) {
|
||||
return super.visitMethodInvocation(node, unused);
|
||||
}
|
||||
|
||||
List<? extends VariableElement> params = method.getParameters();
|
||||
|
||||
for (int i = 0; i < args.size(); i++) {
|
||||
ExpressionTree argument = args.get(i);
|
||||
|
||||
if (!(argument instanceof LiteralTree literal)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!(literal.getValue() instanceof String string)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Determine which parameter this argument maps to (accounting for varargs)
|
||||
int paramIndex = i;
|
||||
if (!params.isEmpty() && method.isVarArgs()) {
|
||||
paramIndex = Math.min(i, params.size() - 1);
|
||||
}
|
||||
|
||||
if (paramIndex >= params.size()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
VariableElement param = params.get(paramIndex);
|
||||
var maxLength = param.getAnnotation(MaxLength.class);
|
||||
if (maxLength == null || string.length() <= maxLength.value()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
m_trees.printMessage(
|
||||
Diagnostic.Kind.ERROR,
|
||||
("String literal exceeds maximum length: \"%s\""
|
||||
+ " (%d characters) is longer than %d character%s")
|
||||
.formatted(
|
||||
string, string.length(), maxLength.value(), maxLength.value() == 1 ? "" : "s"),
|
||||
literal,
|
||||
m_root);
|
||||
}
|
||||
|
||||
return super.visitMethodInvocation(node, unused);
|
||||
}
|
||||
|
||||
// Checks for @MaxLength annotations with invalid configurations (zero or negative lengths)
|
||||
@Override
|
||||
public Void visitAnnotation(AnnotationTree node, Void unused) {
|
||||
// Validate usages of @MaxLength annotation: value must be >= 1
|
||||
TreePath annoTypePath = m_trees.getPath(m_root, node.getAnnotationType());
|
||||
Element annoElement = annoTypePath != null ? m_trees.getElement(annoTypePath) : null;
|
||||
|
||||
if (!(annoElement instanceof TypeElement typeElem)
|
||||
|| !"org.wpilib.annotation.MaxLength".contentEquals(typeElem.getQualifiedName())) {
|
||||
return super.visitAnnotation(node, unused);
|
||||
}
|
||||
|
||||
// Extract the annotation's single parameter "value"
|
||||
ExpressionTree valueExpr = null;
|
||||
List<? extends ExpressionTree> args = node.getArguments();
|
||||
if (args != null && !args.isEmpty()) {
|
||||
for (ExpressionTree arg : args) {
|
||||
if (arg instanceof AssignmentTree assign) {
|
||||
if ("value".equals(assign.getVariable().toString())) {
|
||||
valueExpr = assign.getExpression();
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
// Single unnamed argument form: @MaxLength(5)
|
||||
valueExpr = arg;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Integer constValue = evaluateIntConstant(valueExpr);
|
||||
if (constValue != null && constValue < 1) {
|
||||
m_trees.printMessage(
|
||||
Diagnostic.Kind.ERROR,
|
||||
"@MaxLength value must be >= 1 (was " + constValue + ")",
|
||||
valueExpr,
|
||||
m_root);
|
||||
}
|
||||
|
||||
return super.visitAnnotation(node, unused);
|
||||
}
|
||||
|
||||
private Integer evaluateIntConstant(ExpressionTree expr) {
|
||||
return switch (expr) {
|
||||
case null -> null;
|
||||
|
||||
// Literal like 0, 1, etc.
|
||||
case LiteralTree lit when lit.getValue() instanceof Integer i -> {
|
||||
yield i;
|
||||
}
|
||||
|
||||
// Handle unary minus of an int literal, e.g., -1
|
||||
case UnaryTree unary
|
||||
when unary.getKind() == Kind.UNARY_MINUS
|
||||
&& unary.getExpression() instanceof LiteralTree literal
|
||||
&& literal.getValue() instanceof Integer i -> {
|
||||
yield -i;
|
||||
}
|
||||
|
||||
default -> {
|
||||
// Handle references to compile-time constants (static final int)
|
||||
TreePath path = m_trees.getPath(m_root, expr);
|
||||
if (path != null
|
||||
&& m_trees.getElement(path) instanceof VariableElement var
|
||||
&& var.getConstantValue() instanceof Integer i) {
|
||||
yield i;
|
||||
}
|
||||
|
||||
yield null;
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,159 @@
|
||||
// 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 org.wpilib.javacplugin;
|
||||
|
||||
import com.sun.source.tree.AnnotationTree;
|
||||
import com.sun.source.tree.AssignmentTree;
|
||||
import com.sun.source.tree.CompilationUnitTree;
|
||||
import com.sun.source.tree.ExpressionTree;
|
||||
import com.sun.source.tree.LiteralTree;
|
||||
import com.sun.source.util.JavacTask;
|
||||
import com.sun.source.util.TaskEvent;
|
||||
import com.sun.source.util.TaskListener;
|
||||
import com.sun.source.util.TreePath;
|
||||
import com.sun.source.util.TreeScanner;
|
||||
import com.sun.source.util.Trees;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import javax.lang.model.element.Element;
|
||||
import javax.lang.model.element.TypeElement;
|
||||
import javax.lang.model.element.VariableElement;
|
||||
import javax.tools.Diagnostic;
|
||||
|
||||
/**
|
||||
* Validates opmode annotations {@code @Autonomous}, {@code @Teleop}, {@code @TestOpMode}.
|
||||
*
|
||||
* <p>Requirements:
|
||||
*
|
||||
* <ul>
|
||||
* <li>Name must be <= 32 characters
|
||||
* <li>Group must be <= 12 characters
|
||||
* <li>Description must be <= 64 characters
|
||||
* </ul>
|
||||
*/
|
||||
public class OpModeAnnotationValidator implements TaskListener {
|
||||
private final JavacTask m_task;
|
||||
private final Set<CompilationUnitTree> m_visitedCUs = new HashSet<>();
|
||||
|
||||
/** The maximum number of permitted characters for an opmode name. */
|
||||
private static final int NAME_MAX_LENGTH = 32;
|
||||
|
||||
/** The maximum number of permitted characters for an opmode group name. */
|
||||
private static final int GROUP_MAX_LENGTH = 12;
|
||||
|
||||
/** The maximum number of permitted characters for an opmode description. */
|
||||
private static final int DESCRIPTION_MAX_LENGTH = 64;
|
||||
|
||||
public OpModeAnnotationValidator(JavacTask task) {
|
||||
m_task = task;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finished(TaskEvent e) {
|
||||
// We override `finished` instead of `started` because we want to run after the
|
||||
// ANALYZE attribution phase has completed and assigned types to elements in the AST
|
||||
// Track the visited CUs to avoid re-processing the same CU multiple times when we call
|
||||
// `Trees.getElement()` on a tree path.
|
||||
if (e.getKind() == TaskEvent.Kind.ANALYZE && m_visitedCUs.add(e.getCompilationUnit())) {
|
||||
e.getCompilationUnit().accept(new Scanner(e.getCompilationUnit()), null);
|
||||
}
|
||||
}
|
||||
|
||||
private final class Scanner extends TreeScanner<Void, Void> {
|
||||
private final CompilationUnitTree m_root;
|
||||
private final Trees m_trees;
|
||||
|
||||
Scanner(CompilationUnitTree compilationUnit) {
|
||||
m_root = compilationUnit;
|
||||
m_trees = Trees.instance(m_task);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void visitAnnotation(AnnotationTree node, Void unused) {
|
||||
// Identify if this is one of the target annotations
|
||||
TreePath annoTypePath = m_trees.getPath(m_root, node.getAnnotationType());
|
||||
Element annoElement = annoTypePath != null ? m_trees.getElement(annoTypePath) : null;
|
||||
|
||||
if (!(annoElement instanceof TypeElement typeElem)) {
|
||||
return super.visitAnnotation(node, unused);
|
||||
}
|
||||
|
||||
CharSequence qname = typeElem.getQualifiedName();
|
||||
boolean isAutonomous = "org.wpilib.opmode.Autonomous".contentEquals(qname);
|
||||
boolean isTeleop = "org.wpilib.opmode.Teleop".contentEquals(qname);
|
||||
boolean isTestOpMode = "org.wpilib.opmode.TestOpMode".contentEquals(qname);
|
||||
|
||||
if (!(isAutonomous || isTeleop || isTestOpMode)) {
|
||||
return super.visitAnnotation(node, unused);
|
||||
}
|
||||
|
||||
// Extract provided attributes (they are optional). Only check name, group, description
|
||||
ExpressionTree nameExpr = null;
|
||||
ExpressionTree groupExpr = null;
|
||||
ExpressionTree descriptionExpr = null;
|
||||
|
||||
var args = node.getArguments();
|
||||
if (args != null) {
|
||||
for (ExpressionTree arg : args) {
|
||||
if (arg instanceof AssignmentTree assign) {
|
||||
String key = assign.getVariable().toString();
|
||||
switch (key) {
|
||||
case "name" -> nameExpr = assign.getExpression();
|
||||
case "group" -> groupExpr = assign.getExpression();
|
||||
case "description" -> descriptionExpr = assign.getExpression();
|
||||
default -> {
|
||||
// Not a field we're validating, ignore it
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
checkLength(typeElem.getSimpleName(), "name", NAME_MAX_LENGTH, nameExpr);
|
||||
checkLength(typeElem.getSimpleName(), "group", GROUP_MAX_LENGTH, groupExpr);
|
||||
checkLength(typeElem.getSimpleName(), "description", DESCRIPTION_MAX_LENGTH, descriptionExpr);
|
||||
|
||||
return super.visitAnnotation(node, unused);
|
||||
}
|
||||
|
||||
private void checkLength(
|
||||
CharSequence typeName, String fieldName, int max, ExpressionTree valueExpr) {
|
||||
String value = evaluateStringConstant(valueExpr);
|
||||
if (value == null || value.length() <= max) {
|
||||
// If not provided or not a constant expression we can evaluate, do nothing
|
||||
// If below the permitted maximum, leave it alone
|
||||
return;
|
||||
}
|
||||
|
||||
m_trees.printMessage(
|
||||
Diagnostic.Kind.ERROR,
|
||||
"@%s opmode %s must be <= %d characters (was %d)"
|
||||
.formatted(typeName, fieldName, max, value.length()),
|
||||
valueExpr,
|
||||
m_root);
|
||||
}
|
||||
|
||||
private String evaluateStringConstant(ExpressionTree expr) {
|
||||
if (expr == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Direct literal
|
||||
if (expr instanceof LiteralTree lit && lit.getValue() instanceof String string) {
|
||||
return string;
|
||||
}
|
||||
|
||||
// Reference to a compile-time constant variable
|
||||
TreePath path = m_trees.getPath(m_root, expr);
|
||||
if (path != null) {
|
||||
Element el = m_trees.getElement(path);
|
||||
if (el instanceof VariableElement var && var.getConstantValue() instanceof String string) {
|
||||
return string;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,8 @@ public class WPILibJavacPlugin implements Plugin {
|
||||
@Override
|
||||
public void init(JavacTask task, String... args) {
|
||||
task.addTaskListener(new ReturnValueUsedListener(task));
|
||||
task.addTaskListener(new MaxLengthDetector(task));
|
||||
task.addTaskListener(new OpModeAnnotationValidator(task));
|
||||
task.addTaskListener(new CoroutineYieldInLoopDetector(task));
|
||||
task.addTaskListener(new CodeAfterCoroutineParkDetector(task));
|
||||
task.addTaskListener(new IncorrectCoroutineUseDetector(task));
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
// 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 org.wpilib.javacplugin;
|
||||
|
||||
import static com.google.testing.compile.CompilationSubject.assertThat;
|
||||
import static com.google.testing.compile.Compiler.javac;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.wpilib.javacplugin.CompileTestUtils.kJavaVersionOptions;
|
||||
|
||||
import com.google.testing.compile.Compilation;
|
||||
import com.google.testing.compile.JavaFileObjects;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class MaxLengthDetectorTest {
|
||||
@Test
|
||||
void stringLiteralLessThanConfiguredMaxLength() {
|
||||
String source =
|
||||
"""
|
||||
package wpilib.robot;
|
||||
|
||||
import org.wpilib.annotation.MaxLength;
|
||||
|
||||
class Example {
|
||||
Example(@MaxLength(10) String arg) {
|
||||
}
|
||||
|
||||
Example() {
|
||||
this("short");
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
Compilation compilation =
|
||||
javac()
|
||||
.withOptions(kJavaVersionOptions)
|
||||
.compile(JavaFileObjects.forSourceString("wpilib.robot.Example", source));
|
||||
|
||||
assertThat(compilation).succeededWithoutWarnings();
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringLiteralLongerThanConfiguredMaxLength() {
|
||||
String source =
|
||||
"""
|
||||
package wpilib.robot;
|
||||
|
||||
import org.wpilib.annotation.MaxLength;
|
||||
|
||||
class Example {
|
||||
Example(@MaxLength(1) String arg) {
|
||||
}
|
||||
|
||||
Example() {
|
||||
this("abcdefghijklmnopqrstuvwxyz1234567890");
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
Compilation compilation =
|
||||
javac()
|
||||
.withOptions(kJavaVersionOptions)
|
||||
.compile(JavaFileObjects.forSourceString("wpilib.robot.Example", source));
|
||||
|
||||
assertThat(compilation).failed();
|
||||
var errors = compilation.errors();
|
||||
assertEquals(1, errors.size());
|
||||
var error = errors.get(0);
|
||||
assertEquals(
|
||||
"String literal exceeds maximum length: \"abcdefghijklmnopqrstuvwxyz1234567890\""
|
||||
+ " (36 characters) is longer than 1 character",
|
||||
error.getMessage(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringLiteralConcatenationLongerThanConfiguredMaxLength() {
|
||||
String source =
|
||||
"""
|
||||
package wpilib.robot;
|
||||
|
||||
import org.wpilib.annotation.MaxLength;
|
||||
|
||||
class Example {
|
||||
Example(@MaxLength(1) String arg) {
|
||||
}
|
||||
|
||||
Example() {
|
||||
this("1" + "2");
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
Compilation compilation =
|
||||
javac()
|
||||
.withOptions(kJavaVersionOptions)
|
||||
.compile(JavaFileObjects.forSourceString("wpilib.robot.Example", source));
|
||||
|
||||
assertThat(compilation).failed();
|
||||
var errors = compilation.errors();
|
||||
assertEquals(1, errors.size());
|
||||
var error = errors.get(0);
|
||||
assertEquals(
|
||||
"String literal exceeds maximum length: \"12\" (2 characters) is longer than 1 character",
|
||||
error.getMessage(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringGenerationLongerThanConfiguredMaxLength() {
|
||||
String source =
|
||||
"""
|
||||
package wpilib.robot;
|
||||
|
||||
import org.wpilib.annotation.MaxLength;
|
||||
|
||||
class Example {
|
||||
Example(@MaxLength(1) String arg) {
|
||||
}
|
||||
|
||||
Example() {
|
||||
this("x".repeat(2));
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
Compilation compilation =
|
||||
javac()
|
||||
.withOptions(kJavaVersionOptions)
|
||||
.compile(JavaFileObjects.forSourceString("wpilib.robot.Example", source));
|
||||
|
||||
// Can't detect this
|
||||
assertThat(compilation).succeededWithoutWarnings();
|
||||
}
|
||||
|
||||
@Test
|
||||
void zeroMaxLength() {
|
||||
String source =
|
||||
"""
|
||||
package wpilib.robot;
|
||||
|
||||
import org.wpilib.annotation.MaxLength;
|
||||
|
||||
class Example {
|
||||
Example(@MaxLength(0) String arg) {
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
Compilation compilation =
|
||||
javac()
|
||||
.withOptions(kJavaVersionOptions)
|
||||
.compile(JavaFileObjects.forSourceString("wpilib.robot.Example", source));
|
||||
|
||||
assertThat(compilation).failed();
|
||||
var errors = compilation.errors();
|
||||
assertEquals(1, errors.size());
|
||||
var error = errors.get(0);
|
||||
assertEquals("@MaxLength value must be >= 1 (was 0)", error.getMessage(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void negativeMaxLength() {
|
||||
String source =
|
||||
"""
|
||||
package wpilib.robot;
|
||||
|
||||
import org.wpilib.annotation.MaxLength;
|
||||
|
||||
class Example {
|
||||
Example(@MaxLength(-123) String arg) {
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
Compilation compilation =
|
||||
javac()
|
||||
.withOptions(kJavaVersionOptions)
|
||||
.compile(JavaFileObjects.forSourceString("wpilib.robot.Example", source));
|
||||
|
||||
assertThat(compilation).failed();
|
||||
var errors = compilation.errors();
|
||||
assertEquals(1, errors.size());
|
||||
var error = errors.get(0);
|
||||
assertEquals("@MaxLength value must be >= 1 (was -123)", error.getMessage(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void constantZeroMaxLength() {
|
||||
String source =
|
||||
"""
|
||||
package wpilib.robot;
|
||||
|
||||
import org.wpilib.annotation.MaxLength;
|
||||
|
||||
class Example {
|
||||
public static final int CONFIGURED_MAX_LENGTH = 0;
|
||||
|
||||
Example(@MaxLength(CONFIGURED_MAX_LENGTH) String arg) {
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
Compilation compilation =
|
||||
javac()
|
||||
.withOptions(kJavaVersionOptions)
|
||||
.compile(JavaFileObjects.forSourceString("wpilib.robot.Example", source));
|
||||
|
||||
assertThat(compilation).failed();
|
||||
var errors = compilation.errors();
|
||||
assertEquals(1, errors.size());
|
||||
var error = errors.get(0);
|
||||
assertEquals("@MaxLength value must be >= 1 (was 0)", error.getMessage(null));
|
||||
}
|
||||
|
||||
@Test
|
||||
void constantNegativeMaxLength() {
|
||||
String source =
|
||||
"""
|
||||
package wpilib.robot;
|
||||
|
||||
import org.wpilib.annotation.MaxLength;
|
||||
|
||||
class Example {
|
||||
public static final int CONFIGURED_MAX_LENGTH = -3;
|
||||
|
||||
Example(@MaxLength(CONFIGURED_MAX_LENGTH) String arg) {
|
||||
}
|
||||
}
|
||||
""";
|
||||
|
||||
Compilation compilation =
|
||||
javac()
|
||||
.withOptions(kJavaVersionOptions)
|
||||
.compile(JavaFileObjects.forSourceString("wpilib.robot.Example", source));
|
||||
|
||||
assertThat(compilation).failed();
|
||||
var errors = compilation.errors();
|
||||
assertEquals(1, errors.size());
|
||||
var error = errors.get(0);
|
||||
assertEquals("@MaxLength value must be >= 1 (was -3)", error.getMessage(null));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// 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 org.wpilib.javacplugin;
|
||||
|
||||
import static com.google.testing.compile.CompilationSubject.assertThat;
|
||||
import static com.google.testing.compile.Compiler.javac;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.wpilib.javacplugin.CompileTestUtils.kJavaVersionOptions;
|
||||
|
||||
import com.google.testing.compile.Compilation;
|
||||
import com.google.testing.compile.JavaFileObjects;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@SuppressWarnings("LineLength") // Inline source code can have long lines
|
||||
class OpModeAnnotationValidatorTest {
|
||||
private static final String AUTONOMOUS_ANNOTATION_SOURCE =
|
||||
"""
|
||||
package org.wpilib.opmode;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface Autonomous {
|
||||
String name() default "";
|
||||
String description() default "";
|
||||
String group() default "";
|
||||
}
|
||||
""";
|
||||
|
||||
private static final String TELEOP_ANNOTATION_SOURCE =
|
||||
"""
|
||||
package org.wpilib.opmode;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface Teleop {
|
||||
String name() default "";
|
||||
String description() default "";
|
||||
String group() default "";
|
||||
}
|
||||
""";
|
||||
|
||||
private static final String TEST_OPMODE_ANNOTATION_SOURCE =
|
||||
"""
|
||||
package org.wpilib.opmode;
|
||||
|
||||
import java.lang.annotation.*;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Target(ElementType.TYPE)
|
||||
public @interface TestOpMode {
|
||||
String name() default "";
|
||||
String description() default "";
|
||||
String group() default "";
|
||||
}
|
||||
""";
|
||||
|
||||
@Test
|
||||
void stringLiteralLessThanConfiguredMaxLength() {
|
||||
String source =
|
||||
"""
|
||||
package wpilib.robot;
|
||||
|
||||
import org.wpilib.opmode.*;
|
||||
|
||||
@Autonomous(name = "Short Name", description = "Short Description", group = "Short Group")
|
||||
@Teleop(name = "Short Name", description = "Short Description", group = "Short Group")
|
||||
@TestOpMode(name = "Short Name", description = "Short Description", group = "Short Group")
|
||||
class Example {
|
||||
}
|
||||
""";
|
||||
|
||||
Compilation compilation =
|
||||
javac()
|
||||
.withOptions(kJavaVersionOptions)
|
||||
.compile(
|
||||
JavaFileObjects.forSourceString(
|
||||
"org.wpilib.opmode.Autonomous", AUTONOMOUS_ANNOTATION_SOURCE),
|
||||
JavaFileObjects.forSourceString(
|
||||
"org.wpilib.opmode.Teleop", TELEOP_ANNOTATION_SOURCE),
|
||||
JavaFileObjects.forSourceString(
|
||||
"org.wpilib.opmode.TestOpMode", TEST_OPMODE_ANNOTATION_SOURCE),
|
||||
JavaFileObjects.forSourceString("wpilib.robot.Example", source));
|
||||
|
||||
assertThat(compilation).succeededWithoutWarnings();
|
||||
}
|
||||
|
||||
@Test
|
||||
void stringLiteralsGreaterThanConfiguredMaxLength() {
|
||||
String source =
|
||||
"""
|
||||
package wpilib.robot;
|
||||
|
||||
import org.wpilib.opmode.*;
|
||||
|
||||
@Autonomous(
|
||||
name = "This is much longer than thirty six characters (I counted them all myself)",
|
||||
description = "This is significantly longer than sixty four characters (it's ninety nine, if you bother to count!)",
|
||||
group = "More than twelve characters long"
|
||||
)
|
||||
@Teleop(
|
||||
name = "This is much longer than thirty six characters (I counted them all myself)",
|
||||
description = "This is significantly longer than sixty four characters (it's ninety nine, if you bother to count!)",
|
||||
group = "More than twelve characters long"
|
||||
)
|
||||
@TestOpMode(
|
||||
name = "This is much longer than thirty six characters (I counted them all myself)",
|
||||
description = "This is significantly longer than sixty four characters (it's ninety nine, if you bother to count!)",
|
||||
group = "More than twelve characters long"
|
||||
)
|
||||
class Example {
|
||||
}
|
||||
""";
|
||||
|
||||
Compilation compilation =
|
||||
javac()
|
||||
.withOptions(kJavaVersionOptions)
|
||||
.compile(
|
||||
JavaFileObjects.forSourceString(
|
||||
"org.wpilib.opmode.Autonomous", AUTONOMOUS_ANNOTATION_SOURCE),
|
||||
JavaFileObjects.forSourceString(
|
||||
"org.wpilib.opmode.Teleop", TELEOP_ANNOTATION_SOURCE),
|
||||
JavaFileObjects.forSourceString(
|
||||
"org.wpilib.opmode.TestOpMode", TEST_OPMODE_ANNOTATION_SOURCE),
|
||||
JavaFileObjects.forSourceString("wpilib.robot.Example", source));
|
||||
|
||||
assertThat(compilation).failed();
|
||||
var errors = compilation.errors();
|
||||
assertEquals(9, errors.size());
|
||||
|
||||
// Autonomous
|
||||
assertEquals(
|
||||
"@Autonomous opmode name must be <= 32 characters (was 74)",
|
||||
errors.get(0).getMessage(null));
|
||||
assertEquals(
|
||||
"@Autonomous opmode group must be <= 12 characters (was 32)",
|
||||
errors.get(1).getMessage(null));
|
||||
assertEquals(
|
||||
"@Autonomous opmode description must be <= 64 characters (was 99)",
|
||||
errors.get(2).getMessage(null));
|
||||
|
||||
// Teleop
|
||||
assertEquals(
|
||||
"@Teleop opmode name must be <= 32 characters (was 74)", errors.get(3).getMessage(null));
|
||||
assertEquals(
|
||||
"@Teleop opmode group must be <= 12 characters (was 32)", errors.get(4).getMessage(null));
|
||||
assertEquals(
|
||||
"@Teleop opmode description must be <= 64 characters (was 99)",
|
||||
errors.get(5).getMessage(null));
|
||||
|
||||
// TestOpMode
|
||||
assertEquals(
|
||||
"@TestOpMode opmode name must be <= 32 characters (was 74)",
|
||||
errors.get(6).getMessage(null));
|
||||
assertEquals(
|
||||
"@TestOpMode opmode group must be <= 12 characters (was 32)",
|
||||
errors.get(7).getMessage(null));
|
||||
assertEquals(
|
||||
"@TestOpMode opmode description must be <= 64 characters (was 99)",
|
||||
errors.get(8).getMessage(null));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user