From 28176f106215d9a74c3e1444164b5383e1757068 Mon Sep 17 00:00:00 2001 From: Sam Carlberg Date: Fri, 6 Mar 2026 17:20:20 -0500 Subject: [PATCH] [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 --- .../wpilib/javacplugin/MaxLengthDetector.java | 185 +++++++++++++ .../OpModeAnnotationValidator.java | 159 ++++++++++++ .../wpilib/javacplugin/WPILibJavacPlugin.java | 2 + .../javacplugin/MaxLengthDetectorTest.java | 242 ++++++++++++++++++ .../OpModeAnnotationValidatorTest.java | 167 ++++++++++++ .../java/org/wpilib/annotation/MaxLength.java | 46 ++++ 6 files changed, 801 insertions(+) create mode 100644 javacPlugin/src/main/java/org/wpilib/javacplugin/MaxLengthDetector.java create mode 100644 javacPlugin/src/main/java/org/wpilib/javacplugin/OpModeAnnotationValidator.java create mode 100644 javacPlugin/src/test/java/org/wpilib/javacplugin/MaxLengthDetectorTest.java create mode 100644 javacPlugin/src/test/java/org/wpilib/javacplugin/OpModeAnnotationValidatorTest.java create mode 100644 wpiannotations/src/main/java/org/wpilib/annotation/MaxLength.java diff --git a/javacPlugin/src/main/java/org/wpilib/javacplugin/MaxLengthDetector.java b/javacPlugin/src/main/java/org/wpilib/javacplugin/MaxLengthDetector.java new file mode 100644 index 0000000000..621f1784b3 --- /dev/null +++ b/javacPlugin/src/main/java/org/wpilib/javacplugin/MaxLengthDetector.java @@ -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 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 { + 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 args = node.getArguments(); + + if (!(element instanceof ExecutableElement method)) { + return super.visitMethodInvocation(node, unused); + } + + List 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 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; + } + }; + } + } +} diff --git a/javacPlugin/src/main/java/org/wpilib/javacplugin/OpModeAnnotationValidator.java b/javacPlugin/src/main/java/org/wpilib/javacplugin/OpModeAnnotationValidator.java new file mode 100644 index 0000000000..1d127a832a --- /dev/null +++ b/javacPlugin/src/main/java/org/wpilib/javacplugin/OpModeAnnotationValidator.java @@ -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}. + * + *

Requirements: + * + *

    + *
  • Name must be <= 32 characters + *
  • Group must be <= 12 characters + *
  • Description must be <= 64 characters + *
+ */ +public class OpModeAnnotationValidator implements TaskListener { + private final JavacTask m_task; + private final Set 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 { + 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; + } + } +} diff --git a/javacPlugin/src/main/java/org/wpilib/javacplugin/WPILibJavacPlugin.java b/javacPlugin/src/main/java/org/wpilib/javacplugin/WPILibJavacPlugin.java index d7ca12d2c7..0a8d5874ba 100644 --- a/javacPlugin/src/main/java/org/wpilib/javacplugin/WPILibJavacPlugin.java +++ b/javacPlugin/src/main/java/org/wpilib/javacplugin/WPILibJavacPlugin.java @@ -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)); diff --git a/javacPlugin/src/test/java/org/wpilib/javacplugin/MaxLengthDetectorTest.java b/javacPlugin/src/test/java/org/wpilib/javacplugin/MaxLengthDetectorTest.java new file mode 100644 index 0000000000..1088173cd0 --- /dev/null +++ b/javacPlugin/src/test/java/org/wpilib/javacplugin/MaxLengthDetectorTest.java @@ -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)); + } +} diff --git a/javacPlugin/src/test/java/org/wpilib/javacplugin/OpModeAnnotationValidatorTest.java b/javacPlugin/src/test/java/org/wpilib/javacplugin/OpModeAnnotationValidatorTest.java new file mode 100644 index 0000000000..462515faaf --- /dev/null +++ b/javacPlugin/src/test/java/org/wpilib/javacplugin/OpModeAnnotationValidatorTest.java @@ -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)); + } +} diff --git a/wpiannotations/src/main/java/org/wpilib/annotation/MaxLength.java b/wpiannotations/src/main/java/org/wpilib/annotation/MaxLength.java new file mode 100644 index 0000000000..eb5e7b452e --- /dev/null +++ b/wpiannotations/src/main/java/org/wpilib/annotation/MaxLength.java @@ -0,0 +1,46 @@ +// 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.annotation; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Place on a method parameter of type String. Any string literals passed to that parameter will be + * checked at compile-time to be no longer than the maximum allowed length. Note that this cannot + * check dynamically generated string values - it is strongly recommended to pair this annotation + * with a runtime check to cover cases where dynamic values are used. + * + *

Errors generated by this annotation cannot be suppressed. + * + *

{@code
+ * void acceptString(@MaxLength(5) String str) {
+ *   if (str.length() > 5) {
+ *     throw new IllegalArgumentException("String is too long");
+ *   }
+ *   // ...
+ * }
+ *
+ * acceptString("12345"); // OK - length is 5
+ * acceptString("123456"); // Compile-time error: length is 6
+ * acceptString("123" + "456"); // Compile-time error: length is 6
+ * acceptString(" ".repeat(16)); // Runtime error - string argument is not a literal
+ * }
+ */ +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.PARAMETER) +@Documented +public @interface MaxLength { + /** + * The maximum allowable length of string literals passed to the annotated parameter. Must be a + * positive integer. + * + * @return The maximum length of allowed strings. + */ + int value(); +}