From a1788934c3f3e87d4773ddb97b7afa32f0e37757 Mon Sep 17 00:00:00 2001 From: wilsonwatson Date: Thu, 21 May 2026 20:17:11 -0500 Subject: [PATCH 1/7] refactor --- .../robotools/AnnotationGenerator.java | 28 + .../robotools/AnnotationMirrorVisitor.java | 27 - .../org/frc5572/robotools/BoolVisitor.java | 13 - .../frc5572/robotools/ClassListVisitor.java | 33 - .../org/frc5572/robotools/ClassVisitor.java | 14 - .../org/frc5572/robotools/RobotProcessor.java | 229 +------ .../org/frc5572/robotools/StringVisitor.java | 13 - .../frc5572/robotools/TypeStateBuilder.java | 469 -------------- .../java/org/frc5572/robotools/Utilities.java | 209 ++++++ .../emptyio/GenerateEmptyIOGenerator.java | 100 +++ .../typestate/TypeStateBuilderGenerator.java | 608 ++++++++++++++++++ 11 files changed, 973 insertions(+), 770 deletions(-) create mode 100644 src/main/java/org/frc5572/robotools/AnnotationGenerator.java delete mode 100644 src/main/java/org/frc5572/robotools/AnnotationMirrorVisitor.java delete mode 100644 src/main/java/org/frc5572/robotools/BoolVisitor.java delete mode 100644 src/main/java/org/frc5572/robotools/ClassListVisitor.java delete mode 100644 src/main/java/org/frc5572/robotools/ClassVisitor.java delete mode 100644 src/main/java/org/frc5572/robotools/StringVisitor.java delete mode 100644 src/main/java/org/frc5572/robotools/TypeStateBuilder.java create mode 100644 src/main/java/org/frc5572/robotools/Utilities.java create mode 100644 src/main/java/org/frc5572/robotools/emptyio/GenerateEmptyIOGenerator.java create mode 100644 src/main/java/org/frc5572/robotools/typestate/TypeStateBuilderGenerator.java diff --git a/src/main/java/org/frc5572/robotools/AnnotationGenerator.java b/src/main/java/org/frc5572/robotools/AnnotationGenerator.java new file mode 100644 index 0000000..901f960 --- /dev/null +++ b/src/main/java/org/frc5572/robotools/AnnotationGenerator.java @@ -0,0 +1,28 @@ +package org.frc5572.robotools; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.TypeElement; + +/** + * A specific annotation that generates java classes. + */ +public interface AnnotationGenerator { + + /** + * Provide the generator the processing environment. + */ + public void init(ProcessingEnvironment env); + + /** + * @return the full canonical name of the annotation type this generator handles e.g. + * "frc.robot.util.GenerateEmptyIO" + */ + public String getAnnotationQualifiedName(); + + /** + * Called from the processor for each annotation type on each round. + */ + public void generate(TypeElement annotation, RoundEnvironment roundEnv); + +} diff --git a/src/main/java/org/frc5572/robotools/AnnotationMirrorVisitor.java b/src/main/java/org/frc5572/robotools/AnnotationMirrorVisitor.java deleted file mode 100644 index 1b8ff21..0000000 --- a/src/main/java/org/frc5572/robotools/AnnotationMirrorVisitor.java +++ /dev/null @@ -1,27 +0,0 @@ -package org.frc5572.robotools; - -import java.util.List; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.AnnotationValue; -import javax.lang.model.util.SimpleAnnotationValueVisitor8; - -/** Find all uses of an annotation in annotation values */ -public class AnnotationMirrorVisitor extends SimpleAnnotationValueVisitor8 { - - @Override - public AnnotationMirror visitAnnotation(AnnotationMirror arg0, Void arg1) { - return arg0; - } - - @Override - public AnnotationMirror visitArray(List arg0, Void arg1) { - for (var item : arg0) { - var res = this.visit(item, arg1); - if (res != null) { - return res; - } - } - return null; - } - -} diff --git a/src/main/java/org/frc5572/robotools/BoolVisitor.java b/src/main/java/org/frc5572/robotools/BoolVisitor.java deleted file mode 100644 index c8a2ccc..0000000 --- a/src/main/java/org/frc5572/robotools/BoolVisitor.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.frc5572.robotools; - -import javax.lang.model.util.SimpleAnnotationValueVisitor8; - -/** Find all uses of a string in annotation values */ -public class BoolVisitor extends SimpleAnnotationValueVisitor8 { - - @Override - public Boolean visitBoolean(boolean arg0, Void arg1) { - return arg0; - } - -} diff --git a/src/main/java/org/frc5572/robotools/ClassListVisitor.java b/src/main/java/org/frc5572/robotools/ClassListVisitor.java deleted file mode 100644 index 6b8e9c9..0000000 --- a/src/main/java/org/frc5572/robotools/ClassListVisitor.java +++ /dev/null @@ -1,33 +0,0 @@ -package org.frc5572.robotools; - -import java.util.List; -import javax.lang.model.element.AnnotationValue; -import javax.lang.model.type.TypeMirror; -import javax.lang.model.util.SimpleAnnotationValueVisitor8; - -/** Find all uses of a type in annotation values */ -public class ClassListVisitor extends SimpleAnnotationValueVisitor8 { - - private final List mirrors; - - /** Find all uses of a type in annotation values */ - public ClassListVisitor(List mirrors) { - super(); - this.mirrors = mirrors; - } - - @Override - public Void visitType(TypeMirror arg0, Void arg1) { - mirrors.add(arg0); - return null; - } - - @Override - public Void visitArray(List arg0, Void arg1) { - for (var item : arg0) { - this.visit(item); - } - return null; - } - -} diff --git a/src/main/java/org/frc5572/robotools/ClassVisitor.java b/src/main/java/org/frc5572/robotools/ClassVisitor.java deleted file mode 100644 index 306aa03..0000000 --- a/src/main/java/org/frc5572/robotools/ClassVisitor.java +++ /dev/null @@ -1,14 +0,0 @@ -package org.frc5572.robotools; - -import javax.lang.model.type.TypeMirror; -import javax.lang.model.util.SimpleAnnotationValueVisitor8; - -/** Find all uses of a type in annotation values */ -public class ClassVisitor extends SimpleAnnotationValueVisitor8 { - - @Override - public TypeMirror visitType(TypeMirror arg0, Void arg1) { - return arg0; - } - -} diff --git a/src/main/java/org/frc5572/robotools/RobotProcessor.java b/src/main/java/org/frc5572/robotools/RobotProcessor.java index b7ee52f..424e6d5 100644 --- a/src/main/java/org/frc5572/robotools/RobotProcessor.java +++ b/src/main/java/org/frc5572/robotools/RobotProcessor.java @@ -1,232 +1,59 @@ package org.frc5572.robotools; -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; +import java.util.Arrays; import java.util.Set; -import java.util.concurrent.atomic.AtomicInteger; +import java.util.stream.Collectors; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.ProcessingEnvironment; 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.Element; -import javax.lang.model.element.ElementKind; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.Modifier; -import javax.lang.model.element.PackageElement; import javax.lang.model.element.TypeElement; -import javax.lang.model.element.VariableElement; -import javax.lang.model.type.TypeMirror; -import javax.tools.Diagnostic; -import javax.tools.Diagnostic.Kind; -import com.squareup.javapoet.JavaFile; -import com.squareup.javapoet.MethodSpec; -import com.squareup.javapoet.ParameterSpec; -import com.squareup.javapoet.TypeName; -import com.squareup.javapoet.TypeSpec; +import org.frc5572.robotools.emptyio.GenerateEmptyIOGenerator; +import org.frc5572.robotools.typestate.TypeStateBuilderGenerator; /** * Annotation processor for checks. Used by VS Code. */ -@SupportedAnnotationTypes({"frc.robot.util.typestate.TypeStateBuilder", - "frc.robot.util.GenerateEmptyIO"}) @SupportedSourceVersion(SourceVersion.RELEASE_11) public class RobotProcessor extends AbstractProcessor { + private AnnotationGenerator[] generators = new AnnotationGenerator[] { + // @formatter:off + new GenerateEmptyIOGenerator(), + new TypeStateBuilderGenerator(), + // @formatter:on + }; + /** Initialization function */ @Override public synchronized void init(ProcessingEnvironment processingEnv) { super.init(processingEnv); - } - - /** Process all elements. */ - @Override - public boolean process(Set annotations, RoundEnvironment roundEnv) { - boolean success = false; - for (var anno : annotations) { - // System.out.println("Trying " + anno.getQualifiedName()); - if (anno.getSimpleName().toString().equals("GenerateEmptyIO")) { - processGenerateEmptyIO(anno, roundEnv); - success = true; - } else if (anno.getSimpleName().toString().equals("TypeStateBuilder")) { - processGenerateTypeStateBuilder(anno, roundEnv); - success = true; - } + for (var gen : generators) { + gen.init(processingEnv); } - return success; } - private void processGenerateTypeStateBuilder(TypeElement annotation, - RoundEnvironment roundEnv) { - roundEnv.getElementsAnnotatedWith(annotation).forEach(constructorElement_ -> { - ExecutableElement constructorElement = (ExecutableElement) constructorElement_; - Element parent_ = constructorElement.getEnclosingElement(); - if (!(parent_ instanceof TypeElement)) { - processingEnv.getMessager().printMessage(Kind.ERROR, - "TypeStateBuilder constructor must be the direct child of a TypeElement (e.g. class). Instead found " - + parent_.getKind().toString() + ".", - constructorElement); - } - TypeElement parent = (TypeElement) parent_; - String builderName = parent.getSimpleName() + "Builder"; - String builderPackage = getPackageName(parent); - boolean isLinear = false; - // System.out.println("Processing " + builderPackage + "." + builderName); - for (var mirror : constructorElement.getAnnotationMirrors()) { - if (!mirror.getAnnotationType().asElement().getSimpleName().toString() - .equals("TypeStateBuilder")) { - continue; - } - for (var ev : mirror.getElementValues().entrySet()) { - if (ev.getKey().getSimpleName().toString().equals("value")) { - String res = ev.getValue().accept(new StringVisitor(), null); - if (res != null) { - builderName = res; - } - } else if (ev.getKey().getSimpleName().toString().equals("linear")) { - Boolean res = ev.getValue().accept(new BoolVisitor(), null); - if (res != null) { - isLinear = res; - } - } - } - } - - List fields = new ArrayList<>(); - List params = constructorElement.getParameters(); - for (int i = 0; i < params.size(); i++) { - boolean found = false; - VariableElement param = params.get(i); - for (var mirror : param.getAnnotationMirrors()) { - if (mirror.getAnnotationType().asElement().getSimpleName().toString() - .equals("InitField")) { - if (found) { - processingEnv.getMessager().printMessage(Kind.ERROR, - "Each parameter of a TypeStateBuilder constructor can only have one of @InitField, @RequiredField or @OptionalField", - param); - } - fields.add(new TypeStateBuilder.InitField(param.asType(), - param.getSimpleName().toString())); - found = true; - } else if (mirror.getAnnotationType().asElement().getSimpleName().toString() - .equals("RequiredField")) { - if (found) { - processingEnv.getMessager().printMessage(Kind.ERROR, - "Each parameter of a TypeStateBuilder constructor can only have one of @InitField, @RequiredField or @OptionalField", - param); - } - fields.add(TypeStateBuilder.RequiredField.fromAnnotation(param.asType(), - param.getSimpleName().toString(), mirror)); - found = true; - } else if (mirror.getAnnotationType().asElement().getSimpleName().toString() - .equals("OptionalField")) { - if (found) { - processingEnv.getMessager().printMessage(Kind.ERROR, - "Each parameter of a TypeStateBuilder constructor can only have one of @InitField, @RequiredField or @OptionalField", - param); - } - fields.add(TypeStateBuilder.OptionalField.fromAnnotation(param.asType(), - param.getSimpleName().toString(), mirror)); - found = true; - } - } - if (!found) { - processingEnv.getMessager().printMessage(Kind.ERROR, - "Each parameter of a TypeStateBuilder constructor must have one of @InitField, @RequiredField or @OptionalField", - param); - } - } - - var specBuilder = - TypeSpec.classBuilder(builderName).addModifiers(Modifier.PUBLIC, Modifier.FINAL); - - TypeStateBuilder typeStateBuilder = new TypeStateBuilder(builderName, isLinear, - fields.toArray(TypeStateBuilder.Field[]::new), parent.asType()); - typeStateBuilder.apply(specBuilder); - - var spec = specBuilder.build(); - - JavaFile file = JavaFile.builder(builderPackage, spec).build(); - try { - file.writeTo(processingEnv.getFiler()); - } catch (IOException e) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, - "Failed to write class", constructorElement); - e.printStackTrace(); - } - }); + @Override + public Set getSupportedAnnotationTypes() { + return Arrays.stream(generators).map(x -> x.getAnnotationQualifiedName()) + .collect(Collectors.toSet()); } - private void processGenerateEmptyIO(TypeElement annotation, RoundEnvironment roundEnv) { - roundEnv.getElementsAnnotatedWith(annotation).forEach(classElement_ -> { - TypeElement classElement = (TypeElement) classElement_; - String emptyClassName = classElement.getSimpleName() + "Empty"; - String emptyPackage = getPackageName(classElement); - - List params = new ArrayList<>(); - - for (var mirror : classElement.getAnnotationMirrors()) { - if (!mirror.getAnnotationType().asElement().getSimpleName().toString() - .equals("GenerateEmptyIO")) { - continue; - } - for (var ev : mirror.getElementValues().entrySet()) { - if (ev.getKey().getSimpleName().toString().equals("value")) { - ev.getValue().accept(new ClassListVisitor(params), null); - } - } - } - - var specBuilder = TypeSpec.classBuilder(emptyClassName) - .addSuperinterface(TypeName.get(classElement.asType())) - .addModifiers(Modifier.PUBLIC, Modifier.FINAL); - - AtomicInteger i = new AtomicInteger(); - var constructor = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC) - .addParameters(params.stream().map(ty -> { - return ParameterSpec.builder(TypeName.get(ty), "arg" + i.incrementAndGet()) - .build(); - }).toList()).build(); - - for (var element : classElement.getEnclosedElements()) { - if (element instanceof ExecutableElement javaMethod) { - specBuilder - .addMethod(MethodSpec.methodBuilder(javaMethod.getSimpleName().toString()) - .addModifiers(Modifier.PUBLIC).addAnnotation(Override.class) - .returns(TypeName.VOID) - .addParameters(javaMethod.getParameters().stream().map(param -> { - return ParameterSpec.builder(TypeName.get(param.asType()), - param.getSimpleName().toString()).build(); - }).toList()).addCode("// Intentionally do nothing").build()); + /** Process all elements. */ + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + boolean handledAny = false; + for (TypeElement annotation : annotations) { + String name = annotation.getQualifiedName().toString(); + for (var gen : generators) { + if (gen.getAnnotationQualifiedName().equals(name)) { + gen.generate(annotation, roundEnv); + handledAny = true; } } - - specBuilder = specBuilder.addMethod(constructor); - - var spec = specBuilder.build(); - - JavaFile file = JavaFile.builder(emptyPackage, spec).build(); - try { - file.writeTo(processingEnv.getFiler()); - } catch (IOException e) { - processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, - "Failed to write class", classElement); - e.printStackTrace(); - } - }); - } - - private static String getPackageName(Element e) { - while (e != null) { - if (e.getKind().equals(ElementKind.PACKAGE)) { - return ((PackageElement) e).getQualifiedName().toString(); - } - e = e.getEnclosingElement(); } - - return null; + return handledAny; } } diff --git a/src/main/java/org/frc5572/robotools/StringVisitor.java b/src/main/java/org/frc5572/robotools/StringVisitor.java deleted file mode 100644 index 1b87b3c..0000000 --- a/src/main/java/org/frc5572/robotools/StringVisitor.java +++ /dev/null @@ -1,13 +0,0 @@ -package org.frc5572.robotools; - -import javax.lang.model.util.SimpleAnnotationValueVisitor8; - -/** Find all uses of a string in annotation values */ -public class StringVisitor extends SimpleAnnotationValueVisitor8 { - - @Override - public String visitString(String arg0, Void arg1) { - return arg0; - } - -} diff --git a/src/main/java/org/frc5572/robotools/TypeStateBuilder.java b/src/main/java/org/frc5572/robotools/TypeStateBuilder.java deleted file mode 100644 index 9a68f50..0000000 --- a/src/main/java/org/frc5572/robotools/TypeStateBuilder.java +++ /dev/null @@ -1,469 +0,0 @@ -package org.frc5572.robotools; - -import java.util.ArrayList; -import javax.lang.model.element.AnnotationMirror; -import javax.lang.model.element.Modifier; -import javax.lang.model.type.TypeMirror; -import com.squareup.javapoet.ClassName; -import com.squareup.javapoet.FieldSpec; -import com.squareup.javapoet.MethodSpec; -import com.squareup.javapoet.ParameterSpec; -import com.squareup.javapoet.TypeName; -import com.squareup.javapoet.TypeSpec; - -/** Template builder for TypeState Builders */ -public class TypeStateBuilder { - - private final String name; - private final ArrayList requiredFields = new ArrayList<>(); - private final ArrayList initFields = new ArrayList<>(); - private final ArrayList optionalFields = new ArrayList<>(); - private final Field[] fields; - private final TypeMirror result; - private final boolean isLinear; - - /** Template builder for TypeState Builders */ - public TypeStateBuilder(String name, boolean isLinear, Field[] fields_, TypeMirror result) { - this.fields = fields_; - this.result = result; - this.name = name; - this.isLinear = isLinear; - for (var field : fields_) { - if (field instanceof RequiredField rField) { - requiredFields.add(rField); - } else if (field instanceof OptionalField oField) { - optionalFields.add(oField); - } else if (field instanceof InitField iField) { - initFields.add(iField); - } - } - } - - private static boolean advance(boolean[] enabled, boolean isLinear) { - for (int i = 0; i < enabled.length; i++) { - if (enabled[i]) { - if (!isLinear) { - enabled[i] = false; - } - } else { - enabled[i] = true; - return true; - } - } - return false; - } - - /** Create typestate builders and write them to a typespec */ - public void apply(TypeSpec.Builder builder) { - boolean[] enabled = new boolean[requiredFields.size()]; - - MethodSpec.Builder constructor = MethodSpec.constructorBuilder(); - for (var field : initFields) { - builder.addField(FieldSpec.builder(TypeName.get(field.type), field.name + "_", - Modifier.PRIVATE, Modifier.FINAL).build()); - constructor.addParameter( - ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); - constructor.addCode("this." + field.name + "_ = " + field.name + "_;\n"); - } - for (var field : optionalFields) { - builder.addField(FieldSpec.builder(TypeName.get(field.type), field.name + "_", - Modifier.PRIVATE, Modifier.FINAL).build()); - constructor.addCode("this." + field.name + "_ = " + field.default_code + ";\n"); - } - builder.addMethod(constructor.addModifiers(Modifier.PUBLIC).build()); - constructor = MethodSpec.constructorBuilder(); - boolean isDifferent = false; - for (var field : initFields) { - constructor.addParameter( - ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); - constructor.addCode("this." + field.name + "_ = " + field.name + "_;\n"); - } - for (var field : optionalFields) { - constructor.addParameter( - ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); - constructor.addCode("this." + field.name + "_ = " + field.name + "_;\n"); - isDifferent = true; - } - if (isDifferent) { - builder.addMethod(constructor.addModifiers(Modifier.PRIVATE).build()); - } - addMethods(builder, enabled); - while (advance(enabled, isLinear)) { - constructor = MethodSpec.constructorBuilder(); - for (int i = 0; i < enabled.length; i++) { - if (enabled[i]) { - var field = requiredFields.get(i); - constructor.addParameter( - ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); - constructor.addCode("this." + field.name + "_ = " + field.name + "_;\n"); - } - } - TypeSpec.Builder stepClass = TypeSpec.classBuilder(getName(enabled)); - for (int i = 0; i < enabled.length; i++) { - if (enabled[i]) { - var field = requiredFields.get(i); - stepClass.addField(FieldSpec.builder(TypeName.get(field.type), field.name + "_", - Modifier.PRIVATE, Modifier.FINAL).build()); - } - } - for (var field : initFields) { - stepClass.addField(FieldSpec.builder(TypeName.get(field.type), field.name + "_", - Modifier.PRIVATE, Modifier.FINAL).build()); - constructor.addParameter( - ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); - constructor.addCode("this." + field.name + "_ = " + field.name + "_;\n"); - } - for (var field : optionalFields) { - stepClass.addField(FieldSpec.builder(TypeName.get(field.type), field.name + "_", - Modifier.PRIVATE, Modifier.FINAL).build()); - constructor.addParameter( - ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); - constructor.addCode("this." + field.name + "_ = " + field.name + "_;\n"); - } - stepClass.addMethod(constructor.addModifiers(Modifier.PRIVATE).build()); - addMethods(stepClass, enabled); - builder.addType(stepClass.addModifiers(Modifier.PUBLIC).build()); - } - } - - private String getName(boolean[] enabled) { - StringBuilder builderName = new StringBuilder(this.name); - boolean any = false; - for (int i = 0; i < enabled.length; i++) { - builderName.append(enabled[i] ? "1" : "0"); - any = any || enabled[i]; - } - if (any) { - return builderName.toString(); - } else { - return this.name; - } - } - - private void addMethods(TypeSpec.Builder builder, boolean[] enabled) { - boolean isFinishable = true; - for (int i = 0; i < enabled.length; i++) { - if (!enabled[i]) { - isFinishable = false; - break; - } - } - if (isFinishable) { - TypeName returnType = TypeName.get(result); - MethodSpec.Builder finish = MethodSpec.methodBuilder("finish") - .addModifiers(Modifier.PUBLIC).returns(returnType); - String code = "return new " + returnType + "("; - boolean isFirst = true; - for (Field field : fields) { - if (!isFirst) { - code += ", "; - } - code += field.name + "_"; - isFirst = false; - } - finish.addCode(code + ");\n"); - builder.addMethod(finish.build()); - } - for (int i = 0; i < enabled.length; i++) { - if (!enabled[i]) { - var field = requiredFields.get(i); - boolean[] next = new boolean[enabled.length]; - System.arraycopy(enabled, 0, next, 0, enabled.length); - next[i] = true; - String nextName = getName(next); - MethodSpec.Builder method = - MethodSpec.methodBuilder(field.name).returns(ClassName.get("", nextName)); - method.addParameter( - ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); - String code = "return new " + nextName + "("; - boolean isFirst = true; - for (int j = 0; j < enabled.length; j++) { - if (enabled[j] || i == j) { - if (!isFirst) { - code += ", "; - } - code += requiredFields.get(j).name + "_"; - isFirst = false; - } - } - for (var field_ : initFields) { - if (!isFirst) { - code += ", "; - } - code += field_.name + "_"; - isFirst = false; - } - for (var field_ : optionalFields) { - if (!isFirst) { - code += ", "; - } - code += field_.name + "_"; - isFirst = false; - } - method.addCode(code + ");\n"); - builder.addMethod(method.addModifiers(Modifier.PUBLIC).build()); - - if (field.alt != null) { - method = - MethodSpec.methodBuilder(field.name).returns(ClassName.get("", nextName)); - method.addParameter(ParameterSpec - .builder(TypeName.get(field.alt.type), field.alt.parameterName).build()); - code = "return new " + nextName + "("; - isFirst = true; - for (int j = 0; j < enabled.length; j++) { - if (i == j) { - if (!isFirst) { - code += ", "; - } - code += field.alt.code; - isFirst = false; - continue; - } - if (enabled[j]) { - if (!isFirst) { - code += ", "; - } - code += requiredFields.get(j).name + "_"; - isFirst = false; - } - } - for (var field_ : initFields) { - if (!isFirst) { - code += ", "; - } - code += field_.name + "_"; - isFirst = false; - } - for (var field_ : optionalFields) { - if (!isFirst) { - code += ", "; - } - code += field_.name + "_"; - isFirst = false; - } - method.addCode(code + ");\n"); - builder.addMethod(method.addModifiers(Modifier.PUBLIC).build()); - } - if (isLinear) { - break; - } - } - } - String thisName = getName(enabled); - for (var field : optionalFields) { - MethodSpec.Builder method = - MethodSpec.methodBuilder(field.name).returns(ClassName.get("", thisName)); - method.addParameter( - ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); - String code = "return new " + thisName + "("; - boolean isFirst = true; - for (int i = 0; i < enabled.length; i++) { - if (enabled[i]) { - if (!isFirst) { - code += ", "; - } - code += requiredFields.get(i).name + "_"; - isFirst = false; - } - } - for (var field_ : initFields) { - if (!isFirst) { - code += ", "; - } - code += field_.name + "_"; - isFirst = false; - } - for (var field_ : optionalFields) { - if (!isFirst) { - code += ", "; - } - code += field_.name + "_"; - isFirst = false; - } - method.addCode(code + ");\n"); - builder.addMethod(method.addModifiers(Modifier.PUBLIC).build()); - - if (field.alt != null) { - method = MethodSpec.methodBuilder(field.name).returns(ClassName.get("", thisName)); - method.addParameter(ParameterSpec - .builder(TypeName.get(field.alt.type), field.alt.parameterName).build()); - code = "return new " + thisName + "("; - isFirst = true; - for (int i = 0; i < enabled.length; i++) { - if (enabled[i]) { - if (!isFirst) { - code += ", "; - } - code += requiredFields.get(i).name + "_"; - isFirst = false; - } - } - for (var field_ : initFields) { - if (!isFirst) { - code += ", "; - } - code += field_.name + "_"; - isFirst = false; - } - for (var field_ : optionalFields) { - if (!isFirst) { - code += ", "; - } - if (field_ == field) { - code += field.alt.code; - } else { - code += field_.name + "_"; - } - isFirst = false; - } - method.addCode(code + ");\n"); - builder.addMethod(method.addModifiers(Modifier.PUBLIC).build()); - } - } - } - - /** Base class for fields */ - public static class Field { - /** Field type */ - public final TypeMirror type; - /** Field name */ - public final String name; - - /** Base class for fields */ - public Field(TypeMirror type, String name) { - this.type = type; - this.name = name; - } - } - - /** Field that must be provided when creating a builder */ - public static class InitField extends Field { - /** Field that must be provided when creating a builder */ - public InitField(TypeMirror type, String name) { - super(type, name); - } - } - - /** A non-init field */ - public static class MethodField extends Field { - /** An alternative method for fulfilling this field. */ - public final AltMethod alt; - - /** A non-init field */ - public MethodField(TypeMirror type, String name, AltMethod alt) { - super(type, name); - this.alt = alt; - } - - /** A non-init field */ - public MethodField(TypeMirror type, String name) { - this(type, name, null); - } - } - - /** A field that is required to finish the builder. */ - public static class RequiredField extends MethodField { - - /** A field that is required to finish the builder. */ - public RequiredField(TypeMirror type, String name, AltMethod alt) { - super(type, name, alt); - } - - /** A field that is required to finish the builder. */ - public RequiredField(TypeMirror type, String name) { - super(type, name); - } - - /** A field that is required to finish the builder. */ - public static RequiredField fromAnnotation(TypeMirror type, String name, - AnnotationMirror mirror) { - AltMethod alt = null; - for (var ev : mirror.getElementValues().entrySet()) { - if (ev.getKey().getSimpleName().toString().equals("alt")) { - AnnotationMirror altMirror = - ev.getValue().accept(new AnnotationMirrorVisitor(), null); - alt = AltMethod.fromAnnotation(altMirror, name); - } - } - return new RequiredField(type, name, alt); - } - } - - /** A field that has a default in case it is not specified. */ - public static class OptionalField extends MethodField { - /** Java expression that provides the default value. */ - public final String default_code; - - /** A field that has a default in case it is not specified. */ - public OptionalField(TypeMirror type, String name, AltMethod alt, String default_code) { - super(type, name, alt); - this.default_code = default_code; - } - - /** A field that has a default in case it is not specified. */ - public OptionalField(TypeMirror type, String name, String default_code) { - super(type, name); - this.default_code = default_code; - } - - /** A field that has a default in case it is not specified. */ - public static OptionalField fromAnnotation(TypeMirror type, String name, - AnnotationMirror mirror) { - String defaultCode = ""; - AltMethod alt = null; - for (var ev : mirror.getElementValues().entrySet()) { - if (ev.getKey().getSimpleName().toString().equals("value")) { - defaultCode = ev.getValue().accept(new StringVisitor(), null); - } else if (ev.getKey().getSimpleName().toString().equals("alt")) { - AnnotationMirror altMirror = - ev.getValue().accept(new AnnotationMirrorVisitor(), null); - alt = AltMethod.fromAnnotation(altMirror, name); - } - } - return new OptionalField(type, name, alt, defaultCode); - } - } - - /** Alternative method for a field. */ - public static record AltMethod(TypeMirror type, String parameterName, String code) { - /** Alternative method for a field. */ - public static AltMethod fromAnnotation(AnnotationMirror mirror, String defaultName) { - if (mirror == null) { - System.out.println("annotation is null"); - return null; - } - - if (!mirror.getAnnotationType().asElement().getSimpleName().toString() - .equals("AltMethod")) { - System.out.println("annotation name doesn't match " - + mirror.getAnnotationType().asElement().getSimpleName().toString()); - return null; - } - TypeMirror type = null; - String parameterName = defaultName; - String code = null; - - for (var ev : mirror.getElementValues().entrySet()) { - if (ev.getKey().getSimpleName().toString().equals("type")) { - type = ev.getValue().accept(new ClassVisitor(), null); - } else if (ev.getKey().getSimpleName().toString().equals("parameter_name")) { - parameterName = ev.getValue().accept(new StringVisitor(), null); - } else if (ev.getKey().getSimpleName().toString().equals("value")) { - code = ev.getValue().accept(new StringVisitor(), null); - } - } - - if (type == null) { - System.out.println("Missing type"); - return null; - } - if (code == null) { - System.out.println("Missing code"); - return null; - } - - return new AltMethod(type, parameterName, code); - } - } - -} diff --git a/src/main/java/org/frc5572/robotools/Utilities.java b/src/main/java/org/frc5572/robotools/Utilities.java new file mode 100644 index 0000000..3181b58 --- /dev/null +++ b/src/main/java/org/frc5572/robotools/Utilities.java @@ -0,0 +1,209 @@ +package org.frc5572.robotools; + +import java.util.ArrayList; +import java.util.List; +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.PackageElement; +import javax.lang.model.type.TypeMirror; +import javax.lang.model.util.SimpleAnnotationValueVisitor8; + +/** + * Utility methods for annotation processors or other compile-time tools. + * + *

+ * All methods are static; this class is not intended to be instantiated. + */ +public class Utilities { + + private Utilities() {} + + /** + * Returns the fully qualified package name that contains the given element. + * + *

+ * The method walks up the enclosing-element chain until a {@link ElementKind#PACKAGE} is found. + * If no package element is found (which is unlikely in normal use), {@code null} is returned. + * + * @param e the element whose package name should be determined; may be {@code null} + * @return the fully qualified package name of the element, or {@code null} if no package + * element can be found + */ + public static String getPackageName(Element e) { + while (e != null) { + if (e.getKind().equals(ElementKind.PACKAGE)) { + return ((PackageElement) e).getQualifiedName().toString(); + } + e = e.getEnclosingElement(); + } + + return null; + } + + /** + * Extracts a {@link String} value from an {@link AnnotationValue}. + * + *

+ * This is typically used for annotation members declared as {@code String}. + * + * @param value the annotation value to read + * @return the contained string value, or {@code null} if the value is not a string + */ + private static class StringVisitor extends SimpleAnnotationValueVisitor8 { + + @Override + public String visitString(String arg0, Void arg1) { + return arg0; + } + + } + + public static String stringAnnotationValue(AnnotationValue value) { + return value.accept(new StringVisitor(), null); + } + + private static class ClassVisitor extends SimpleAnnotationValueVisitor8 { + + @Override + public TypeMirror visitType(TypeMirror arg0, Void arg1) { + return arg0; + } + + } + + /** + * Extracts a single {@link TypeMirror} (class) value from an {@link AnnotationValue}. + * + *

+ * This is typically used for annotation members declared as a single {@code Class} type. + * + * @param value the annotation value to read + * @return the contained {@link TypeMirror}, or {@code null} if the value is not a single type + */ + public static TypeMirror classAnnotationValue(AnnotationValue value) { + return value.accept(new ClassVisitor(), null); + } + + private static class ClassListVisitor extends SimpleAnnotationValueVisitor8 { + + private final List mirrors; + + /** Find all uses of a type in annotation values */ + public ClassListVisitor(List mirrors) { + super(); + this.mirrors = mirrors; + } + + @Override + public Void visitType(TypeMirror arg0, Void arg1) { + mirrors.add(arg0); + return null; + } + + @Override + public Void visitArray(List arg0, Void arg1) { + for (var item : arg0) { + this.visit(item); + } + return null; + } + + } + + /** + * Extracts all {@link TypeMirror} (class) values from the given {@link AnnotationValue} into an + * existing list. + * + *

+ * This method supports both single class values and arrays of classes from annotation members + * declared as {@code Class} or {@code Class[]}. + * + * @param value the annotation value to read + * @param out the list into which all discovered {@link TypeMirror}s are added; must not be + * {@code null} + */ + public static void classListAnnotationValue(AnnotationValue value, List out) { + value.accept(new ClassListVisitor(out), null); + } + + /** + * Extracts all {@link TypeMirror} (class) values from the given {@link AnnotationValue}. + * + *

+ * This is a convenience overload of {@link #classListAnnotationValue(AnnotationValue, List)} + * that creates and returns a new list. + * + * @param value the annotation value to read + * @return a list of all {@link TypeMirror}s contained in the annotation value; never + * {@code null}, but may be empty + */ + public static List classListAnnotationValue(AnnotationValue value) { + List out = new ArrayList<>(); + classListAnnotationValue(value, out); + return out; + } + + /** + * Extracts a {@code boolean} value from an {@link AnnotationValue}. + * + *

+ * This is typically used for annotation members declared as {@code boolean}. + * + * @param value the annotation value to read + * @return the contained boolean value, or {@code null} if the value is not a boolean + */ + private static class BoolVisitor extends SimpleAnnotationValueVisitor8 { + + @Override + public Boolean visitBoolean(boolean arg0, Void arg1) { + return arg0; + } + + } + + public static Boolean boolAnnotationValue(AnnotationValue value) { + return value.accept(new BoolVisitor(), null); + } + + private static class AnnotationMirrorVisitor + extends SimpleAnnotationValueVisitor8 { + + @Override + public AnnotationMirror visitAnnotation(AnnotationMirror arg0, Void arg1) { + return arg0; + } + + @Override + public AnnotationMirror visitArray(List arg0, Void arg1) { + for (var item : arg0) { + var res = this.visit(item, arg1); + if (res != null) { + return res; + } + } + return null; + } + + } + + /** + * Extracts a nested {@link AnnotationMirror} from an {@link AnnotationValue}. + * + *

+ * This is typically used for annotation members declared as another annotation type, possibly + * in an array. + * + *

+ * If the value is an array, the first nested annotation found is returned. + * + * @param value the annotation value to read + * @return the contained {@link AnnotationMirror}, or {@code null} if the value does not contain + * an annotation + */ + public static AnnotationMirror annotationAnnotationValue(AnnotationValue value) { + return value.accept(new AnnotationMirrorVisitor(), null); + } + +} diff --git a/src/main/java/org/frc5572/robotools/emptyio/GenerateEmptyIOGenerator.java b/src/main/java/org/frc5572/robotools/emptyio/GenerateEmptyIOGenerator.java new file mode 100644 index 0000000..28d1061 --- /dev/null +++ b/src/main/java/org/frc5572/robotools/emptyio/GenerateEmptyIOGenerator.java @@ -0,0 +1,100 @@ +package org.frc5572.robotools.emptyio; + + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.type.TypeMirror; +import javax.tools.Diagnostic; +import org.frc5572.robotools.AnnotationGenerator; +import org.frc5572.robotools.Utilities; +import com.squareup.javapoet.JavaFile; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterSpec; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; + +/** + * Annotation Generator for {@code @GenerateEmptyIO} + */ +public class GenerateEmptyIOGenerator implements AnnotationGenerator { + + private ProcessingEnvironment processingEnv; + + @Override + public void init(ProcessingEnvironment env) { + this.processingEnv = env; + } + + @Override + public String getAnnotationQualifiedName() { + return "frc.robot.util.GenerateEmptyIO"; + } + + @Override + public void generate(TypeElement annotation, RoundEnvironment roundEnv) { + roundEnv.getElementsAnnotatedWith(annotation).forEach(classElement_ -> { + TypeElement classElement = (TypeElement) classElement_; + String emptyClassName = classElement.getSimpleName() + "Empty"; + String emptyPackage = Utilities.getPackageName(classElement); + + List params = new ArrayList<>(); + + for (var mirror : classElement.getAnnotationMirrors()) { + if (!mirror.getAnnotationType().asElement().getSimpleName().toString() + .equals("GenerateEmptyIO")) { + continue; + } + for (var ev : mirror.getElementValues().entrySet()) { + if (ev.getKey().getSimpleName().toString().equals("value")) { + Utilities.classListAnnotationValue(ev.getValue(), params); + } + } + } + + var specBuilder = TypeSpec.classBuilder(emptyClassName) + .addSuperinterface(TypeName.get(classElement.asType())) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL); + + AtomicInteger i = new AtomicInteger(); + var constructor = MethodSpec.constructorBuilder().addModifiers(Modifier.PUBLIC) + .addParameters(params.stream().map(ty -> { + return ParameterSpec.builder(TypeName.get(ty), "arg" + i.incrementAndGet()) + .build(); + }).toList()).build(); + + for (var element : classElement.getEnclosedElements()) { + if (element instanceof ExecutableElement javaMethod) { + specBuilder + .addMethod(MethodSpec.methodBuilder(javaMethod.getSimpleName().toString()) + .addModifiers(Modifier.PUBLIC).addAnnotation(Override.class) + .returns(TypeName.VOID) + .addParameters(javaMethod.getParameters().stream().map(param -> { + return ParameterSpec.builder(TypeName.get(param.asType()), + param.getSimpleName().toString()).build(); + }).toList()).addCode("// Intentionally do nothing").build()); + } + } + + specBuilder = specBuilder.addMethod(constructor); + + var spec = specBuilder.build(); + + JavaFile file = JavaFile.builder(emptyPackage, spec).build(); + try { + file.writeTo(processingEnv.getFiler()); + } catch (IOException e) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, + "Failed to write class", classElement); + e.printStackTrace(); + } + }); + } + +} diff --git a/src/main/java/org/frc5572/robotools/typestate/TypeStateBuilderGenerator.java b/src/main/java/org/frc5572/robotools/typestate/TypeStateBuilderGenerator.java new file mode 100644 index 0000000..43cc30b --- /dev/null +++ b/src/main/java/org/frc5572/robotools/typestate/TypeStateBuilderGenerator.java @@ -0,0 +1,608 @@ +package org.frc5572.robotools.typestate; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.element.AnnotationMirror; +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; +import javax.tools.Diagnostic; +import javax.tools.Diagnostic.Kind; +import org.frc5572.robotools.AnnotationGenerator; +import org.frc5572.robotools.Utilities; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.JavaFile; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterSpec; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; + +/** + * Annotation Generator for {@code @TypeStateBuilder} + */ +public final class TypeStateBuilderGenerator implements AnnotationGenerator { + + private ProcessingEnvironment processingEnv; + + @Override + public void init(ProcessingEnvironment env) { + this.processingEnv = env; + } + + @Override + public String getAnnotationQualifiedName() { + return "frc.robot.util.typestate.TypeStateBuilder"; + } + + @Override + public void generate(TypeElement annotation, RoundEnvironment roundEnv) { + roundEnv.getElementsAnnotatedWith(annotation).forEach(constructorElement_ -> { + ExecutableElement constructorElement = (ExecutableElement) constructorElement_; + Element parent_ = constructorElement.getEnclosingElement(); + if (!(parent_ instanceof TypeElement)) { + processingEnv.getMessager().printMessage(Kind.ERROR, + "TypeStateBuilder constructor must be the direct child of a TypeElement (e.g. class). Instead found " + + parent_.getKind().toString() + ".", + constructorElement); + } + TypeElement parent = (TypeElement) parent_; + String builderName = parent.getSimpleName() + "Builder"; + String builderPackage = Utilities.getPackageName(parent); + boolean isLinear = false; + // System.out.println("Processing " + builderPackage + "." + builderName); + for (var mirror : constructorElement.getAnnotationMirrors()) { + if (!mirror.getAnnotationType().asElement().getSimpleName().toString() + .equals("TypeStateBuilder")) { + continue; + } + for (var ev : mirror.getElementValues().entrySet()) { + if (ev.getKey().getSimpleName().toString().equals("value")) { + String res = Utilities.stringAnnotationValue(ev.getValue()); + if (res != null) { + builderName = res; + } + } else if (ev.getKey().getSimpleName().toString().equals("linear")) { + Boolean res = Utilities.boolAnnotationValue(ev.getValue()); + if (res != null) { + isLinear = res; + } + } + } + } + + List fields = new ArrayList<>(); + List params = constructorElement.getParameters(); + for (int i = 0; i < params.size(); i++) { + boolean found = false; + VariableElement param = params.get(i); + for (var mirror : param.getAnnotationMirrors()) { + if (mirror.getAnnotationType().asElement().getSimpleName().toString() + .equals("InitField")) { + if (found) { + processingEnv.getMessager().printMessage(Kind.ERROR, + "Each parameter of a TypeStateBuilder constructor can only " + + "have one of @InitField, @RequiredField or @OptionalField", + param); + } + fields.add(new TypeStateBuilder.InitField(param.asType(), + param.getSimpleName().toString())); + found = true; + } else if (mirror.getAnnotationType().asElement().getSimpleName().toString() + .equals("RequiredField")) { + if (found) { + processingEnv.getMessager().printMessage(Kind.ERROR, + "Each parameter of a TypeStateBuilder constructor can only " + + "have one of @InitField, @RequiredField or @OptionalField", + param); + } + fields.add(TypeStateBuilder.RequiredField.fromAnnotation(param.asType(), + param.getSimpleName().toString(), mirror)); + found = true; + } else if (mirror.getAnnotationType().asElement().getSimpleName().toString() + .equals("OptionalField")) { + if (found) { + processingEnv.getMessager().printMessage(Kind.ERROR, + "Each parameter of a TypeStateBuilder constructor can only " + + "have one of @InitField, @RequiredField or @OptionalField", + param); + } + fields.add(TypeStateBuilder.OptionalField.fromAnnotation(param.asType(), + param.getSimpleName().toString(), mirror)); + found = true; + } + } + if (!found) { + processingEnv.getMessager() + .printMessage(Kind.ERROR, + "Each parameter of a TypeStateBuilder constructor must " + + "have one of @InitField, @RequiredField or @OptionalField", + param); + } + } + + var specBuilder = + TypeSpec.classBuilder(builderName).addModifiers(Modifier.PUBLIC, Modifier.FINAL); + + TypeStateBuilder typeStateBuilder = new TypeStateBuilder(builderName, isLinear, + fields.toArray(TypeStateBuilder.Field[]::new), parent.asType()); + typeStateBuilder.apply(specBuilder); + + var spec = specBuilder.build(); + + JavaFile file = JavaFile.builder(builderPackage, spec).build(); + try { + file.writeTo(processingEnv.getFiler()); + } catch (IOException e) { + processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR, + "Failed to write class", constructorElement); + e.printStackTrace(); + } + }); + } + + private static class TypeStateBuilder { + + private final String name; + private final ArrayList requiredFields = new ArrayList<>(); + private final ArrayList initFields = new ArrayList<>(); + private final ArrayList optionalFields = new ArrayList<>(); + private final Field[] fields; + private final TypeMirror result; + private final boolean isLinear; + + /** Template builder for TypeState Builders */ + public TypeStateBuilder(String name, boolean isLinear, Field[] fields_, TypeMirror result) { + this.fields = fields_; + this.result = result; + this.name = name; + this.isLinear = isLinear; + for (var field : fields_) { + if (field instanceof RequiredField rField) { + requiredFields.add(rField); + } else if (field instanceof OptionalField oField) { + optionalFields.add(oField); + } else if (field instanceof InitField iField) { + initFields.add(iField); + } + } + } + + private static boolean advance(boolean[] enabled, boolean isLinear) { + for (int i = 0; i < enabled.length; i++) { + if (enabled[i]) { + if (!isLinear) { + enabled[i] = false; + } + } else { + enabled[i] = true; + return true; + } + } + return false; + } + + /** Create typestate builders and write them to a typespec */ + public void apply(TypeSpec.Builder builder) { + boolean[] enabled = new boolean[requiredFields.size()]; + + MethodSpec.Builder constructor = MethodSpec.constructorBuilder(); + for (var field : initFields) { + builder.addField(FieldSpec.builder(TypeName.get(field.type), field.name + "_", + Modifier.PRIVATE, Modifier.FINAL).build()); + constructor.addParameter( + ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); + constructor.addCode("this." + field.name + "_ = " + field.name + "_;\n"); + } + for (var field : optionalFields) { + builder.addField(FieldSpec.builder(TypeName.get(field.type), field.name + "_", + Modifier.PRIVATE, Modifier.FINAL).build()); + constructor.addCode("this." + field.name + "_ = " + field.default_code + ";\n"); + } + builder.addMethod(constructor.addModifiers(Modifier.PUBLIC).build()); + constructor = MethodSpec.constructorBuilder(); + boolean isDifferent = false; + for (var field : initFields) { + constructor.addParameter( + ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); + constructor.addCode("this." + field.name + "_ = " + field.name + "_;\n"); + } + for (var field : optionalFields) { + constructor.addParameter( + ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); + constructor.addCode("this." + field.name + "_ = " + field.name + "_;\n"); + isDifferent = true; + } + if (isDifferent) { + builder.addMethod(constructor.addModifiers(Modifier.PRIVATE).build()); + } + addMethods(builder, enabled); + while (advance(enabled, isLinear)) { + constructor = MethodSpec.constructorBuilder(); + for (int i = 0; i < enabled.length; i++) { + if (enabled[i]) { + var field = requiredFields.get(i); + constructor.addParameter(ParameterSpec + .builder(TypeName.get(field.type), field.name + "_").build()); + constructor.addCode("this." + field.name + "_ = " + field.name + "_;\n"); + } + } + TypeSpec.Builder stepClass = TypeSpec.classBuilder(getName(enabled)); + for (int i = 0; i < enabled.length; i++) { + if (enabled[i]) { + var field = requiredFields.get(i); + stepClass.addField(FieldSpec.builder(TypeName.get(field.type), + field.name + "_", Modifier.PRIVATE, Modifier.FINAL).build()); + } + } + for (var field : initFields) { + stepClass.addField(FieldSpec.builder(TypeName.get(field.type), field.name + "_", + Modifier.PRIVATE, Modifier.FINAL).build()); + constructor.addParameter( + ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); + constructor.addCode("this." + field.name + "_ = " + field.name + "_;\n"); + } + for (var field : optionalFields) { + stepClass.addField(FieldSpec.builder(TypeName.get(field.type), field.name + "_", + Modifier.PRIVATE, Modifier.FINAL).build()); + constructor.addParameter( + ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); + constructor.addCode("this." + field.name + "_ = " + field.name + "_;\n"); + } + stepClass.addMethod(constructor.addModifiers(Modifier.PRIVATE).build()); + addMethods(stepClass, enabled); + builder.addType(stepClass.addModifiers(Modifier.PUBLIC).build()); + } + } + + private String getName(boolean[] enabled) { + StringBuilder builderName = new StringBuilder(this.name); + boolean any = false; + for (int i = 0; i < enabled.length; i++) { + builderName.append(enabled[i] ? "1" : "0"); + any = any || enabled[i]; + } + if (any) { + return builderName.toString(); + } else { + return this.name; + } + } + + private void addMethods(TypeSpec.Builder builder, boolean[] enabled) { + boolean isFinishable = true; + for (int i = 0; i < enabled.length; i++) { + if (!enabled[i]) { + isFinishable = false; + break; + } + } + if (isFinishable) { + TypeName returnType = TypeName.get(result); + MethodSpec.Builder finish = MethodSpec.methodBuilder("finish") + .addModifiers(Modifier.PUBLIC).returns(returnType); + String code = "return new " + returnType + "("; + boolean isFirst = true; + for (Field field : fields) { + if (!isFirst) { + code += ", "; + } + code += field.name + "_"; + isFirst = false; + } + finish.addCode(code + ");\n"); + builder.addMethod(finish.build()); + } + for (int i = 0; i < enabled.length; i++) { + if (!enabled[i]) { + var field = requiredFields.get(i); + boolean[] next = new boolean[enabled.length]; + System.arraycopy(enabled, 0, next, 0, enabled.length); + next[i] = true; + String nextName = getName(next); + MethodSpec.Builder method = + MethodSpec.methodBuilder(field.name).returns(ClassName.get("", nextName)); + method.addParameter( + ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); + String code = "return new " + nextName + "("; + boolean isFirst = true; + for (int j = 0; j < enabled.length; j++) { + if (enabled[j] || i == j) { + if (!isFirst) { + code += ", "; + } + code += requiredFields.get(j).name + "_"; + isFirst = false; + } + } + for (var field_ : initFields) { + if (!isFirst) { + code += ", "; + } + code += field_.name + "_"; + isFirst = false; + } + for (var field_ : optionalFields) { + if (!isFirst) { + code += ", "; + } + code += field_.name + "_"; + isFirst = false; + } + method.addCode(code + ");\n"); + builder.addMethod(method.addModifiers(Modifier.PUBLIC).build()); + + if (field.alt != null) { + method = MethodSpec.methodBuilder(field.name) + .returns(ClassName.get("", nextName)); + method.addParameter(ParameterSpec + .builder(TypeName.get(field.alt.type), field.alt.parameterName) + .build()); + code = "return new " + nextName + "("; + isFirst = true; + for (int j = 0; j < enabled.length; j++) { + if (i == j) { + if (!isFirst) { + code += ", "; + } + code += field.alt.code; + isFirst = false; + continue; + } + if (enabled[j]) { + if (!isFirst) { + code += ", "; + } + code += requiredFields.get(j).name + "_"; + isFirst = false; + } + } + for (var field_ : initFields) { + if (!isFirst) { + code += ", "; + } + code += field_.name + "_"; + isFirst = false; + } + for (var field_ : optionalFields) { + if (!isFirst) { + code += ", "; + } + code += field_.name + "_"; + isFirst = false; + } + method.addCode(code + ");\n"); + builder.addMethod(method.addModifiers(Modifier.PUBLIC).build()); + } + if (isLinear) { + break; + } + } + } + String thisName = getName(enabled); + for (var field : optionalFields) { + MethodSpec.Builder method = + MethodSpec.methodBuilder(field.name).returns(ClassName.get("", thisName)); + method.addParameter( + ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); + String code = "return new " + thisName + "("; + boolean isFirst = true; + for (int i = 0; i < enabled.length; i++) { + if (enabled[i]) { + if (!isFirst) { + code += ", "; + } + code += requiredFields.get(i).name + "_"; + isFirst = false; + } + } + for (var field_ : initFields) { + if (!isFirst) { + code += ", "; + } + code += field_.name + "_"; + isFirst = false; + } + for (var field_ : optionalFields) { + if (!isFirst) { + code += ", "; + } + code += field_.name + "_"; + isFirst = false; + } + method.addCode(code + ");\n"); + builder.addMethod(method.addModifiers(Modifier.PUBLIC).build()); + + if (field.alt != null) { + method = + MethodSpec.methodBuilder(field.name).returns(ClassName.get("", thisName)); + method.addParameter(ParameterSpec + .builder(TypeName.get(field.alt.type), field.alt.parameterName).build()); + code = "return new " + thisName + "("; + isFirst = true; + for (int i = 0; i < enabled.length; i++) { + if (enabled[i]) { + if (!isFirst) { + code += ", "; + } + code += requiredFields.get(i).name + "_"; + isFirst = false; + } + } + for (var field_ : initFields) { + if (!isFirst) { + code += ", "; + } + code += field_.name + "_"; + isFirst = false; + } + for (var field_ : optionalFields) { + if (!isFirst) { + code += ", "; + } + if (field_ == field) { + code += field.alt.code; + } else { + code += field_.name + "_"; + } + isFirst = false; + } + method.addCode(code + ");\n"); + builder.addMethod(method.addModifiers(Modifier.PUBLIC).build()); + } + } + } + + /** Base class for fields */ + public static class Field { + /** Field type */ + public final TypeMirror type; + /** Field name */ + public final String name; + + /** Base class for fields */ + public Field(TypeMirror type, String name) { + this.type = type; + this.name = name; + } + } + + /** Field that must be provided when creating a builder */ + public static class InitField extends Field { + /** Field that must be provided when creating a builder */ + public InitField(TypeMirror type, String name) { + super(type, name); + } + } + + /** A non-init field */ + public static class MethodField extends Field { + /** An alternative method for fulfilling this field. */ + public final AltMethod alt; + + /** A non-init field */ + public MethodField(TypeMirror type, String name, AltMethod alt) { + super(type, name); + this.alt = alt; + } + + /** A non-init field */ + public MethodField(TypeMirror type, String name) { + this(type, name, null); + } + } + + /** A field that is required to finish the builder. */ + public static class RequiredField extends MethodField { + + /** A field that is required to finish the builder. */ + public RequiredField(TypeMirror type, String name, AltMethod alt) { + super(type, name, alt); + } + + /** A field that is required to finish the builder. */ + public RequiredField(TypeMirror type, String name) { + super(type, name); + } + + /** A field that is required to finish the builder. */ + public static RequiredField fromAnnotation(TypeMirror type, String name, + AnnotationMirror mirror) { + AltMethod alt = null; + for (var ev : mirror.getElementValues().entrySet()) { + if (ev.getKey().getSimpleName().toString().equals("alt")) { + AnnotationMirror altMirror = + Utilities.annotationAnnotationValue(ev.getValue()); + alt = AltMethod.fromAnnotation(altMirror, name); + } + } + return new RequiredField(type, name, alt); + } + } + + /** A field that has a default in case it is not specified. */ + public static class OptionalField extends MethodField { + /** Java expression that provides the default value. */ + public final String default_code; + + /** A field that has a default in case it is not specified. */ + public OptionalField(TypeMirror type, String name, AltMethod alt, String default_code) { + super(type, name, alt); + this.default_code = default_code; + } + + /** A field that has a default in case it is not specified. */ + public OptionalField(TypeMirror type, String name, String default_code) { + super(type, name); + this.default_code = default_code; + } + + /** A field that has a default in case it is not specified. */ + public static OptionalField fromAnnotation(TypeMirror type, String name, + AnnotationMirror mirror) { + String defaultCode = ""; + AltMethod alt = null; + for (var ev : mirror.getElementValues().entrySet()) { + if (ev.getKey().getSimpleName().toString().equals("value")) { + defaultCode = Utilities.stringAnnotationValue(ev.getValue()); + } else if (ev.getKey().getSimpleName().toString().equals("alt")) { + AnnotationMirror altMirror = + Utilities.annotationAnnotationValue(ev.getValue()); + alt = AltMethod.fromAnnotation(altMirror, name); + } + } + return new OptionalField(type, name, alt, defaultCode); + } + } + + /** Alternative method for a field. */ + public static record AltMethod(TypeMirror type, String parameterName, String code) { + /** Alternative method for a field. */ + public static AltMethod fromAnnotation(AnnotationMirror mirror, String defaultName) { + if (mirror == null) { + System.out.println("annotation is null"); + return null; + } + + if (!mirror.getAnnotationType().asElement().getSimpleName().toString() + .equals("AltMethod")) { + System.out.println("annotation name doesn't match " + + mirror.getAnnotationType().asElement().getSimpleName().toString()); + return null; + } + TypeMirror type = null; + String parameterName = defaultName; + String code = null; + + for (var ev : mirror.getElementValues().entrySet()) { + if (ev.getKey().getSimpleName().toString().equals("type")) { + type = Utilities.classAnnotationValue(ev.getValue()); + } else if (ev.getKey().getSimpleName().toString().equals("parameter_name")) { + parameterName = Utilities.stringAnnotationValue(ev.getValue()); + } else if (ev.getKey().getSimpleName().toString().equals("value")) { + code = Utilities.stringAnnotationValue(ev.getValue()); + } + } + + if (type == null) { + System.out.println("Missing type"); + return null; + } + if (code == null) { + System.out.println("Missing code"); + return null; + } + + return new AltMethod(type, parameterName, code); + } + } + + } +} + From 53b732df16909a120a8892b86b6e4351804740d0 Mon Sep 17 00:00:00 2001 From: wilsonwatson Date: Thu, 21 May 2026 20:21:57 -0500 Subject: [PATCH 2/7] split out files --- .../robotools/typestate/AltMethod.java | 47 ++ .../frc5572/robotools/typestate/Field.java | 17 + .../robotools/typestate/InitField.java | 11 + .../robotools/typestate/MethodField.java | 20 + .../robotools/typestate/OptionalField.java | 39 ++ .../robotools/typestate/RequiredField.java | 32 ++ .../robotools/typestate/TypeStateBuilder.java | 324 ++++++++++++ .../typestate/TypeStateBuilderGenerator.java | 476 +----------------- 8 files changed, 495 insertions(+), 471 deletions(-) create mode 100644 src/main/java/org/frc5572/robotools/typestate/AltMethod.java create mode 100644 src/main/java/org/frc5572/robotools/typestate/Field.java create mode 100644 src/main/java/org/frc5572/robotools/typestate/InitField.java create mode 100644 src/main/java/org/frc5572/robotools/typestate/MethodField.java create mode 100644 src/main/java/org/frc5572/robotools/typestate/OptionalField.java create mode 100644 src/main/java/org/frc5572/robotools/typestate/RequiredField.java create mode 100644 src/main/java/org/frc5572/robotools/typestate/TypeStateBuilder.java diff --git a/src/main/java/org/frc5572/robotools/typestate/AltMethod.java b/src/main/java/org/frc5572/robotools/typestate/AltMethod.java new file mode 100644 index 0000000..dd26760 --- /dev/null +++ b/src/main/java/org/frc5572/robotools/typestate/AltMethod.java @@ -0,0 +1,47 @@ +package org.frc5572.robotools.typestate; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.type.TypeMirror; +import org.frc5572.robotools.Utilities; + +/** Alternative method for a field. */ +public record AltMethod(TypeMirror type, String parameterName, String code) { + /** Alternative method for a field. */ + public static AltMethod fromAnnotation(AnnotationMirror mirror, String defaultName) { + if (mirror == null) { + System.out.println("annotation is null"); + return null; + } + + if (!mirror.getAnnotationType().asElement().getSimpleName().toString() + .equals("AltMethod")) { + System.out.println("annotation name doesn't match " + + mirror.getAnnotationType().asElement().getSimpleName().toString()); + return null; + } + TypeMirror type = null; + String parameterName = defaultName; + String code = null; + + for (var ev : mirror.getElementValues().entrySet()) { + if (ev.getKey().getSimpleName().toString().equals("type")) { + type = Utilities.classAnnotationValue(ev.getValue()); + } else if (ev.getKey().getSimpleName().toString().equals("parameter_name")) { + parameterName = Utilities.stringAnnotationValue(ev.getValue()); + } else if (ev.getKey().getSimpleName().toString().equals("value")) { + code = Utilities.stringAnnotationValue(ev.getValue()); + } + } + + if (type == null) { + System.out.println("Missing type"); + return null; + } + if (code == null) { + System.out.println("Missing code"); + return null; + } + + return new AltMethod(type, parameterName, code); + } +} diff --git a/src/main/java/org/frc5572/robotools/typestate/Field.java b/src/main/java/org/frc5572/robotools/typestate/Field.java new file mode 100644 index 0000000..31094fe --- /dev/null +++ b/src/main/java/org/frc5572/robotools/typestate/Field.java @@ -0,0 +1,17 @@ +package org.frc5572.robotools.typestate; + +import javax.lang.model.type.TypeMirror; + +/** Base class for fields */ +public class Field { + /** Field type */ + public final TypeMirror type; + /** Field name */ + public final String name; + + /** Base class for fields */ + public Field(TypeMirror type, String name) { + this.type = type; + this.name = name; + } +} diff --git a/src/main/java/org/frc5572/robotools/typestate/InitField.java b/src/main/java/org/frc5572/robotools/typestate/InitField.java new file mode 100644 index 0000000..d77804d --- /dev/null +++ b/src/main/java/org/frc5572/robotools/typestate/InitField.java @@ -0,0 +1,11 @@ +package org.frc5572.robotools.typestate; + +import javax.lang.model.type.TypeMirror; + +/** Field that must be provided when creating a builder */ +public class InitField extends Field { + /** Field that must be provided when creating a builder */ + public InitField(TypeMirror type, String name) { + super(type, name); + } +} diff --git a/src/main/java/org/frc5572/robotools/typestate/MethodField.java b/src/main/java/org/frc5572/robotools/typestate/MethodField.java new file mode 100644 index 0000000..8639cfa --- /dev/null +++ b/src/main/java/org/frc5572/robotools/typestate/MethodField.java @@ -0,0 +1,20 @@ +package org.frc5572.robotools.typestate; + +import javax.lang.model.type.TypeMirror; + +/** A non-init field */ +public class MethodField extends Field { + /** An alternative method for fulfilling this field. */ + public final AltMethod alt; + + /** A non-init field */ + public MethodField(TypeMirror type, String name, AltMethod alt) { + super(type, name); + this.alt = alt; + } + + /** A non-init field */ + public MethodField(TypeMirror type, String name) { + this(type, name, null); + } +} diff --git a/src/main/java/org/frc5572/robotools/typestate/OptionalField.java b/src/main/java/org/frc5572/robotools/typestate/OptionalField.java new file mode 100644 index 0000000..03ad3fa --- /dev/null +++ b/src/main/java/org/frc5572/robotools/typestate/OptionalField.java @@ -0,0 +1,39 @@ +package org.frc5572.robotools.typestate; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.type.TypeMirror; +import org.frc5572.robotools.Utilities; + +/** A field that has a default in case it is not specified. */ +public class OptionalField extends MethodField { + /** Java expression that provides the default value. */ + public final String default_code; + + /** A field that has a default in case it is not specified. */ + public OptionalField(TypeMirror type, String name, AltMethod alt, String default_code) { + super(type, name, alt); + this.default_code = default_code; + } + + /** A field that has a default in case it is not specified. */ + public OptionalField(TypeMirror type, String name, String default_code) { + super(type, name); + this.default_code = default_code; + } + + /** A field that has a default in case it is not specified. */ + public static OptionalField fromAnnotation(TypeMirror type, String name, + AnnotationMirror mirror) { + String defaultCode = ""; + AltMethod alt = null; + for (var ev : mirror.getElementValues().entrySet()) { + if (ev.getKey().getSimpleName().toString().equals("value")) { + defaultCode = Utilities.stringAnnotationValue(ev.getValue()); + } else if (ev.getKey().getSimpleName().toString().equals("alt")) { + AnnotationMirror altMirror = Utilities.annotationAnnotationValue(ev.getValue()); + alt = AltMethod.fromAnnotation(altMirror, name); + } + } + return new OptionalField(type, name, alt, defaultCode); + } +} diff --git a/src/main/java/org/frc5572/robotools/typestate/RequiredField.java b/src/main/java/org/frc5572/robotools/typestate/RequiredField.java new file mode 100644 index 0000000..9d7a877 --- /dev/null +++ b/src/main/java/org/frc5572/robotools/typestate/RequiredField.java @@ -0,0 +1,32 @@ +package org.frc5572.robotools.typestate; + +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.type.TypeMirror; +import org.frc5572.robotools.Utilities; + +/** A field that is required to finish the builder. */ +public class RequiredField extends MethodField { + + /** A field that is required to finish the builder. */ + public RequiredField(TypeMirror type, String name, AltMethod alt) { + super(type, name, alt); + } + + /** A field that is required to finish the builder. */ + public RequiredField(TypeMirror type, String name) { + super(type, name); + } + + /** A field that is required to finish the builder. */ + public static RequiredField fromAnnotation(TypeMirror type, String name, + AnnotationMirror mirror) { + AltMethod alt = null; + for (var ev : mirror.getElementValues().entrySet()) { + if (ev.getKey().getSimpleName().toString().equals("alt")) { + AnnotationMirror altMirror = Utilities.annotationAnnotationValue(ev.getValue()); + alt = AltMethod.fromAnnotation(altMirror, name); + } + } + return new RequiredField(type, name, alt); + } +} diff --git a/src/main/java/org/frc5572/robotools/typestate/TypeStateBuilder.java b/src/main/java/org/frc5572/robotools/typestate/TypeStateBuilder.java new file mode 100644 index 0000000..85ee3c8 --- /dev/null +++ b/src/main/java/org/frc5572/robotools/typestate/TypeStateBuilder.java @@ -0,0 +1,324 @@ +package org.frc5572.robotools.typestate; + +import java.util.ArrayList; +import javax.lang.model.element.Modifier; +import javax.lang.model.type.TypeMirror; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.FieldSpec; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterSpec; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; + +public class TypeStateBuilder { + + private final String name; + private final ArrayList requiredFields = new ArrayList<>(); + private final ArrayList initFields = new ArrayList<>(); + private final ArrayList optionalFields = new ArrayList<>(); + private final Field[] fields; + private final TypeMirror result; + private final boolean isLinear; + + /** Template builder for TypeState Builders */ + public TypeStateBuilder(String name, boolean isLinear, Field[] fields_, TypeMirror result) { + this.fields = fields_; + this.result = result; + this.name = name; + this.isLinear = isLinear; + for (var field : fields_) { + if (field instanceof RequiredField rField) { + requiredFields.add(rField); + } else if (field instanceof OptionalField oField) { + optionalFields.add(oField); + } else if (field instanceof InitField iField) { + initFields.add(iField); + } + } + } + + private static boolean advance(boolean[] enabled, boolean isLinear) { + for (int i = 0; i < enabled.length; i++) { + if (enabled[i]) { + if (!isLinear) { + enabled[i] = false; + } + } else { + enabled[i] = true; + return true; + } + } + return false; + } + + /** Create typestate builders and write them to a typespec */ + public void apply(TypeSpec.Builder builder) { + boolean[] enabled = new boolean[requiredFields.size()]; + + MethodSpec.Builder constructor = MethodSpec.constructorBuilder(); + for (var field : initFields) { + builder.addField(FieldSpec.builder(TypeName.get(field.type), field.name + "_", + Modifier.PRIVATE, Modifier.FINAL).build()); + constructor.addParameter( + ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); + constructor.addCode("this." + field.name + "_ = " + field.name + "_;\n"); + } + for (var field : optionalFields) { + builder.addField(FieldSpec.builder(TypeName.get(field.type), field.name + "_", + Modifier.PRIVATE, Modifier.FINAL).build()); + constructor.addCode("this." + field.name + "_ = " + field.default_code + ";\n"); + } + builder.addMethod(constructor.addModifiers(Modifier.PUBLIC).build()); + constructor = MethodSpec.constructorBuilder(); + boolean isDifferent = false; + for (var field : initFields) { + constructor.addParameter( + ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); + constructor.addCode("this." + field.name + "_ = " + field.name + "_;\n"); + } + for (var field : optionalFields) { + constructor.addParameter( + ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); + constructor.addCode("this." + field.name + "_ = " + field.name + "_;\n"); + isDifferent = true; + } + if (isDifferent) { + builder.addMethod(constructor.addModifiers(Modifier.PRIVATE).build()); + } + addMethods(builder, enabled); + while (advance(enabled, isLinear)) { + constructor = MethodSpec.constructorBuilder(); + for (int i = 0; i < enabled.length; i++) { + if (enabled[i]) { + var field = requiredFields.get(i); + constructor.addParameter( + ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); + constructor.addCode("this." + field.name + "_ = " + field.name + "_;\n"); + } + } + TypeSpec.Builder stepClass = TypeSpec.classBuilder(getName(enabled)); + for (int i = 0; i < enabled.length; i++) { + if (enabled[i]) { + var field = requiredFields.get(i); + stepClass.addField(FieldSpec.builder(TypeName.get(field.type), field.name + "_", + Modifier.PRIVATE, Modifier.FINAL).build()); + } + } + for (var field : initFields) { + stepClass.addField(FieldSpec.builder(TypeName.get(field.type), field.name + "_", + Modifier.PRIVATE, Modifier.FINAL).build()); + constructor.addParameter( + ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); + constructor.addCode("this." + field.name + "_ = " + field.name + "_;\n"); + } + for (var field : optionalFields) { + stepClass.addField(FieldSpec.builder(TypeName.get(field.type), field.name + "_", + Modifier.PRIVATE, Modifier.FINAL).build()); + constructor.addParameter( + ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); + constructor.addCode("this." + field.name + "_ = " + field.name + "_;\n"); + } + stepClass.addMethod(constructor.addModifiers(Modifier.PRIVATE).build()); + addMethods(stepClass, enabled); + builder.addType(stepClass.addModifiers(Modifier.PUBLIC).build()); + } + } + + private String getName(boolean[] enabled) { + StringBuilder builderName = new StringBuilder(this.name); + boolean any = false; + for (int i = 0; i < enabled.length; i++) { + builderName.append(enabled[i] ? "1" : "0"); + any = any || enabled[i]; + } + if (any) { + return builderName.toString(); + } else { + return this.name; + } + } + + private void addMethods(TypeSpec.Builder builder, boolean[] enabled) { + boolean isFinishable = true; + for (int i = 0; i < enabled.length; i++) { + if (!enabled[i]) { + isFinishable = false; + break; + } + } + if (isFinishable) { + TypeName returnType = TypeName.get(result); + MethodSpec.Builder finish = MethodSpec.methodBuilder("finish") + .addModifiers(Modifier.PUBLIC).returns(returnType); + String code = "return new " + returnType + "("; + boolean isFirst = true; + for (Field field : fields) { + if (!isFirst) { + code += ", "; + } + code += field.name + "_"; + isFirst = false; + } + finish.addCode(code + ");\n"); + builder.addMethod(finish.build()); + } + for (int i = 0; i < enabled.length; i++) { + if (!enabled[i]) { + var field = requiredFields.get(i); + boolean[] next = new boolean[enabled.length]; + System.arraycopy(enabled, 0, next, 0, enabled.length); + next[i] = true; + String nextName = getName(next); + MethodSpec.Builder method = + MethodSpec.methodBuilder(field.name).returns(ClassName.get("", nextName)); + method.addParameter( + ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); + String code = "return new " + nextName + "("; + boolean isFirst = true; + for (int j = 0; j < enabled.length; j++) { + if (enabled[j] || i == j) { + if (!isFirst) { + code += ", "; + } + code += requiredFields.get(j).name + "_"; + isFirst = false; + } + } + for (var field_ : initFields) { + if (!isFirst) { + code += ", "; + } + code += field_.name + "_"; + isFirst = false; + } + for (var field_ : optionalFields) { + if (!isFirst) { + code += ", "; + } + code += field_.name + "_"; + isFirst = false; + } + method.addCode(code + ");\n"); + builder.addMethod(method.addModifiers(Modifier.PUBLIC).build()); + + if (field.alt != null) { + method = + MethodSpec.methodBuilder(field.name).returns(ClassName.get("", nextName)); + method.addParameter(ParameterSpec + .builder(TypeName.get(field.alt.type()), field.alt.parameterName()) + .build()); + code = "return new " + nextName + "("; + isFirst = true; + for (int j = 0; j < enabled.length; j++) { + if (i == j) { + if (!isFirst) { + code += ", "; + } + code += field.alt.code(); + isFirst = false; + continue; + } + if (enabled[j]) { + if (!isFirst) { + code += ", "; + } + code += requiredFields.get(j).name + "_"; + isFirst = false; + } + } + for (var field_ : initFields) { + if (!isFirst) { + code += ", "; + } + code += field_.name + "_"; + isFirst = false; + } + for (var field_ : optionalFields) { + if (!isFirst) { + code += ", "; + } + code += field_.name + "_"; + isFirst = false; + } + method.addCode(code + ");\n"); + builder.addMethod(method.addModifiers(Modifier.PUBLIC).build()); + } + if (isLinear) { + break; + } + } + } + String thisName = getName(enabled); + for (var field : optionalFields) { + MethodSpec.Builder method = + MethodSpec.methodBuilder(field.name).returns(ClassName.get("", thisName)); + method.addParameter( + ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); + String code = "return new " + thisName + "("; + boolean isFirst = true; + for (int i = 0; i < enabled.length; i++) { + if (enabled[i]) { + if (!isFirst) { + code += ", "; + } + code += requiredFields.get(i).name + "_"; + isFirst = false; + } + } + for (var field_ : initFields) { + if (!isFirst) { + code += ", "; + } + code += field_.name + "_"; + isFirst = false; + } + for (var field_ : optionalFields) { + if (!isFirst) { + code += ", "; + } + code += field_.name + "_"; + isFirst = false; + } + method.addCode(code + ");\n"); + builder.addMethod(method.addModifiers(Modifier.PUBLIC).build()); + + if (field.alt != null) { + method = MethodSpec.methodBuilder(field.name).returns(ClassName.get("", thisName)); + method.addParameter(ParameterSpec + .builder(TypeName.get(field.alt.type()), field.alt.parameterName()).build()); + code = "return new " + thisName + "("; + isFirst = true; + for (int i = 0; i < enabled.length; i++) { + if (enabled[i]) { + if (!isFirst) { + code += ", "; + } + code += requiredFields.get(i).name + "_"; + isFirst = false; + } + } + for (var field_ : initFields) { + if (!isFirst) { + code += ", "; + } + code += field_.name + "_"; + isFirst = false; + } + for (var field_ : optionalFields) { + if (!isFirst) { + code += ", "; + } + if (field_ == field) { + code += field.alt.code(); + } else { + code += field_.name + "_"; + } + isFirst = false; + } + method.addCode(code + ");\n"); + builder.addMethod(method.addModifiers(Modifier.PUBLIC).build()); + } + } + } + +} diff --git a/src/main/java/org/frc5572/robotools/typestate/TypeStateBuilderGenerator.java b/src/main/java/org/frc5572/robotools/typestate/TypeStateBuilderGenerator.java index 43cc30b..37bdf37 100644 --- a/src/main/java/org/frc5572/robotools/typestate/TypeStateBuilderGenerator.java +++ b/src/main/java/org/frc5572/robotools/typestate/TypeStateBuilderGenerator.java @@ -5,23 +5,16 @@ import java.util.List; import javax.annotation.processing.ProcessingEnvironment; import javax.annotation.processing.RoundEnvironment; -import javax.lang.model.element.AnnotationMirror; 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; import javax.tools.Diagnostic; import javax.tools.Diagnostic.Kind; import org.frc5572.robotools.AnnotationGenerator; import org.frc5572.robotools.Utilities; -import com.squareup.javapoet.ClassName; -import com.squareup.javapoet.FieldSpec; import com.squareup.javapoet.JavaFile; -import com.squareup.javapoet.MethodSpec; -import com.squareup.javapoet.ParameterSpec; -import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeSpec; /** @@ -77,7 +70,7 @@ public void generate(TypeElement annotation, RoundEnvironment roundEnv) { } } - List fields = new ArrayList<>(); + List fields = new ArrayList<>(); List params = constructorElement.getParameters(); for (int i = 0; i < params.size(); i++) { boolean found = false; @@ -91,8 +84,7 @@ public void generate(TypeElement annotation, RoundEnvironment roundEnv) { + "have one of @InitField, @RequiredField or @OptionalField", param); } - fields.add(new TypeStateBuilder.InitField(param.asType(), - param.getSimpleName().toString())); + fields.add(new InitField(param.asType(), param.getSimpleName().toString())); found = true; } else if (mirror.getAnnotationType().asElement().getSimpleName().toString() .equals("RequiredField")) { @@ -102,7 +94,7 @@ public void generate(TypeElement annotation, RoundEnvironment roundEnv) { + "have one of @InitField, @RequiredField or @OptionalField", param); } - fields.add(TypeStateBuilder.RequiredField.fromAnnotation(param.asType(), + fields.add(RequiredField.fromAnnotation(param.asType(), param.getSimpleName().toString(), mirror)); found = true; } else if (mirror.getAnnotationType().asElement().getSimpleName().toString() @@ -113,7 +105,7 @@ public void generate(TypeElement annotation, RoundEnvironment roundEnv) { + "have one of @InitField, @RequiredField or @OptionalField", param); } - fields.add(TypeStateBuilder.OptionalField.fromAnnotation(param.asType(), + fields.add(OptionalField.fromAnnotation(param.asType(), param.getSimpleName().toString(), mirror)); found = true; } @@ -131,7 +123,7 @@ public void generate(TypeElement annotation, RoundEnvironment roundEnv) { TypeSpec.classBuilder(builderName).addModifiers(Modifier.PUBLIC, Modifier.FINAL); TypeStateBuilder typeStateBuilder = new TypeStateBuilder(builderName, isLinear, - fields.toArray(TypeStateBuilder.Field[]::new), parent.asType()); + fields.toArray(Field[]::new), parent.asType()); typeStateBuilder.apply(specBuilder); var spec = specBuilder.build(); @@ -146,463 +138,5 @@ public void generate(TypeElement annotation, RoundEnvironment roundEnv) { } }); } - - private static class TypeStateBuilder { - - private final String name; - private final ArrayList requiredFields = new ArrayList<>(); - private final ArrayList initFields = new ArrayList<>(); - private final ArrayList optionalFields = new ArrayList<>(); - private final Field[] fields; - private final TypeMirror result; - private final boolean isLinear; - - /** Template builder for TypeState Builders */ - public TypeStateBuilder(String name, boolean isLinear, Field[] fields_, TypeMirror result) { - this.fields = fields_; - this.result = result; - this.name = name; - this.isLinear = isLinear; - for (var field : fields_) { - if (field instanceof RequiredField rField) { - requiredFields.add(rField); - } else if (field instanceof OptionalField oField) { - optionalFields.add(oField); - } else if (field instanceof InitField iField) { - initFields.add(iField); - } - } - } - - private static boolean advance(boolean[] enabled, boolean isLinear) { - for (int i = 0; i < enabled.length; i++) { - if (enabled[i]) { - if (!isLinear) { - enabled[i] = false; - } - } else { - enabled[i] = true; - return true; - } - } - return false; - } - - /** Create typestate builders and write them to a typespec */ - public void apply(TypeSpec.Builder builder) { - boolean[] enabled = new boolean[requiredFields.size()]; - - MethodSpec.Builder constructor = MethodSpec.constructorBuilder(); - for (var field : initFields) { - builder.addField(FieldSpec.builder(TypeName.get(field.type), field.name + "_", - Modifier.PRIVATE, Modifier.FINAL).build()); - constructor.addParameter( - ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); - constructor.addCode("this." + field.name + "_ = " + field.name + "_;\n"); - } - for (var field : optionalFields) { - builder.addField(FieldSpec.builder(TypeName.get(field.type), field.name + "_", - Modifier.PRIVATE, Modifier.FINAL).build()); - constructor.addCode("this." + field.name + "_ = " + field.default_code + ";\n"); - } - builder.addMethod(constructor.addModifiers(Modifier.PUBLIC).build()); - constructor = MethodSpec.constructorBuilder(); - boolean isDifferent = false; - for (var field : initFields) { - constructor.addParameter( - ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); - constructor.addCode("this." + field.name + "_ = " + field.name + "_;\n"); - } - for (var field : optionalFields) { - constructor.addParameter( - ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); - constructor.addCode("this." + field.name + "_ = " + field.name + "_;\n"); - isDifferent = true; - } - if (isDifferent) { - builder.addMethod(constructor.addModifiers(Modifier.PRIVATE).build()); - } - addMethods(builder, enabled); - while (advance(enabled, isLinear)) { - constructor = MethodSpec.constructorBuilder(); - for (int i = 0; i < enabled.length; i++) { - if (enabled[i]) { - var field = requiredFields.get(i); - constructor.addParameter(ParameterSpec - .builder(TypeName.get(field.type), field.name + "_").build()); - constructor.addCode("this." + field.name + "_ = " + field.name + "_;\n"); - } - } - TypeSpec.Builder stepClass = TypeSpec.classBuilder(getName(enabled)); - for (int i = 0; i < enabled.length; i++) { - if (enabled[i]) { - var field = requiredFields.get(i); - stepClass.addField(FieldSpec.builder(TypeName.get(field.type), - field.name + "_", Modifier.PRIVATE, Modifier.FINAL).build()); - } - } - for (var field : initFields) { - stepClass.addField(FieldSpec.builder(TypeName.get(field.type), field.name + "_", - Modifier.PRIVATE, Modifier.FINAL).build()); - constructor.addParameter( - ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); - constructor.addCode("this." + field.name + "_ = " + field.name + "_;\n"); - } - for (var field : optionalFields) { - stepClass.addField(FieldSpec.builder(TypeName.get(field.type), field.name + "_", - Modifier.PRIVATE, Modifier.FINAL).build()); - constructor.addParameter( - ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); - constructor.addCode("this." + field.name + "_ = " + field.name + "_;\n"); - } - stepClass.addMethod(constructor.addModifiers(Modifier.PRIVATE).build()); - addMethods(stepClass, enabled); - builder.addType(stepClass.addModifiers(Modifier.PUBLIC).build()); - } - } - - private String getName(boolean[] enabled) { - StringBuilder builderName = new StringBuilder(this.name); - boolean any = false; - for (int i = 0; i < enabled.length; i++) { - builderName.append(enabled[i] ? "1" : "0"); - any = any || enabled[i]; - } - if (any) { - return builderName.toString(); - } else { - return this.name; - } - } - - private void addMethods(TypeSpec.Builder builder, boolean[] enabled) { - boolean isFinishable = true; - for (int i = 0; i < enabled.length; i++) { - if (!enabled[i]) { - isFinishable = false; - break; - } - } - if (isFinishable) { - TypeName returnType = TypeName.get(result); - MethodSpec.Builder finish = MethodSpec.methodBuilder("finish") - .addModifiers(Modifier.PUBLIC).returns(returnType); - String code = "return new " + returnType + "("; - boolean isFirst = true; - for (Field field : fields) { - if (!isFirst) { - code += ", "; - } - code += field.name + "_"; - isFirst = false; - } - finish.addCode(code + ");\n"); - builder.addMethod(finish.build()); - } - for (int i = 0; i < enabled.length; i++) { - if (!enabled[i]) { - var field = requiredFields.get(i); - boolean[] next = new boolean[enabled.length]; - System.arraycopy(enabled, 0, next, 0, enabled.length); - next[i] = true; - String nextName = getName(next); - MethodSpec.Builder method = - MethodSpec.methodBuilder(field.name).returns(ClassName.get("", nextName)); - method.addParameter( - ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); - String code = "return new " + nextName + "("; - boolean isFirst = true; - for (int j = 0; j < enabled.length; j++) { - if (enabled[j] || i == j) { - if (!isFirst) { - code += ", "; - } - code += requiredFields.get(j).name + "_"; - isFirst = false; - } - } - for (var field_ : initFields) { - if (!isFirst) { - code += ", "; - } - code += field_.name + "_"; - isFirst = false; - } - for (var field_ : optionalFields) { - if (!isFirst) { - code += ", "; - } - code += field_.name + "_"; - isFirst = false; - } - method.addCode(code + ");\n"); - builder.addMethod(method.addModifiers(Modifier.PUBLIC).build()); - - if (field.alt != null) { - method = MethodSpec.methodBuilder(field.name) - .returns(ClassName.get("", nextName)); - method.addParameter(ParameterSpec - .builder(TypeName.get(field.alt.type), field.alt.parameterName) - .build()); - code = "return new " + nextName + "("; - isFirst = true; - for (int j = 0; j < enabled.length; j++) { - if (i == j) { - if (!isFirst) { - code += ", "; - } - code += field.alt.code; - isFirst = false; - continue; - } - if (enabled[j]) { - if (!isFirst) { - code += ", "; - } - code += requiredFields.get(j).name + "_"; - isFirst = false; - } - } - for (var field_ : initFields) { - if (!isFirst) { - code += ", "; - } - code += field_.name + "_"; - isFirst = false; - } - for (var field_ : optionalFields) { - if (!isFirst) { - code += ", "; - } - code += field_.name + "_"; - isFirst = false; - } - method.addCode(code + ");\n"); - builder.addMethod(method.addModifiers(Modifier.PUBLIC).build()); - } - if (isLinear) { - break; - } - } - } - String thisName = getName(enabled); - for (var field : optionalFields) { - MethodSpec.Builder method = - MethodSpec.methodBuilder(field.name).returns(ClassName.get("", thisName)); - method.addParameter( - ParameterSpec.builder(TypeName.get(field.type), field.name + "_").build()); - String code = "return new " + thisName + "("; - boolean isFirst = true; - for (int i = 0; i < enabled.length; i++) { - if (enabled[i]) { - if (!isFirst) { - code += ", "; - } - code += requiredFields.get(i).name + "_"; - isFirst = false; - } - } - for (var field_ : initFields) { - if (!isFirst) { - code += ", "; - } - code += field_.name + "_"; - isFirst = false; - } - for (var field_ : optionalFields) { - if (!isFirst) { - code += ", "; - } - code += field_.name + "_"; - isFirst = false; - } - method.addCode(code + ");\n"); - builder.addMethod(method.addModifiers(Modifier.PUBLIC).build()); - - if (field.alt != null) { - method = - MethodSpec.methodBuilder(field.name).returns(ClassName.get("", thisName)); - method.addParameter(ParameterSpec - .builder(TypeName.get(field.alt.type), field.alt.parameterName).build()); - code = "return new " + thisName + "("; - isFirst = true; - for (int i = 0; i < enabled.length; i++) { - if (enabled[i]) { - if (!isFirst) { - code += ", "; - } - code += requiredFields.get(i).name + "_"; - isFirst = false; - } - } - for (var field_ : initFields) { - if (!isFirst) { - code += ", "; - } - code += field_.name + "_"; - isFirst = false; - } - for (var field_ : optionalFields) { - if (!isFirst) { - code += ", "; - } - if (field_ == field) { - code += field.alt.code; - } else { - code += field_.name + "_"; - } - isFirst = false; - } - method.addCode(code + ");\n"); - builder.addMethod(method.addModifiers(Modifier.PUBLIC).build()); - } - } - } - - /** Base class for fields */ - public static class Field { - /** Field type */ - public final TypeMirror type; - /** Field name */ - public final String name; - - /** Base class for fields */ - public Field(TypeMirror type, String name) { - this.type = type; - this.name = name; - } - } - - /** Field that must be provided when creating a builder */ - public static class InitField extends Field { - /** Field that must be provided when creating a builder */ - public InitField(TypeMirror type, String name) { - super(type, name); - } - } - - /** A non-init field */ - public static class MethodField extends Field { - /** An alternative method for fulfilling this field. */ - public final AltMethod alt; - - /** A non-init field */ - public MethodField(TypeMirror type, String name, AltMethod alt) { - super(type, name); - this.alt = alt; - } - - /** A non-init field */ - public MethodField(TypeMirror type, String name) { - this(type, name, null); - } - } - - /** A field that is required to finish the builder. */ - public static class RequiredField extends MethodField { - - /** A field that is required to finish the builder. */ - public RequiredField(TypeMirror type, String name, AltMethod alt) { - super(type, name, alt); - } - - /** A field that is required to finish the builder. */ - public RequiredField(TypeMirror type, String name) { - super(type, name); - } - - /** A field that is required to finish the builder. */ - public static RequiredField fromAnnotation(TypeMirror type, String name, - AnnotationMirror mirror) { - AltMethod alt = null; - for (var ev : mirror.getElementValues().entrySet()) { - if (ev.getKey().getSimpleName().toString().equals("alt")) { - AnnotationMirror altMirror = - Utilities.annotationAnnotationValue(ev.getValue()); - alt = AltMethod.fromAnnotation(altMirror, name); - } - } - return new RequiredField(type, name, alt); - } - } - - /** A field that has a default in case it is not specified. */ - public static class OptionalField extends MethodField { - /** Java expression that provides the default value. */ - public final String default_code; - - /** A field that has a default in case it is not specified. */ - public OptionalField(TypeMirror type, String name, AltMethod alt, String default_code) { - super(type, name, alt); - this.default_code = default_code; - } - - /** A field that has a default in case it is not specified. */ - public OptionalField(TypeMirror type, String name, String default_code) { - super(type, name); - this.default_code = default_code; - } - - /** A field that has a default in case it is not specified. */ - public static OptionalField fromAnnotation(TypeMirror type, String name, - AnnotationMirror mirror) { - String defaultCode = ""; - AltMethod alt = null; - for (var ev : mirror.getElementValues().entrySet()) { - if (ev.getKey().getSimpleName().toString().equals("value")) { - defaultCode = Utilities.stringAnnotationValue(ev.getValue()); - } else if (ev.getKey().getSimpleName().toString().equals("alt")) { - AnnotationMirror altMirror = - Utilities.annotationAnnotationValue(ev.getValue()); - alt = AltMethod.fromAnnotation(altMirror, name); - } - } - return new OptionalField(type, name, alt, defaultCode); - } - } - - /** Alternative method for a field. */ - public static record AltMethod(TypeMirror type, String parameterName, String code) { - /** Alternative method for a field. */ - public static AltMethod fromAnnotation(AnnotationMirror mirror, String defaultName) { - if (mirror == null) { - System.out.println("annotation is null"); - return null; - } - - if (!mirror.getAnnotationType().asElement().getSimpleName().toString() - .equals("AltMethod")) { - System.out.println("annotation name doesn't match " - + mirror.getAnnotationType().asElement().getSimpleName().toString()); - return null; - } - TypeMirror type = null; - String parameterName = defaultName; - String code = null; - - for (var ev : mirror.getElementValues().entrySet()) { - if (ev.getKey().getSimpleName().toString().equals("type")) { - type = Utilities.classAnnotationValue(ev.getValue()); - } else if (ev.getKey().getSimpleName().toString().equals("parameter_name")) { - parameterName = Utilities.stringAnnotationValue(ev.getValue()); - } else if (ev.getKey().getSimpleName().toString().equals("value")) { - code = Utilities.stringAnnotationValue(ev.getValue()); - } - } - - if (type == null) { - System.out.println("Missing type"); - return null; - } - if (code == null) { - System.out.println("Missing code"); - return null; - } - - return new AltMethod(type, parameterName, code); - } - } - - } } From 5a926115649109258c4a2a4ed8494a1f8cba6013 Mon Sep 17 00:00:00 2001 From: wilsonwatson Date: Thu, 21 May 2026 20:22:49 -0500 Subject: [PATCH 3/7] javadoc --- .../java/org/frc5572/robotools/typestate/TypeStateBuilder.java | 1 + 1 file changed, 1 insertion(+) diff --git a/src/main/java/org/frc5572/robotools/typestate/TypeStateBuilder.java b/src/main/java/org/frc5572/robotools/typestate/TypeStateBuilder.java index 85ee3c8..5109705 100644 --- a/src/main/java/org/frc5572/robotools/typestate/TypeStateBuilder.java +++ b/src/main/java/org/frc5572/robotools/typestate/TypeStateBuilder.java @@ -10,6 +10,7 @@ import com.squareup.javapoet.TypeName; import com.squareup.javapoet.TypeSpec; +/** Main builder for typestate builders */ public class TypeStateBuilder { private final String name; From efc8607dd746aa72afbd2d2b0c560e225fe89522 Mon Sep 17 00:00:00 2001 From: wilsonwatson Date: Thu, 21 May 2026 20:36:08 -0500 Subject: [PATCH 4/7] tests --- build.gradle | 8 ++++ .../robotools/GenerateEmptyIOTest.java | 48 +++++++++++++++++++ 2 files changed, 56 insertions(+) create mode 100644 src/test/java/org/frc5572/robotools/GenerateEmptyIOTest.java diff --git a/build.gradle b/build.gradle index d870be0..ae53d62 100755 --- a/build.gradle +++ b/build.gradle @@ -13,6 +13,14 @@ repositories { dependencies { implementation "com.squareup:javapoet:1.13.0" + + testImplementation 'junit:junit:4.13.2' + testImplementation 'com.google.testing.compile:compile-testing:0.21.0' + testImplementation 'com.google.truth:truth:1.4.4' +} + +test { + useJUnit() } publishing { diff --git a/src/test/java/org/frc5572/robotools/GenerateEmptyIOTest.java b/src/test/java/org/frc5572/robotools/GenerateEmptyIOTest.java new file mode 100644 index 0000000..fa29bf1 --- /dev/null +++ b/src/test/java/org/frc5572/robotools/GenerateEmptyIOTest.java @@ -0,0 +1,48 @@ +package org.frc5572.robotools; + +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; + +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; +import javax.tools.JavaFileObject; +import org.junit.Test; + +/** Test for {@code @GenerateEmptyIO} */ +public class GenerateEmptyIOTest { + + // @formatter:off + private static final JavaFileObject ANNOTATION = JavaFileObjects.forSourceLines( + "frc.robot.util.GenerateEmptyIO", + "package frc.robot.util;", + "import java.lang.annotation.*;", + "@Target(ElementType.TYPE)", + "@Retention(RetentionPolicy.SOURCE)", + "public @interface GenerateEmptyIO {", + " Class[] value() default {};", + "}" + ); + // @formatter:on + + /** Test simple execution */ + @Test + public void generatesEmptyClassForSimpleInterface() { + // @formatter:off + JavaFileObject input = JavaFileObjects.forSourceLines( + "frc.robot.util.ExampleIO", + "package frc.robot.util;", + "@GenerateEmptyIO", + "public interface ExampleIO {", + " void setMotorVoltage(double voltage);", + " void stop();", + "}" + ); + // @formatter:on + + Compilation compilation = + javac().withProcessors(new RobotProcessor()).compile(ANNOTATION, input); + + assertThat(compilation).succeeded(); + assertThat(compilation).generatedSourceFile("frc/robot/util/ExampleIOEmpty"); + } +} From 02ab2a76f700dcfdc3972f04ec1ca68bfcd92a2b Mon Sep 17 00:00:00 2001 From: wilsonwatson Date: Thu, 21 May 2026 20:43:25 -0500 Subject: [PATCH 5/7] workflow tests --- .github/workflows/main.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 88f6c0e..7483a99 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -5,6 +5,16 @@ on: permissions: contents: write jobs: + test: + name: Test + runs-on: ubuntu-latest + container: wpilib/roborio-cross-ubuntu:2025-24.04 + steps: + - uses: actions/checkout@v4 + - name: Grant execute permission for gradlew + run: chmod +x gradlew + - name: Compile and run tests on robot code + run: ./gradlew test linting: name: Linting runs-on: ubuntu-latest From 6a8a2ac9e8a60a65c61be1e562dff5ff67634d4f Mon Sep 17 00:00:00 2001 From: wilsonwatson Date: Thu, 21 May 2026 20:47:12 -0500 Subject: [PATCH 6/7] better task display --- build.gradle | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/build.gradle b/build.gradle index ae53d62..7ceddcb 100755 --- a/build.gradle +++ b/build.gradle @@ -35,3 +35,42 @@ publishing { } } } + +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.api.tasks.testing.logging.TestLogEvent + +tasks.withType(Test) { + testLogging { + // set options for log level LIFECYCLE + events TestLogEvent.FAILED, + TestLogEvent.PASSED, + TestLogEvent.SKIPPED, + TestLogEvent.STANDARD_OUT + exceptionFormat TestExceptionFormat.FULL + showExceptions true + showCauses true + showStackTraces true + + // set options for log level DEBUG and INFO + debug { + events TestLogEvent.STARTED, + TestLogEvent.FAILED, + TestLogEvent.PASSED, + TestLogEvent.SKIPPED, + TestLogEvent.STANDARD_ERROR, + TestLogEvent.STANDARD_OUT + exceptionFormat TestExceptionFormat.FULL + } + info.events = debug.events + info.exceptionFormat = debug.exceptionFormat + + afterSuite { desc, result -> + if (!desc.parent) { // will match the outermost suite + def output = "Results: ${result.resultType} (${result.testCount} tests, ${result.successfulTestCount} passed, ${result.failedTestCount} failed, ${result.skippedTestCount} skipped)" + def startItem = '| ', endItem = ' |' + def repeatLength = startItem.length() + output.length() + endItem.length() + println('\n' + ('-' * repeatLength) + '\n' + startItem + output + endItem + '\n' + ('-' * repeatLength)) + } + } + } +} From a3440f80a5adf8bb6a67e41880726bcc629585c4 Mon Sep 17 00:00:00 2001 From: wilsonwatson Date: Fri, 22 May 2026 09:23:33 -0500 Subject: [PATCH 7/7] more tests --- .../robotools/GenerateEmptyIOTest.java | 68 +++++- .../robotools/TypeStateBuilderTest.java | 207 ++++++++++++++++++ 2 files changed, 263 insertions(+), 12 deletions(-) create mode 100644 src/test/java/org/frc5572/robotools/TypeStateBuilderTest.java diff --git a/src/test/java/org/frc5572/robotools/GenerateEmptyIOTest.java b/src/test/java/org/frc5572/robotools/GenerateEmptyIOTest.java index fa29bf1..a8697c8 100644 --- a/src/test/java/org/frc5572/robotools/GenerateEmptyIOTest.java +++ b/src/test/java/org/frc5572/robotools/GenerateEmptyIOTest.java @@ -2,39 +2,38 @@ import static com.google.testing.compile.CompilationSubject.assertThat; import static com.google.testing.compile.Compiler.javac; - -import com.google.testing.compile.Compilation; -import com.google.testing.compile.JavaFileObjects; import javax.tools.JavaFileObject; import org.junit.Test; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; /** Test for {@code @GenerateEmptyIO} */ public class GenerateEmptyIOTest { // @formatter:off private static final JavaFileObject ANNOTATION = JavaFileObjects.forSourceLines( - "frc.robot.util.GenerateEmptyIO", + "frc.robot.util.GenerateEmptyIO", "package frc.robot.util;", - "import java.lang.annotation.*;", + "import java.lang.annotation.*;", "@Target(ElementType.TYPE)", - "@Retention(RetentionPolicy.SOURCE)", + "@Retention(RetentionPolicy.SOURCE)", "public @interface GenerateEmptyIO {", - " Class[] value() default {};", + " Class[] value() default {};", "}" ); // @formatter:on - /** Test simple execution */ + /** No-arg case */ @Test public void generatesEmptyClassForSimpleInterface() { // @formatter:off JavaFileObject input = JavaFileObjects.forSourceLines( "frc.robot.util.ExampleIO", - "package frc.robot.util;", - "@GenerateEmptyIO", + "package frc.robot.util;", + "@GenerateEmptyIO", "public interface ExampleIO {", - " void setMotorVoltage(double voltage);", - " void stop();", + " void setMotorVoltage(double voltage);", + " void stop();", "}" ); // @formatter:on @@ -45,4 +44,49 @@ public void generatesEmptyClassForSimpleInterface() { assertThat(compilation).succeeded(); assertThat(compilation).generatedSourceFile("frc/robot/util/ExampleIOEmpty"); } + + /** Single constructor arg */ + @Test + public void generatesConstructorWithSingleArg() { + // @formatter:off + JavaFileObject input = JavaFileObjects.forSourceLines( + "frc.robot.util.OdometryIO", + "package frc.robot.util;", + "@GenerateEmptyIO(Runnable.class)", + "public interface OdometryIO {", + " void updateInputs(Object inputs);", + "}" + ); + // @formatter:on + + Compilation compilation = + javac().withProcessors(new RobotProcessor()).compile(ANNOTATION, input); + + assertThat(compilation).succeeded(); + assertThat(compilation).generatedSourceFile("frc/robot/util/OdometryIOEmpty") + .contentsAsUtf8String().contains("OdometryIOEmpty(Runnable arg1)"); + } + + /** Multi-arg constructor */ + @Test + public void generatesConstructorWithMultipleArgs() { + // @formatter:off + JavaFileObject input = JavaFileObjects.forSourceLines( + "frc.robot.util.ModuleIO", + "package frc.robot.util;", + "@GenerateEmptyIO({int.class, Runnable.class})", + "public interface ModuleIO {", + " void updateInputs(Object inputs);", + " void runDriveOpenLoop(double output);", + "}" + ); + // @formatter:on + + Compilation compilation = + javac().withProcessors(new RobotProcessor()).compile(ANNOTATION, input); + + assertThat(compilation).succeeded(); + assertThat(compilation).generatedSourceFile("frc/robot/util/ModuleIOEmpty") + .contentsAsUtf8String().contains("ModuleIOEmpty(int arg1, Runnable arg2)"); + } } diff --git a/src/test/java/org/frc5572/robotools/TypeStateBuilderTest.java b/src/test/java/org/frc5572/robotools/TypeStateBuilderTest.java new file mode 100644 index 0000000..5b1072d --- /dev/null +++ b/src/test/java/org/frc5572/robotools/TypeStateBuilderTest.java @@ -0,0 +1,207 @@ +package org.frc5572.robotools; + +import static com.google.testing.compile.CompilationSubject.assertThat; +import static com.google.testing.compile.Compiler.javac; +import javax.tools.JavaFileObject; +import org.junit.Test; +import com.google.testing.compile.Compilation; +import com.google.testing.compile.JavaFileObjects; + +/** Tests for {@code @TypeStateBuilder} */ +public class TypeStateBuilderTest { + + // @formatter:off + private static final JavaFileObject ANN_TYPESTATE_BUILDER = JavaFileObjects.forSourceLines( + "frc.robot.util.typestate.TypeStateBuilder", + "package frc.robot.util.typestate;", + "import java.lang.annotation.*;", + "@Retention(RetentionPolicy.CLASS)", + "@Target(ElementType.CONSTRUCTOR)", + "public @interface TypeStateBuilder {", + " String value() default \"\";", + " boolean linear() default false;", + "}" + ); + private static final JavaFileObject ANN_INIT_FIELD = JavaFileObjects.forSourceLines( + "frc.robot.util.typestate.InitField", + "package frc.robot.util.typestate;", + "import java.lang.annotation.*;", + "@Retention(RetentionPolicy.CLASS)", + "@Target(ElementType.PARAMETER)", + "public @interface InitField {}" + ); + private static final JavaFileObject ANN_REQUIRED_FIELD = JavaFileObjects.forSourceLines( + "frc.robot.util.typestate.RequiredField", + "package frc.robot.util.typestate;", + "import java.lang.annotation.*;", + "@Retention(RetentionPolicy.CLASS)", + "@Target(ElementType.PARAMETER)", + "public @interface RequiredField {", + " AltMethod[] alt() default {};", + "}" + ); + private static final JavaFileObject ANN_OPTIONAL_FIELD = JavaFileObjects.forSourceLines( + "frc.robot.util.typestate.OptionalField", + "package frc.robot.util.typestate;", + "import java.lang.annotation.*;", + "@Retention(RetentionPolicy.CLASS)", + "@Target(ElementType.PARAMETER)", + "public @interface OptionalField {", + " String value();", + " AltMethod[] alt() default {};", + "}" + ); + private static final JavaFileObject ANN_ALT_METHOD = JavaFileObjects.forSourceLines( + "frc.robot.util.typestate.AltMethod", + "package frc.robot.util.typestate;", + "public @interface AltMethod {", + " Class type();", + " String parameter_name() default \"\";", + " String value();", + "}" + ); + // @formatter:on + + private Compilation compile(JavaFileObject input) { + return javac().withProcessors(new RobotProcessor()).compile(ANN_TYPESTATE_BUILDER, + ANN_INIT_FIELD, ANN_REQUIRED_FIELD, ANN_OPTIONAL_FIELD, ANN_ALT_METHOD, input); + } + + /** No value() -> builder name is {ClassName}Builder. */ + @Test + public void generatesBuilderWithDefaultName() { + // @formatter:off + JavaFileObject input = JavaFileObjects.forSourceLines( + "frc.robot.util.tunable.Color", + "package frc.robot.util.tunable;", + "import frc.robot.util.typestate.*;", + "public class Color {", + " public final int r;", + " @TypeStateBuilder", + " public Color(@RequiredField int r) { this.r = r; }", + "}" + ); + // @formatter:on + + Compilation compilation = compile(input); + + assertThat(compilation).succeeded(); + assertThat(compilation).generatedSourceFile("frc/robot/util/tunable/ColorBuilder"); + } + + /** value() overrides the builder name. */ + @Test + public void generatesBuilderWithCustomName() { + // @formatter:off + JavaFileObject input = JavaFileObjects.forSourceLines( + "frc.robot.util.tunable.Config", + "package frc.robot.util.tunable;", + "import frc.robot.util.typestate.*;", + "public class Config {", + " public final double value;", + " @TypeStateBuilder(\"SpecialBuilder\")", + " public Config(@RequiredField double value) { this.value = value; }", + "}" + ); + // @formatter:on + + Compilation compilation = compile(input); + + assertThat(compilation).succeeded(); + assertThat(compilation).generatedSourceFile("frc/robot/util/tunable/SpecialBuilder"); + } + + /** + * Each @RequiredField becomes a setter; finish() appears only when all are set. + */ + @Test + public void requiredFieldsGenerateSettersThenFinish() { + // @formatter:off + JavaFileObject input = JavaFileObjects.forSourceLines( + "frc.robot.util.tunable.Point", + "package frc.robot.util.tunable;", + "import frc.robot.util.typestate.*;", + "public class Point {", + " public final int x, y;", + " @TypeStateBuilder", + " public Point(@InitField String label, @RequiredField int x, @RequiredField int y) {", + " this.x = x; this.y = y;", + " }", + "}" + ); + // @formatter:on + + Compilation compilation = compile(input); + + assertThat(compilation).succeeded(); + // Initial builder exposes setters for all required fields + assertThat(compilation).generatedSourceFile("frc/robot/util/tunable/PointBuilder") + .contentsAsUtf8String().containsMatch("PointBuilder10 x\\("); + assertThat(compilation).generatedSourceFile("frc/robot/util/tunable/PointBuilder") + .contentsAsUtf8String().containsMatch("PointBuilder01 y\\("); + // finish() only available once both are set + assertThat(compilation).generatedSourceFile("frc/robot/util/tunable/PointBuilder") + .contentsAsUtf8String().containsMatch("Point finish\\("); + } + + /** + * @OptionalField gets a setter at every stage and defaults on the no-arg constructor. + */ + @Test + public void optionalFieldHasSetterAndDefault() { + // @formatter:off + JavaFileObject input = JavaFileObjects.forSourceLines( + "frc.robot.util.tunable.Gains", + "package frc.robot.util.tunable;", + "import frc.robot.util.typestate.*;", + "public class Gains {", + " public final double kP, kI;", + " @TypeStateBuilder", + " public Gains(@InitField String name, @RequiredField double kP, @OptionalField(\"0.0\") double kI) {", + " this.kP = kP; this.kI = kI;", + " }", + "}" + ); + // @formatter:on + + Compilation compilation = compile(input); + + assertThat(compilation).succeeded(); + // finish() available on the state where kP is set + assertThat(compilation).generatedSourceFile("frc/robot/util/tunable/GainsBuilder") + .contentsAsUtf8String().containsMatch("Gains finish\\("); + // kI setter present at the initial state + assertThat(compilation).generatedSourceFile("frc/robot/util/tunable/GainsBuilder") + .contentsAsUtf8String().containsMatch("GainsBuilder kI\\("); + } + + /** + * linear=true exposes only the next required field setter at each step. + */ + @Test + public void linearBuilderExposesOnlyNextStep() { + // @formatter:off + JavaFileObject input = JavaFileObjects.forSourceLines( + "frc.robot.util.tunable.Step", + "package frc.robot.util.tunable;", + "import frc.robot.util.typestate.*;", + "public class Step {", + " public final int a, b;", + " @TypeStateBuilder(linear = true)", + " public Step(@RequiredField int a, @RequiredField int b) {", + " this.a = a; this.b = b;", + " }", + "}" + ); + // @formatter:on + + Compilation compilation = compile(input); + + assertThat(compilation).succeeded(); + // Initial builder only exposes 'a', not 'b' yet + assertThat(compilation).generatedSourceFile("frc/robot/util/tunable/StepBuilder") + .contentsAsUtf8String().containsMatch("StepBuilder10 a\\("); + assertThat(compilation).generatedSourceFile("frc/robot/util/tunable/StepBuilder") + .contentsAsUtf8String().doesNotContainMatch("StepBuilder0. b\\("); + } +}