diff --git a/pom.xml b/pom.xml index 06ad918..e0c3c90 100644 --- a/pom.xml +++ b/pom.xml @@ -34,6 +34,8 @@ 3.2.0 3.0.0-M5 + 0.8.7 + src/etc/header.txt 1.12.1 @@ -326,6 +328,12 @@ maven-gpg-plugin ${maven-gpg-plugin-version} + + + org.jacoco + jacoco-maven-plugin + ${jacoco-maven-plugin-version} + diff --git a/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilder.java b/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilder.java index 668804b..372e05c 100644 --- a/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilder.java +++ b/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilder.java @@ -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) diff --git a/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilderFull.java b/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilderFull.java index df03d36..42923e5 100644 --- a/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilderFull.java +++ b/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilderFull.java @@ -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) diff --git a/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilderGenerated.java b/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilderGenerated.java new file mode 100644 index 0000000..a870eb9 --- /dev/null +++ b/record-builder-core/src/main/java/io/soabase/recordbuilder/core/RecordBuilderGenerated.java @@ -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 { +} diff --git a/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InternalRecordBuilderProcessor.java b/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InternalRecordBuilderProcessor.java index 434e020..dbda7e3 100644 --- a/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InternalRecordBuilderProcessor.java +++ b/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InternalRecordBuilderProcessor.java @@ -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,63 +567,83 @@ class InternalRecordBuilderProcessor { return codeBuilder.build(); } + 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; + + @Override + public String p1() { + return from.p1(); + } + + @Override + public String p2() { + return from.p2(); + } + } + */ + + 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); + } + + 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); + MethodSpec methodSpec = MethodSpec.methodBuilder(prefixedName(component, true)) + .returns(component.typeName()) + .addAnnotation(Override.class) + .addModifiers(Modifier.PUBLIC) + .addStatement("return from.$L()", component.name()) + .build(); + fromWithClassBuilder.addMethod(methodSpec); + }); + this.builder.addType(fromWithClassBuilder.build()); + } + private void addStaticFromWithMethod() { /* Adds static method that returns a "with"er view of an existing record. public static With from(MyRecord from) { - return new MyRecordBuilder.With() { - @Override - public String p1() { - return from.p1(); - } - - @Override - public String p2() { - return from.p2(); - } - }; + return new _FromWith(from); } */ - 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(", "); - } - 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(); - 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() - .addStatement("return from.$L()", component.name()) - .unindent() - .add("}\n"); - }); - codeBuilder.unindent().addStatement("}"); - var withType = ClassName.get("", witherClassName); - var methodSpec = MethodSpec.methodBuilder("from")//metaData.copyMethodName()) + 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); } @@ -951,13 +978,13 @@ class InternalRecordBuilderProcessor { } var type = optionalType.get(); var methodSpec = MethodSpec.methodBuilder(prefixedName(component, false)) - .addModifiers(Modifier.PUBLIC) - .addAnnotation(generatedRecordBuilderAnnotation) - .returns(builderClassType.typeName()); + .addModifiers(Modifier.PUBLIC) + .addAnnotation(generatedRecordBuilderAnnotation) + .returns(builderClassType.typeName()); var parameterSpecBuilder = ParameterSpec.builder(type.valueType(), component.name()); methodSpec.addJavadoc("Set a new value for the {@code $L} record component in the builder\n", component.name()) - .addStatement("this.$L = $T.of($L)", component.name(), type.typeName(), component.name()); + .addStatement("this.$L = $T.of($L)", component.name(), type.typeName(), component.name()); addConstructorAnnotations(component, parameterSpecBuilder); methodSpec.addStatement("return this").addParameter(parameterSpecBuilder.build()); builder.addMethod(methodSpec.build()); @@ -1030,7 +1057,7 @@ class InternalRecordBuilderProcessor { private String prefixedName(RecordClassType component, boolean isGetter) { BiFunction prefixer = (p, s) -> p.isEmpty() - ? s : p + Character.toUpperCase(s.charAt(0)) + s.substring(1); + ? s : p + Character.toUpperCase(s.charAt(0)) + s.substring(1); boolean isBool = component.typeName().toString().toLowerCase(Locale.ROOT).equals("boolean"); if (isGetter) { if (isBool) { diff --git a/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InternalRecordInterfaceProcessor.java b/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InternalRecordInterfaceProcessor.java index 9b7ba71..9b2288f 100644 --- a/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InternalRecordInterfaceProcessor.java +++ b/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/InternalRecordInterfaceProcessor.java @@ -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()); diff --git a/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/RecordBuilderProcessor.java b/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/RecordBuilderProcessor.java index 520502f..0347c05 100644 --- a/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/RecordBuilderProcessor.java +++ b/record-builder-processor/src/main/java/io/soabase/recordbuilder/processor/RecordBuilderProcessor.java @@ -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 annotations, RoundEnvironment roundEnv) { diff --git a/record-builder-test/pom.xml b/record-builder-test/pom.xml index af9bf10..6abeded 100644 --- a/record-builder-test/pom.xml +++ b/record-builder-test/pom.xml @@ -66,6 +66,48 @@ true + + + org.jacoco + jacoco-maven-plugin + + + default-prepare-agent + + prepare-agent + + + + default-report + + report + + + + default-check + + check + + + + io/soabase/recordbuilder/test/jacoco/* + + + + BUNDLE + + + COMPLEXITY + COVEREDRATIO + 0.60 + + + + + + + + diff --git a/record-builder-test/src/main/java/io/soabase/recordbuilder/test/jacoco/FullRecordForJacoco.java b/record-builder-test/src/main/java/io/soabase/recordbuilder/test/jacoco/FullRecordForJacoco.java new file mode 100644 index 0000000..e845d0e --- /dev/null +++ b/record-builder-test/src/main/java/io/soabase/recordbuilder/test/jacoco/FullRecordForJacoco.java @@ -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 numbers, @NotNull Map fullRecords, @NotNull String justAString) { +}