Options/changes so that Jacoco checks don't fail (#104)

- Added new optional Annotation `@RecordBuilderGenerated` - Jacoco ignores
classes with any annotation names "*Generated*" but it needs to be class retained.
For backward compatibility this annotation is not added by default (though it's been
added to `@RecordBuilderFull`). There is a new option to enable it.
- The from with method now uses an internal static class instead of an anonymous
inner class so that the annotation can be on this as well. This new class's name
is configurable in the options.

Thanks to user @madisparn for initial PR and issue report.

Fixes #87
This commit is contained in:
Jordan Zimmerman
2022-04-07 12:21:50 +01:00
committed by GitHub
parent 642dd01421
commit cd059f1207
9 changed files with 204 additions and 50 deletions

View File

@@ -34,6 +34,8 @@
<maven-jar-plugin-version>3.2.0</maven-jar-plugin-version>
<maven-surefire-plugin-version>3.0.0-M5</maven-surefire-plugin-version>
<jacoco-maven-plugin-version>0.8.7</jacoco-maven-plugin-version>
<license-file-path>src/etc/header.txt</license-file-path>
<javapoet-version>1.12.1</javapoet-version>
@@ -326,6 +328,12 @@
<artifactId>maven-gpg-plugin</artifactId>
<version>${maven-gpg-plugin-version}</version>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>${jacoco-maven-plugin-version}</version>
</plugin>
</plugins>
</pluginManagement>

View File

@@ -234,6 +234,18 @@ public @interface RecordBuilder {
* this option does nothing.
*/
String beanClassName() default "";
/**
* If true, generated classes are annotated with {@code RecordBuilderGenerated} which has a retention
* policy of {@code CLASS}. This ensures that analyzers such as Jacoco will ignore the generated class.
*/
boolean addClassRetainedGenerated() default false;
/**
* The {@link #fromMethodName} method instantiates an internal private class. This is the
* name of that class.
*/
String fromWithClassName() default "_FromWith";
}
@Retention(RetentionPolicy.CLASS)

View File

@@ -21,7 +21,8 @@ import java.lang.annotation.*;
interpretNotNulls = true,
useImmutableCollections = true,
addSingleItemCollectionBuilders = true,
addFunctionalMethodsToWith = true
addFunctionalMethodsToWith = true,
addClassRetainedGenerated = true
))
@Retention(RetentionPolicy.SOURCE)
@Target(ElementType.TYPE)

View File

@@ -0,0 +1,30 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.core;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import static java.lang.annotation.ElementType.*;
/**
* Jacoco ignores classes and methods annotated with `*Generated`
*/
@Target({PACKAGE, TYPE, METHOD, CONSTRUCTOR, FIELD, LOCAL_VARIABLE, PARAMETER})
@Retention(RetentionPolicy.CLASS)
public @interface RecordBuilderGenerated {
}

View File

