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 extends TypeElement> 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) {
+}