diff --git a/src/main/java/com/github/joschi/jadconfig/ParameterDocumentationValidator.java b/src/main/java/com/github/joschi/jadconfig/ParameterDocumentationValidator.java new file mode 100644 index 0000000..8b14e7a --- /dev/null +++ b/src/main/java/com/github/joschi/jadconfig/ParameterDocumentationValidator.java @@ -0,0 +1,126 @@ +package com.github.joschi.jadconfig; + +import com.github.joschi.jadconfig.documentation.Documentation; +import org.apache.commons.lang3.StringUtils; + +import javax.annotation.processing.AbstractProcessor; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import java.util.Arrays; +import java.util.Optional; +import java.util.Set; + +@javax.annotation.processing.SupportedAnnotationTypes("com.github.joschi.jadconfig.Parameter") +@javax.annotation.processing.SupportedSourceVersion(SourceVersion.RELEASE_21) +public class ParameterDocumentationValidator extends AbstractProcessor { + + private static final int LINE_LENGTH_WARNING = 120; + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + } + + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + annotations.stream() + .flatMap(annotation -> roundEnv.getElementsAnnotatedWith(annotation).stream()) + .map(element -> (VariableElement) element) + .forEach(this::processField); + return false; // do not claim this annotation, let other processors handle it as well + } + + private void processField(VariableElement field) { + final String fieldName = getFieldName(field); + + final AnnotationMirror parameterAnnotationMirror = getAnnotationMirror(field, Parameter.class) + .orElseThrow(() -> new IllegalStateException("This should not happen, field should be always annotated with @Parameter")); + + final Optional documentationAnnotationMirror = getAnnotationMirror(field, Documentation.class); + + final String parameterName = getParameterValue(parameterAnnotationMirror); + + documentationAnnotationMirror.ifPresentOrElse(documentationAnnotation -> { + final boolean isDocumentationVisible = isDocumentationVisible(documentationAnnotation); + final Optional documentationText = getDocumentationText(documentationAnnotation); + + if (isDocumentationVisible) { + + documentationText.ifPresentOrElse(docs -> { + if (Documentation.MISSING.equals(docs)) { + processingEnv.getMessager().printWarning("Property " + parameterName + " assigned to field " + fieldName + " has TDB documentation, please write proper doc!", field); + } + + final int maxLineLength = maxLineLength(docs); + if (maxLineLength > LINE_LENGTH_WARNING) { + processingEnv.getMessager().printWarning("Property " + parameterName + " assigned to field " + fieldName + " has too long lines (" + maxLineLength + "), please consider splitting it into multiple lines.", field); + } + }, () -> processingEnv.getMessager().printError("Property " + parameterName + " assigned to field " + fieldName + " has no documentation available. Please, add @Documentation annotation value!", field)); + + } + }, () -> processingEnv.getMessager().printError("Property " + parameterName + " assigned to field " + fieldName + " has no documentation available. Please, add @Documentation annotation!", field)); + } + + private int maxLineLength(String docs) { + return Arrays.stream(docs.split("\n")) + .map(String::trim) + .mapToInt(String::length) + .max() + .orElse(0); + } + + private Optional getDocumentationText(AnnotationMirror documentationAnnotationMirror) { + return documentationAnnotationMirror.getElementValues().entrySet().stream() + .filter(entry -> entry.getKey().getSimpleName().toString().equals("value")) + .map(entry -> (String) entry.getValue().getValue()) + .findFirst() + .filter(value -> !StringUtils.isEmpty(value)); + + } + + private static String getParameterValue(AnnotationMirror annotationMirror) { + return annotationMirror.getElementValues().entrySet().stream() + .filter(entry -> entry.getKey().getSimpleName().toString().equals("value")) + .map(entry -> (String) entry.getValue().getValue()) + .findFirst() + .orElseThrow(() -> new IllegalStateException("Value is mandatory!")); + } + + private static boolean isDocumentationVisible(AnnotationMirror annotationMirror) { + return annotationMirror.getElementValues().entrySet().stream() + .filter(entry -> entry.getKey().getSimpleName().toString().equals("visible")) + .map(entry -> (Boolean) entry.getValue().getValue()) + .findFirst() + .orElse(true); + } + + private static Optional getAnnotationMirror(Element element, Class annotationClass) { + return element.getAnnotationMirrors() + .stream() + .filter(mirror -> mirror.getAnnotationType().toString().equals( + annotationClass.getCanonicalName())) + .findFirst(); + } + + private static String getFieldName(VariableElement field) { + final String className = getParentClassName(field); + return className + "#" + field.getSimpleName().toString(); + } + + private static String getParentClassName(VariableElement field) { + final Element enclosingElement = field.getEnclosingElement(); + if (enclosingElement.getKind() == ElementKind.CLASS) { + final TypeElement classElement = (TypeElement) enclosingElement; + return classElement.getQualifiedName().toString(); + } else { + return enclosingElement.getSimpleName().toString(); + } + } +} diff --git a/src/main/java/com/github/joschi/jadconfig/documentation/Documentation.java b/src/main/java/com/github/joschi/jadconfig/documentation/Documentation.java index 2e6995c..fe8eef3 100644 --- a/src/main/java/com/github/joschi/jadconfig/documentation/Documentation.java +++ b/src/main/java/com/github/joschi/jadconfig/documentation/Documentation.java @@ -13,6 +13,9 @@ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Documentation { + + String MISSING = "tbd"; + /** * We don't want to expose some configuration fields to users. They are internal, required for system packages functionality * or deprecated. Set to false if you want to hide this field from documentation. diff --git a/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/src/main/resources/META-INF/services/javax.annotation.processing.Processor index 7218a74..7aa6db8 100644 --- a/src/main/resources/META-INF/services/javax.annotation.processing.Processor +++ b/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -1 +1,2 @@ com.github.joschi.jadconfig.ParameterTypesValidator +com.github.joschi.jadconfig.ParameterDocumentationValidator diff --git a/src/test/java/com/github/joschi/jadconfig/ParameterDocumentationValidatorTest.java b/src/test/java/com/github/joschi/jadconfig/ParameterDocumentationValidatorTest.java new file mode 100644 index 0000000..0911baa --- /dev/null +++ b/src/test/java/com/github/joschi/jadconfig/ParameterDocumentationValidatorTest.java @@ -0,0 +1,31 @@ +package com.github.joschi.jadconfig; + +import io.toolisticon.cute.Cute; +import io.toolisticon.cute.CuteApi; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ParameterDocumentationValidatorTest { + + CuteApi.BlackBoxTestSourceFilesInterface compileTestBuilder; + + @BeforeEach + public void init() { + compileTestBuilder = Cute + .blackBoxTest() + .given() + .processor(ParameterDocumentationValidator.class); + } + + @Test + public void testDocumentationProcessor() { + compileTestBuilder + .andSourceFiles("/com/github/joschi/jadconfig/MyDocumentationValidatorTestClass.java") + .whenCompiled().thenExpectThat() + .compilationFails() + .andThat() + .compilerMessage() + .ofKindError().atLine(18).atColumn(22).equals("Property my_duration assigned to field MyDocumentationValidatorTestClass#myDurationField has no documentation available. Please, add @Documentation annotation!") + .executeTest(); + } +} \ No newline at end of file diff --git a/src/test/resources/com/github/joschi/jadconfig/MyDocumentationValidatorTestClass.java b/src/test/resources/com/github/joschi/jadconfig/MyDocumentationValidatorTestClass.java new file mode 100644 index 0000000..ab2fc91 --- /dev/null +++ b/src/test/resources/com/github/joschi/jadconfig/MyDocumentationValidatorTestClass.java @@ -0,0 +1,20 @@ +import com.github.joschi.jadconfig.Parameter; +import com.github.joschi.jadconfig.documentation.Documentation; + +import java.time.Duration; + +public class MyDocumentationValidatorTestClass { + + @Documentation(visible = false) + @Parameter(value = "my_hidden_property") + private String hiddenProperty; + + @Documentation("configure some int value") + @Parameter(value = "my_int", required = true) + private int myIntField = 10; + + // here's no @Documentation annotation, should lead to an error + @Parameter(value = "my_duration") + private Duration myDurationField = Duration.ofDays(1); + +}