@@ -33,6 +33,7 @@ import static io.soabase.recordbuilder.processor.CollectionBuilderUtils.SingleIt
import static io.soabase.recordbuilder.processor.ElementUtils.getBuilderName;
import static io.soabase.recordbuilder.processor.ElementUtils.getWithMethodName;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.generatedRecordBuilderAnnotation;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.recordBuilderGeneratedAnnotation;
class InternalRecordBuilderProcessor {
private final RecordBuilder.Options metaData;
@@ -69,6 +70,9 @@ class InternalRecordBuilderProcessor {
builder = TypeSpec.classBuilder(builderClassType.name())
.addAnnotation(generatedRecordBuilderAnnotation)
.addTypeVariables(typeVariables);
if (metaData.addClassRetainedGenerated()) {
builder.addAnnotation(recordBuilderGeneratedAnnotation);
}
addVisibility(recordActualPackage.equals(packageName), record.getModifiers());
if (metaData.enableWither()) {
addWithNestedClass();
@@ -158,6 +162,9 @@ class InternalRecordBuilderProcessor {
.addJavadoc("Add withers to {@code $L}\n", recordClassType.name())
.addModifiers(Modifier.PUBLIC)
.addTypeVariables(typeVariables);
if (metaData.addClassRetainedGenerated()) {
classBuilder.addAnnotation(recordBuilderGeneratedAnnotation);
}
recordComponents.forEach(component -> addNestedGetterMethod(classBuilder, component, prefixedName(component, true)));
addWithBuilderMethod(classBuilder);
addWithSuppliedBuilderMethod(classBuilder);
@@ -560,12 +567,22 @@ class InternalRecordBuilderProcessor {
return codeBuilder.build();
}
private void addStaticFromWithMethod() {
/*
Adds static method that returns a "with"er view of an existing record.
private TypeName buildWithTypeName()
{
ClassName rawTypeName = ClassName.get(packageName, builderClassType.name() + "." + metaData.withClassName());
if (typeVariables.isEmpty()) {
return rawTypeName;
}
return ParameterizedTypeName.get(rawTypeName, typeVariables.toArray(new TypeName[]{}));
}
private void addFromWithClass() {
/*
Adds static private class that implements/proxies the Wither
private static final class _FromWith implements MyRecordBuilder.With {
private final MyRecord from;
public static With from(MyRecord from) {
return new MyRecordBuilder.With() {
@Override
public String p1() {
return from.p1();
@@ -575,48 +592,58 @@ class InternalRecordBuilderProcessor {
public String p2() {
return from.p2();
}
};
}
*/
var witherClassNameBuilder = CodeBlock.builder()
.add("$L.$L", builderClassType.name(), metaData.withClassName());
if (!typeVariables.isEmpty()) {
witherClassNameBuilder.add("<");
IntStream.range(0, typeVariables.size()).forEach(index -> {
if (index > 0) {
witherClassNameBuilder.add(", ");
var fromWithClassBuilder = TypeSpec.classBuilder(metaData.fromWithClassName())
.addModifiers(Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL)
.addAnnotation(generatedRecordBuilderAnnotation)
.addTypeVariables(typeVariables)
.addSuperinterface(buildWithTypeName());
if (metaData.addClassRetainedGenerated()) {
fromWithClassBuilder.addAnnotation(recordBuilderGeneratedAnnotation);
}
witherClassNameBuilder.add(typeVariables.get(index).name);
});
witherClassNameBuilder.add(">");
}
var witherClassName = witherClassNameBuilder.build().toString();
var codeBuilder = CodeBlock.builder()
.add("return new $L", witherClassName)
.add("() {\n").indent();
fromWithClassBuilder.addField(recordClassType.typeName(), "from", Modifier.PRIVATE, Modifier.FINAL);
MethodSpec constructorSpec = MethodSpec.constructorBuilder()
.addParameter(recordClassType.typeName(), "from")
.addStatement("this.from = from")
.addModifiers(Modifier.PRIVATE)
.build();
fromWithClassBuilder.addMethod(constructorSpec);
IntStream.range(0, recordComponents.size()).forEach(index -> {
var component = recordComponents.get(index);
if (index > 0) {
codeBuilder.add("\n");
}
codeBuilder.add("@Override\n")
.add("public $T $L() {\n", component.typeName(), prefixedName(component, true))
.indent()
MethodSpec methodSpec = MethodSpec.methodBuilder(prefixedName(component, true))
.returns(component.typeName())
.addAnnotation(Override.class)
.addModifiers(Modifier.PUBLIC)
.addStatement("return from.$L()", component.name())
.unindent()
.add("}\n");
.build();
fromWithClassBuilder.addMethod(methodSpec);
});
codeBuilder.unindent().addStatement("}");
this.builder.addType(fromWithClassBuilder.build());
}
var withType = ClassName.get("", witherClassName);
var methodSpec = MethodSpec.methodBuilder("from")//metaData.copyMethodName())
private void addStaticFromWithMethod() {
/*
Adds static method that returns a "with"er view of an existing record.
public static With from(MyRecord from) {
return new _FromWith(from);
}
*/
addFromWithClass();
var methodSpec = MethodSpec.methodBuilder(metaData.fromMethodName())
.addJavadoc("Return a \"with\"er for an existing record instance\n")
.addModifiers(Modifier.PUBLIC, Modifier.STATIC)
.addAnnotation(generatedRecordBuilderAnnotation)
.addTypeVariables(typeVariables)
.addParameter(recordClassType.typeName(), metaData.fromMethodName())
.returns(withType)
.addCode(codeBuilder.build())
.returns(buildWithTypeName())
.addStatement("return new $L$L(from)", metaData.fromWithClassName(), typeVariables.isEmpty() ? "" : "<>")
.build();
builder.addMethod(methodSpec);
}

View File

@@ -33,6 +33,7 @@ import java.util.stream.Collectors;
import static io.soabase.recordbuilder.processor.ElementUtils.getBuilderName;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.generatedRecordInterfaceAnnotation;
import static io.soabase.recordbuilder.processor.RecordBuilderProcessor.recordBuilderGeneratedAnnotation;
class InternalRecordInterfaceProcessor {
private final ProcessingEnvironment processingEnv;
@@ -68,6 +69,9 @@ class InternalRecordInterfaceProcessor {
.addModifiers(Modifier.PUBLIC)
.addAnnotation(generatedRecordInterfaceAnnotation)
.addTypeVariables(typeVariables);
if (metaData.addClassRetainedGenerated()) {
builder.addAnnotation(recordBuilderGeneratedAnnotation);
}
if (addRecordBuilder) {
ClassType builderClassType = ElementUtils.getClassType(packageName, getBuilderName(iface, metaData, recordClassType, metaData.suffix()) + "." + metaData.withClassName(), iface.getTypeParameters());

View File

@@ -19,6 +19,7 @@ import com.squareup.javapoet.AnnotationSpec;
import com.squareup.javapoet.JavaFile;
import com.squareup.javapoet.TypeSpec;
import io.soabase.recordbuilder.core.RecordBuilder;
import io.soabase.recordbuilder.core.RecordBuilderGenerated;
import io.soabase.recordbuilder.core.RecordInterface;
import javax.annotation.processing.AbstractProcessor;
@@ -46,6 +47,7 @@ public class RecordBuilderProcessor
static final AnnotationSpec generatedRecordBuilderAnnotation = AnnotationSpec.builder(Generated.class).addMember("value", "$S", RecordBuilder.class.getName()).build();
static final AnnotationSpec generatedRecordInterfaceAnnotation = AnnotationSpec.builder(Generated.class).addMember("value", "$S", RecordInterface.class.getName()).build();
static final AnnotationSpec recordBuilderGeneratedAnnotation = AnnotationSpec.builder(RecordBuilderGenerated.class).build();
@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

View File

@@ -66,6 +66,48 @@
<skip>true</skip>
</configuration>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<executions>
<execution>
<id>default-prepare-agent</id>
<goals>
<goal>prepare-agent</goal>
</goals>
</execution>
<execution>
<id>default-report</id>
<goals>
<goal>report</goal>
</goals>
</execution>
<execution>
<id>default-check</id>
<goals>
<goal>check</goal>
</goals>
<configuration>
<includes>
<include>io/soabase/recordbuilder/test/jacoco/*</include>
</includes>
<rules>
<rule>
<element>BUNDLE</element>
<limits>
<limit>
<counter>COMPLEXITY</counter>
<value>COVEREDRATIO</value>
<minimum>0.60</minimum>
</limit>
</limits>
</rule>
</rules>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -0,0 +1,28 @@
/**
* Copyright 2019 Jordan Zimmerman
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package io.soabase.recordbuilder.test.jacoco;
import io.soabase.recordbuilder.core.RecordBuilderFull;
import io.soabase.recordbuilder.core.RecordBuilderGenerated;
import javax.validation.constraints.NotNull;
import java.util.List;
import java.util.Map;
@RecordBuilderFull
@RecordBuilderGenerated
public record FullRecordForJacoco(@NotNull List<Number> numbers, @NotNull Map<Number, FullRecordForJacoco> fullRecords, @NotNull String justAString) {
}