diff --git a/base-validating-builders/build.gradle b/base-validating-builders/build.gradle index 1b001b0b2b..78a805f3ea 100644 --- a/base-validating-builders/build.gradle +++ b/base-validating-builders/build.gradle @@ -92,20 +92,17 @@ dependencies { } modelCompiler { - mainProtoSrcDir = "$projectDir/../base/src/main/proto" + generateValidatingBuilders = false } sourceSets { main.java.srcDirs "$projectDir/generated/main/spine" - main.proto.srcDirs = [modelCompiler.mainProtoSrcDir] + main.proto.srcDirs = ["$projectDir/../base/src/main/proto"] } protobuf { generateProtoTasks { all().each { - it.builtins { - remove java - } it.plugins { remove grpc } @@ -113,14 +110,14 @@ protobuf { } } -ext.buildersDir = "$projectDir/builders" +ext.compiledProtoDir = "$projectDir/compiled-proto" task copyCompiledClasses(type: Copy) { from sourceSets.main.java.outputDir - into buildersDir + into compiledProtoDir include { - it.isDirectory() || it.name.endsWith('VBuilder.class') + it.isDirectory() || it.name.endsWith('.class') } dependsOn compileJava @@ -142,11 +139,11 @@ build.doLast { } task cleanGenerated(type: Delete) { - delete files("$projectDir/generated", "$projectDir/build", "$projectDir/.spine", buildersDir) + delete files("$projectDir/generated", "$projectDir/build", "$projectDir/.spine", compiledProtoDir) } clean.dependsOn cleanGenerated idea.module { - generatedSourceDirs += buildersDir + generatedSourceDirs += compiledProtoDir } diff --git a/base/build.gradle b/base/build.gradle index a3b3b9b7bc..6678ccf35d 100644 --- a/base/build.gradle +++ b/base/build.gradle @@ -53,7 +53,7 @@ sourceSets { jar { // See `base-validating-builders/README.md`. - from "$rootDir/base-validating-builders/builders" + from "$rootDir/base-validating-builders/compiled-proto" } build.doLast { diff --git a/base/src/main/java/io/spine/code/proto/FieldDeclaration.java b/base/src/main/java/io/spine/code/proto/FieldDeclaration.java index ce0e7d1ed3..28f6585767 100644 --- a/base/src/main/java/io/spine/code/proto/FieldDeclaration.java +++ b/base/src/main/java/io/spine/code/proto/FieldDeclaration.java @@ -21,11 +21,12 @@ package io.spine.code.proto; import com.google.common.base.Joiner; -import com.google.protobuf.DescriptorProtos.DescriptorProto; +import com.google.common.base.Objects; import com.google.protobuf.DescriptorProtos.FieldDescriptorProto; import com.google.protobuf.Descriptors.FieldDescriptor; import com.google.protobuf.Descriptors.FieldDescriptor.JavaType; import com.google.protobuf.Descriptors.FileDescriptor; +import com.google.protobuf.Message; import io.spine.base.MessageFile; import io.spine.code.java.ClassName; import io.spine.logging.Logging; @@ -37,11 +38,13 @@ import io.spine.type.TypeName; import io.spine.type.TypeUrl; import io.spine.type.UnknownTypeException; +import io.spine.validate.Validate; import java.util.List; import java.util.Optional; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.protobuf.DescriptorProtos.DescriptorProto.FIELD_FIELD_NUMBER; import static com.google.protobuf.Descriptors.FieldDescriptor.Type.ENUM; import static com.google.protobuf.Descriptors.FieldDescriptor.Type.MESSAGE; import static com.google.protobuf.Descriptors.FieldDescriptor.Type.STRING; @@ -97,6 +100,34 @@ public MessageType declaringType() { return declaringMessage; } + /** + * Checks if the given value is the default value for this field. + * + * @param fieldValue + * the value of the field + * @return {@code true} if the given value is default for this field, {@code false} otherwise + */ + public boolean isDefault(Object fieldValue) { + checkNotNull(fieldValue); + if (isMessage()) { + if (fieldValue instanceof Message) { + Message message = (Message) fieldValue; + return Validate.isDefault(message) && sameMessageType(message); + } else { + return false; + } + } else { + return fieldValue.equals(field.getDefaultValue()); + } + } + + private boolean sameMessageType(Message msg) { + String messageClassName = msg.getClass() + .getName(); + String fieldClassName = messageClassName(); + return fieldClassName.equals(messageClassName); + } + /** * Obtains fully-qualified name of the Java class that corresponds to the declared type * of the field. @@ -247,11 +278,6 @@ public FieldDeclaration valueDeclaration() { return new FieldDeclaration(valueDescriptor); } - /** Returns the name of the type of this field. */ - public String typeName(){ - return field.getType().name(); - } - private boolean isEntityField() { EntityOption entityOption = field.getContainingType() .getOptions() @@ -304,17 +330,35 @@ public Optional leadingComments() { private LocationPath fieldPath() { LocationPath locationPath = new LocationPath(); locationPath.addAll(declaringMessage.path()); - locationPath.add(DescriptorProto.FIELD_FIELD_NUMBER); + locationPath.add(FIELD_FIELD_NUMBER); int fieldIndex = fieldIndex(); locationPath.add(fieldIndex); return locationPath; } private int fieldIndex() { - FieldDescriptorProto fproto = this.field.toProto(); + FieldDescriptorProto proto = this.field.toProto(); return declaringMessage.descriptor() .toProto() .getFieldList() - .indexOf(fproto); + .indexOf(proto); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof FieldDeclaration)) { + return false; + } + FieldDeclaration that = (FieldDeclaration) o; + return Objects.equal(declaringMessage, that.declaringMessage) && + Objects.equal(field.getFullName(), that.field.getFullName()); + } + + @Override + public int hashCode() { + return Objects.hashCode(declaringMessage, field.getFullName()); } } diff --git a/base/src/main/java/io/spine/protobuf/Diff.java b/base/src/main/java/io/spine/protobuf/Diff.java new file mode 100644 index 0000000000..9ac1756bf3 --- /dev/null +++ b/base/src/main/java/io/spine/protobuf/Diff.java @@ -0,0 +1,136 @@ +/* + * Copyright 2019, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.protobuf; + +import com.google.common.base.Objects; +import com.google.common.collect.ImmutableSet; +import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.Message; +import io.spine.annotation.Internal; +import io.spine.code.proto.FieldDeclaration; + +import java.util.Map; +import java.util.Set; + +import static com.google.common.base.Preconditions.checkArgument; +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.google.common.collect.Sets.symmetricDifference; +import static java.util.stream.Collectors.toSet; + +/** + * Difference between two messages of the same type. + * + *

For two messages {@code A} and {@code B}, their diff includes all the fields which are present + * in {@code A} and not in {@code B}, all the fields which are present in {@code B} and not in + * {@code A}, and all the fields which are present in both messages but have different values. + * + * @see com.google.common.collect.Sets#symmetricDifference(Set, Set) Sets.symmetricDifference(..) + */ +@Internal +public final class Diff { + + private final ImmutableSet fields; + + private Diff(ImmutableSet fields) { + this.fields = fields; + } + + /** + * Calculates the difference between the given two messages. + * + * @param a + * one message + * @param b + * the other message + * @param + * the type of the messages + * @return difference between the messages + * @throws IllegalArgumentException + * if the types of the messages are not the same + */ + public static Diff between(M a, M b) { + checkNotNull(a); + checkNotNull(b); + checkArgument(a.getClass().equals(b.getClass())); + ImmutableSet fields = + symmetricDifference(decompose(a), decompose(b)) + .stream() + .map(tuple -> tuple.declaration) + .collect(toImmutableSet()); + return new Diff(fields); + } + + private static Set decompose(Message message) { + Map fieldMap = message.getAllFields(); + return fieldMap + .entrySet() + .stream() + .map(entry -> new FieldTuple( + new FieldDeclaration(entry.getKey()), entry.getValue() + )) + .collect(toSet()); + } + + /** + * Checks if the given field is present in the diff or not. + * + * @param field + * the field declaration to find + * @return {@code true} if the field has different values in the two given messages, + * {@code false} otherwise + */ + public boolean contains(FieldDeclaration field) { + return fields.contains(field); + } + + /** + * A field declaration and a value of that field. + */ + private static final class FieldTuple { + + private final FieldDeclaration declaration; + private final Object value; + + private FieldTuple(FieldDeclaration declaration, Object value) { + this.declaration = checkNotNull(declaration); + this.value = checkNotNull(value); + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (!(o instanceof FieldTuple)) { + return false; + } + FieldTuple tuple = (FieldTuple) o; + return Objects.equal(declaration, tuple.declaration) && + Objects.equal(value, tuple.value); + } + + @Override + public int hashCode() { + return Objects.hashCode(declaration, value); + } + } +} diff --git a/base/src/main/java/io/spine/protobuf/ValidatingBuilder.java b/base/src/main/java/io/spine/protobuf/ValidatingBuilder.java new file mode 100644 index 0000000000..3e5c270ec7 --- /dev/null +++ b/base/src/main/java/io/spine/protobuf/ValidatingBuilder.java @@ -0,0 +1,77 @@ +/* + * Copyright 2019, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.protobuf; + +import com.google.protobuf.Message; +import io.spine.annotation.GeneratedMixin; +import io.spine.validate.NotValidated; +import io.spine.validate.Validate; +import io.spine.validate.Validated; +import io.spine.validate.ValidationException; + +/** + * Implementation base for generated message builders. + * + *

This interface defines a default method {@link #vBuild()} which validates the built message + * before returning it to the user. In most cases, the users should use {@code vBuild()} and not + * the {@code build()}. If a user specifically needs to skip validation, they should use + * {@link #buildPartial()} to make the intent explicit. + * + * @param + * the type of the message to build + */ +@GeneratedMixin +public interface ValidatingBuilder extends Message.Builder { + + /** + * Constructs the message with the given fields. + * + *

Users should not call this method directly. Instead, call {@link #vBuild()} for + * a validated message or {@link #buildPartial()} to skip message validation. + */ + @Override + @NotValidated M build(); + + /** + * Constructs the message with the given fields without validation. + * + *

Users should prefer {@link #vBuild()} over this method. However, in cases, when validation + * is not required, call this method instead of {@link #build()}. + * + * @return the build message, potentially invalid + */ + @Override + @NotValidated M buildPartial(); + + /** + * Constructs the message and {@linkplain Validate validates} it according to the constraints + * declared in Protobuf. + * + * @return the built message + * @throws ValidationException + * if the message is invalid + */ + default @Validated M vBuild() throws ValidationException { + M message = build(); + Validate.checkValid(message); + return message; + } +} diff --git a/base/src/main/java/io/spine/type/MessageType.java b/base/src/main/java/io/spine/type/MessageType.java index b005cb1028..f3fa472854 100644 --- a/base/src/main/java/io/spine/type/MessageType.java +++ b/base/src/main/java/io/spine/type/MessageType.java @@ -283,7 +283,8 @@ public Optional leadingComments(LocationPath locationPath) { .toProto(); if (!file.hasSourceCodeInfo()) { _warn("Unable to obtain proto source code info. " + - "Please configure the Gradle Protobuf plugin as follows:%n%s", + "Please configure the Gradle Protobuf plugin as follows:{}{}", + System.lineSeparator(), "`task.descriptorSetOptions.includeSourceInfo = true`."); return Optional.empty(); } diff --git a/base/src/main/java/io/spine/validate/NotValidated.java b/base/src/main/java/io/spine/validate/NotValidated.java new file mode 100644 index 0000000000..820f6b2ec9 --- /dev/null +++ b/base/src/main/java/io/spine/validate/NotValidated.java @@ -0,0 +1,45 @@ +/* + * Copyright 2019, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.validate; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE_PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.CLASS; + +/** + * Marks a message which may not be valid. + * + *

By default, all the messages should be validated. In some cases, users may choose not to + * validate certain parts of model at a certain point. For example, to group them into a bigger + * message which is going to be validated later. + * + * @see io.spine.protobuf.ValidatingBuilder + * @see Validated + */ +@Documented +@Retention(CLASS) +@Target({TYPE_USE, TYPE_PARAMETER}) +public @interface NotValidated { +} diff --git a/base/src/main/java/io/spine/validate/Validate.java b/base/src/main/java/io/spine/validate/Validate.java index c89b531e96..47ac4706ac 100644 --- a/base/src/main/java/io/spine/validate/Validate.java +++ b/base/src/main/java/io/spine/validate/Validate.java @@ -20,15 +20,24 @@ package io.spine.validate; +import com.google.common.collect.ImmutableSet; import com.google.errorprone.annotations.CanIgnoreReturnValue; import com.google.protobuf.Message; +import io.spine.code.proto.FieldDeclaration; +import io.spine.code.proto.FieldName; +import io.spine.logging.Logging; +import io.spine.protobuf.Diff; import io.spine.type.TypeName; import org.checkerframework.checker.nullness.qual.Nullable; +import org.slf4j.Logger; import java.util.List; +import java.util.Optional; +import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import static com.google.common.collect.ImmutableSet.toImmutableSet; import static io.spine.util.Exceptions.newIllegalArgumentException; import static io.spine.util.Exceptions.newIllegalStateException; @@ -37,8 +46,6 @@ */ public final class Validate { - private static final String MUST_BE_A_POSITIVE_VALUE = "%s must be a positive value"; - /** Prevents instantiation of this utility class. */ private Validate() { } @@ -170,25 +177,6 @@ public static M checkDefault(M object) { return object; } - /** - * Ensures the truth of an expression involving one parameter to the calling method. - * - * @param expression a boolean expression with the parameter we check - * @param errorMessageFormat the format of the error message, which has {@code %s} placeholder - * for the parameter name - * @param parameterName the name of the parameter - * @throws IllegalArgumentException if {@code expression} is false - */ - public static void checkParameter(boolean expression, - String errorMessageFormat, - String parameterName) { - checkNotNull(errorMessageFormat); - checkNotNull(parameterName); - if (!expression) { - throw newIllegalArgumentException(errorMessageFormat, parameterName); - } - } - /** * Ensures that the passed string is not {@code null}, empty or blank string. * @@ -201,13 +189,9 @@ public static void checkParameter(boolean expression, public static String checkNotEmptyOrBlank(String stringToCheck, String fieldName) { checkNotNull(stringToCheck); checkNotNull(fieldName); - checkParameter(!stringToCheck.isEmpty(), - "Field %s must not be an empty string.", fieldName - ); + checkArgument(!stringToCheck.isEmpty(), "Field %s must not be an empty string.", fieldName); String trimmed = stringToCheck.trim(); - checkParameter(trimmed.length() > 0, - "Field %s must not be a blank string.", fieldName - ); + checkArgument(trimmed.length() > 0, "Field %s must not be a blank string.", fieldName); return stringToCheck; } @@ -232,7 +216,7 @@ public static void checkPositive(long value) { */ public static void checkPositive(long value, String argumentName) { checkNotNull(argumentName); - checkParameter(value > 0L, MUST_BE_A_POSITIVE_VALUE, argumentName); + checkArgument(value > 0L, "%s must be a positive value", argumentName); } /** @@ -255,18 +239,6 @@ private static boolean isBetween(int value, int lowBound, int highBound) { return lowBound <= value && value <= highBound; } - /** - * Ensures that the passed name is not empty or blank. - * - * @param name the name to check - * @return the passed value - * @throws IllegalArgumentException if the ID string value is empty or blank - */ - @SuppressWarnings("DuplicateStringLiteralInspection") // is OK for this popular field name value - public static String checkNameNotEmptyOrBlank(String name) { - return checkNotEmptyOrBlank(name, "name"); - } - /** * Validates the given message according to its definition and throws * {@code ValidationException} if any constraints are violated. @@ -283,4 +255,107 @@ public static void checkValid(Message message) throws ValidationException { throw new ValidationException(violations); } } + + /** + * Checks that when transitioning a message state from {@code previous} to {@code current}, + * the {@code set_once} constrains are met. + * + * @param previous + * the previous state of the message + * @param current + * the new state of the message + * @param + * the type of the message + */ + public static void checkValidChange(M previous, M current) { + checkNotNull(previous); + checkNotNull(current); + + Diff diff = Diff.between(previous, current); + ImmutableSet setOnceViolations = current + .getDescriptorForType() + .getFields() + .stream() + .map(FieldDeclaration::new) + .filter(Validate::isNonOverridable) + .filter(diff::contains) + .filter(field -> { + Object fieldValue = previous.getField(field.descriptor()); + return !field.isDefault(fieldValue); + }) + .map(Validate::violatedSetOnce) + .collect(toImmutableSet()); + if (!setOnceViolations.isEmpty()) { + throw new ValidationException(setOnceViolations); + } + } + + /** + * Checks if the given field, once set, may not be changed. + * + *

This property is defined by the {@code (set_once)} option. If the option is set to + * {@code true} on a non-{@code repeated} and non-{@code map} field, this field is + * non-overridable. + * + *

Logs if the option is set but the field is {@code repeated} or a {@code map}. + * + * @param field + * the field to check + * @return {@code true} if the field is neither {@code repeated} nor {@code map} and is + * {@code (set_once)} + */ + private static boolean isNonOverridable(FieldDeclaration field) { + checkNotNull(field); + + boolean marked = markedSetOnce(field); + if (marked) { + boolean setOnceInapplicable = field.isCollection(); + if (setOnceInapplicable) { + onSetOnceMisuse(field); + return false; + } else { + return true; + } + } else { + return false; + } + } + + private static boolean markedSetOnce(FieldDeclaration declaration) { + Optional setOnceDeclaration = SetOnce.from(declaration.descriptor()); + boolean setOnceValue = setOnceDeclaration.orElse(false); + boolean requiredByDefault = declaration.isEntityId() + && !setOnceDeclaration.isPresent(); + return setOnceValue || requiredByDefault; + } + + @SuppressWarnings("DuplicateStringLiteralInspection") + // Usage in AbstractValidatingBuilder will be removed. + private static void onSetOnceMisuse(FieldDeclaration field) { + Logger logger = Logging.get(Validate.class); + FieldName fieldName = field.name(); + logger.error("Error found in `{}`. " + + "Repeated and map fields cannot be marked as `(set_once) = true`.", + fieldName); + } + + @SuppressWarnings("DuplicateStringLiteralInspection") + // Usage in AbstractValidatingBuilder will be removed. + private static ConstraintViolation violatedSetOnce(FieldDeclaration declaration) { + TypeName declaringTypeName = declaration.declaringType().name(); + FieldName fieldName = declaration.name(); + ConstraintViolation violation = ConstraintViolation + .newBuilder() + .setMsgFormat("Attempted to change the value of the field `%s.%s` which has " + + "`(set_once) = true` and is already set.") + .addParam(declaringTypeName.value()) + .addParam(fieldName.value()) + .setFieldPath(declaration.name() + .asPath()) + .setTypeName(declaration.declaringType() + .name() + .value()) + .build(); + return violation; + } } diff --git a/base/src/main/java/io/spine/validate/Validated.java b/base/src/main/java/io/spine/validate/Validated.java new file mode 100644 index 0000000000..e28cf0c1cd --- /dev/null +++ b/base/src/main/java/io/spine/validate/Validated.java @@ -0,0 +1,46 @@ +/* + * Copyright 2019, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.validate; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +import static java.lang.annotation.ElementType.TYPE_PARAMETER; +import static java.lang.annotation.ElementType.TYPE_USE; +import static java.lang.annotation.RetentionPolicy.CLASS; + +/** + * Marks a message which is guaranteed to be valid. + * + *

In most cases this annotation is implied. However, sometimes users may want to state + * explicitly that the message is validated. For example, if a method marked as {@link NotValidated} + * is overridden with a version which returns only valid messages, that version should be marked + * with this annotation. + * + * @see io.spine.protobuf.ValidatingBuilder + * @see NotValidated + */ +@Documented +@Retention(CLASS) +@Target({TYPE_USE, TYPE_PARAMETER}) +public @interface Validated { +} diff --git a/base/src/test/java/io/spine/code/proto/FieldDeclarationTest.java b/base/src/test/java/io/spine/code/proto/FieldDeclarationTest.java new file mode 100644 index 0000000000..8a230b5d85 --- /dev/null +++ b/base/src/test/java/io/spine/code/proto/FieldDeclarationTest.java @@ -0,0 +1,170 @@ +/* + * Copyright 2019, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.code.proto; + +import com.google.common.testing.EqualsTester; +import com.google.common.testing.NullPointerTester; +import com.google.protobuf.Any; +import com.google.protobuf.ByteString; +import com.google.protobuf.BytesValue; +import com.google.protobuf.Descriptors.Descriptor; +import com.google.protobuf.Descriptors.FieldDescriptor; +import com.google.protobuf.Empty; +import com.google.protobuf.Int32Value; +import com.google.protobuf.Int64Value; +import com.google.protobuf.StringValue; +import io.spine.net.Uri; +import io.spine.net.Uri.Protocol; +import io.spine.type.MessageType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DisplayName("FieldDeclaration should") +class FieldDeclarationTest { + + @Test + @DisplayName("not accept nulls on construction") + void notAcceptNullsOnCtor() { + Descriptor descriptor = Any.getDescriptor(); + NullPointerTester tester = new NullPointerTester() + .setDefault(MessageType.class, new MessageType(descriptor)) + .setDefault(FieldDescriptor.class, descriptor.getFields() + .get(0)); + tester.testAllPublicConstructors(FieldDeclaration.class); + } + + @Test + @DisplayName("not accept nulls") + void notAcceptNulls() { + Descriptor descriptor = Any.getDescriptor(); + FieldDescriptor field = descriptor.getFields() + .get(0); + new NullPointerTester() + .testAllPublicInstanceMethods(new FieldDeclaration(field)); + } + + @Test + @DisplayName("have equals() and hashCode()") + void equalsAndHashCode() { + Descriptor any = Any.getDescriptor(); + FieldDescriptor typeUrl = any.getFields() + .get(0); + FieldDescriptor bytes = any.getFields() + .get(1); + new EqualsTester() + .addEqualityGroup(new FieldDeclaration(typeUrl), + new FieldDeclaration(typeUrl, new MessageType(any))) + .addEqualityGroup(new FieldDeclaration(bytes), + new FieldDeclaration(bytes, new MessageType(any))) + .testEquals(); + } + + @Nested + @DisplayName("check default values of") + class Defaults { + + @Test + @DisplayName("int32") + void anInt32() { + FieldDescriptor int32Field = Int32Value.getDescriptor() + .getFields() + .get(0); + FieldDeclaration declaration = new FieldDeclaration(int32Field); + assertTrue(declaration.isDefault(0)); + assertFalse(declaration.isDefault("")); + assertFalse(declaration.isDefault(0.0)); + } + + @Test + @DisplayName("string") + void aString() { + FieldDescriptor stringField = StringValue.getDescriptor() + .getFields() + .get(0); + FieldDeclaration declaration = new FieldDeclaration(stringField); + assertTrue(declaration.isDefault("")); + assertFalse(declaration.isDefault(0L)); + } + + @Test + @DisplayName("message") + void aMessage() { + FieldDescriptor messageField = Uri.getDescriptor() + .findFieldByName("auth"); + FieldDeclaration declaration = new FieldDeclaration(messageField); + assertTrue(declaration.isDefault(Uri.Authorization.getDefaultInstance())); + assertFalse(declaration.isDefault(0L)); + assertFalse(declaration.isDefault(Empty.getDefaultInstance())); + } + } + + @Nested + @DisplayName("obtain Java type name of") + class TypeName { + + @Test + @DisplayName("int64") + void int64() { + FieldDescriptor int64Field = Int64Value.getDescriptor() + .getFields() + .get(0); + FieldDeclaration declaration = new FieldDeclaration(int64Field); + String typeName = declaration.javaTypeName(); + assertThat(typeName).isEqualTo(long.class.getName()); + } + + @Test + @DisplayName("bytes") + void bytes() { + FieldDescriptor int64Field = BytesValue.getDescriptor() + .getFields() + .get(0); + FieldDeclaration declaration = new FieldDeclaration(int64Field); + String typeName = declaration.javaTypeName(); + assertThat(typeName).isEqualTo(ByteString.class.getName()); + } + + @Test + @DisplayName("message") + void message() { + FieldDescriptor messageField = Uri.getDescriptor() + .findFieldByName("protocol"); + FieldDeclaration declaration = new FieldDeclaration(messageField); + String typeName = declaration.javaTypeName(); + assertThat(typeName).isEqualTo(Protocol.class.getName()); + } + + @Test + @DisplayName("enum") + void anEnum() { + FieldDescriptor enumField = Uri.Protocol.getDescriptor() + .findFieldByName("schema"); + FieldDeclaration declaration = new FieldDeclaration(enumField); + String typeName = declaration.javaTypeName(); + assertThat(typeName).isEqualTo(Uri.Schema.class.getName()); + } + } +} diff --git a/base/src/test/java/io/spine/validate/ValidateTest.java b/base/src/test/java/io/spine/validate/ValidateTest.java index 8b5f63d6af..7e41913223 100644 --- a/base/src/test/java/io/spine/validate/ValidateTest.java +++ b/base/src/test/java/io/spine/validate/ValidateTest.java @@ -22,13 +22,22 @@ import com.google.protobuf.Message; import com.google.protobuf.StringValue; +import io.spine.base.FieldPaths; +import io.spine.net.Url; +import io.spine.people.PersonName; +import io.spine.test.validate.Passport; import io.spine.testing.Tests; import io.spine.testing.UtilityClassTest; +import io.spine.testing.logging.MuteLogging; import io.spine.type.TypeName; import io.spine.validate.diags.ViolationText; import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; +import java.util.List; + +import static com.google.common.truth.Truth.assertThat; import static io.spine.protobuf.TypeConverter.toMessage; import static io.spine.testing.TestValues.newUuidValue; import static io.spine.validate.Validate.checkBounds; @@ -36,6 +45,7 @@ import static io.spine.validate.Validate.checkNotDefault; import static io.spine.validate.Validate.checkNotEmptyOrBlank; import static io.spine.validate.Validate.checkPositive; +import static io.spine.validate.Validate.checkValidChange; import static io.spine.validate.Validate.isDefault; import static io.spine.validate.Validate.isNotDefault; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -52,35 +62,35 @@ class ValidateTest extends UtilityClassTest { @Test @DisplayName("not consider zero as a positive") - void check_positive_if_zero() { + void checkPositiveIfZero() { assertThrows(IllegalArgumentException.class, () -> checkPositive(0)); } @Test @DisplayName("throw if not a positive") - void check_positive_if_negative() { + void checkPositiveIfNegative() { assertThrows(IllegalArgumentException.class, () -> checkPositive(-1)); } @Test @DisplayName("throw if not positive and display a message") - void check_positive_with_message() { + void checkPositiveWithMessage() { assertThrows(IllegalArgumentException.class, () -> checkPositive(-1, "negativeInteger")); } @Test @DisplayName("throw if long value is not positive") - void throw_exception_if_long_value_is_not_positive() { + void throwExceptionIfLongValueIsNotPositive() { assertThrows(IllegalArgumentException.class, () -> checkPositive(-2L, "negativeLong")); } @Test @DisplayName("verify that message is not in default state") - void verify_that_message_is_not_in_default_state() { + void verifyThatMessageIsNotInDefaultState() { Message msg = toMessage("check_if_message_is_not_in_default_state"); assertTrue(isNotDefault(msg)); @@ -89,14 +99,14 @@ void verify_that_message_is_not_in_default_state() { @Test @DisplayName("throw if checked value out of bounds") - void throw_exception_if_checked_value_out_of_bounds() { + void throwExceptionIfCheckedValueOutOfBounds() { assertThrows(IllegalArgumentException.class, () -> checkBounds(10, "checked value", -5, 9)); } @Test @DisplayName("verify that message is in default state") - void verify_that_message_is_in_default_state() { + void verifyThatMessageIsInDefaultState() { Message nonDefault = newUuidValue(); assertTrue(isDefault(StringValue.getDefaultInstance())); @@ -105,7 +115,7 @@ void verify_that_message_is_in_default_state() { @Test @DisplayName("check that message is in default state") - void check_if_message_is_in_default() { + void checkIfMessageIsInDefault() { StringValue nonDefault = newUuidValue(); assertThrows(IllegalStateException.class, () -> checkDefault(nonDefault)); @@ -113,7 +123,7 @@ void check_if_message_is_in_default() { @Test @DisplayName("check that message is in default state with a parametrized error message") - void check_a_message_is_default_with_parametrized_error_message() { + void checkAMessageIsDefaultWithParametrizedErrorMessage() { StringValue nonDefault = newUuidValue(); assertThrows(IllegalStateException.class, () -> checkDefault(nonDefault, @@ -124,7 +134,7 @@ void check_a_message_is_default_with_parametrized_error_message() { @Test @DisplayName("return default value on check") - void return_default_value_on_check() { + void returnDefaultValueOnCheck() { Message defaultValue = StringValue.getDefaultInstance(); assertEquals(defaultValue, checkDefault(defaultValue)); assertEquals(defaultValue, checkDefault(defaultValue, "error message")); @@ -132,14 +142,14 @@ void return_default_value_on_check() { @Test @DisplayName("check if message is not in default state") - void check_if_message_is_in_not_in_default_state_throwing_exception_if_not() { + void checkIfMessageIsInNotInDefaultStateThrowingExceptionIfNot() { assertThrows(IllegalStateException.class, () -> checkNotDefault(StringValue.getDefaultInstance())); } @Test @DisplayName("return non-default value on check") - void return_non_default_value_on_check() { + void returnNonDefaultValueOnCheck() { StringValue nonDefault = newUuidValue(); assertEquals(nonDefault, checkNotDefault(nonDefault)); assertEquals(nonDefault, checkNotDefault(nonDefault, "with error message")); @@ -147,34 +157,34 @@ void return_non_default_value_on_check() { @Test @DisplayName("throw if checked string is null") - void throw_exception_if_checked_string_is_null() { + void throwExceptionIfCheckedStringIsNull() { assertThrows(NullPointerException.class, () -> checkNotEmptyOrBlank(Tests.nullRef(), "")); } @Test @DisplayName("throw if checked string is empty") - void throw_exception_if_checked_string_is_empty() { + void throwExceptionIfCheckedStringIsEmpty() { assertThrows(IllegalArgumentException.class, () -> checkNotEmptyOrBlank("", "")); } @Test @DisplayName("throw if checked string is blank") - void throw_exception_if_checked_string_is_blank() { + void throwExceptionIfCheckedStringIsBlank() { assertThrows(IllegalArgumentException.class, () -> checkNotEmptyOrBlank(" ", "")); } @Test @DisplayName("not throw if checked strign is not empty or blank") - public void do_not_throw_exception_if_checked_string_is_valid() { + void doNotThrowExceptionIfCheckedStringIsValid() { checkNotEmptyOrBlank("valid_string", ""); } @Test @DisplayName("format message from constraint violation") - void format_message_from_constraint_violation() { + void formatMessageFromConstraintViolation() { ConstraintViolation violation = ConstraintViolation.newBuilder() .setMsgFormat("test %s test %s") .addParam("1") @@ -186,4 +196,102 @@ void format_message_from_constraint_violation() { assertEquals("test 1 test 2", formatted); } + @MuteLogging + @Nested + @DisplayName("test message changes upon (set_once) and") + class SetOnce { + + private static final String ID = "id"; + private static final String BIRTHPLACE = "birthplace"; + + @Test + @DisplayName("throw ValidationException if a (set_once) field is overridden") + void reportIllegalChanges() { + Passport oldValue = Passport + .newBuilder() + .setBirthplace("Kyiv") + .build(); + Passport newValue = Passport + .newBuilder() + .setBirthplace("Kharkiv") + .build(); + checkViolated(oldValue, newValue, BIRTHPLACE); + } + + @Test + @DisplayName("throw ValidationException if an entity ID is overridden") + void reportIdChanges() { + Passport oldValue = Passport + .newBuilder() + .setId("MT 000100010001") + .build(); + Passport newValue = Passport + .newBuilder() + .setId("JC 424242424242") + .build(); + checkViolated(oldValue, newValue, ID); + } + + @Test + @DisplayName("throw ValidationException with several violations") + void reportManyFields() { + Passport oldValue = Passport + .newBuilder() + .setId("MT 111") + .setBirthplace("London") + .build(); + Passport newValue = Passport + .newBuilder() + .setId("JC 424") + .setBirthplace("Edinburgh") + .build(); + checkViolated(oldValue, newValue, ID, BIRTHPLACE); + } + + @Test + @DisplayName("allow overriding repeated fields") + void ignoreRepeated() { + Passport oldValue = Passport + .newBuilder() + .addPhoto(Url.newBuilder() + .setSpec("foo.bar/pic1")) + .build(); + Passport newValue = Passport.getDefaultInstance(); + checkValidChange(oldValue, newValue); + } + + @Test + @DisplayName("allow overriding if (set_once) = false") + void ignoreNonSetOnce() { + Passport oldValue = Passport.getDefaultInstance(); + Passport newValue = Passport + .newBuilder() + .setName(PersonName + .newBuilder() + .setGivenName("John") + .setFamilyName("Doe")) + .build(); + checkValidChange(oldValue, newValue); + } + + private void checkViolated(Passport oldValue, Passport newValue, String... fields) { + ValidationException exception = + assertThrows(ValidationException.class, + () -> checkValidChange(oldValue, newValue)); + List violations = exception.getConstraintViolations(); + assertThat(violations).hasSize(fields.length); + + for (int i = 0; i < fields.length; i++) { + ConstraintViolation violation = violations.get(i); + String field = fields[i]; + + assertThat(violation.getMsgFormat()).contains("(set_once)"); + + String expectedTypeName = TypeName.of(newValue).value(); + assertThat(violation.getTypeName()).contains(expectedTypeName); + + assertThat(violation.getFieldPath()).isEqualTo(FieldPaths.parse(field)); + } + } + } } diff --git a/base/src/test/proto/spine/test/validate/set_once_test.proto b/base/src/test/proto/spine/test/validate/set_once_test.proto new file mode 100644 index 0000000000..6e4f94aba9 --- /dev/null +++ b/base/src/test/proto/spine/test/validate/set_once_test.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; + +package spine.test.validate; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.io"; +option java_package = "io.spine.test.validate"; +option java_outer_classname = "SetOnceTestProto"; +option java_multiple_files = true; + +import "spine/people/person_name.proto"; +import "spine/net/url.proto"; + +message Passport { + option (entity).kind = ENTITY; + + string id = 1; // implicitly: (set_once) = false + + people.PersonName name = 2 [(set_once) = false]; + + string birthplace = 3 [(set_once) = true]; + + // Misuse of `set_once`: should not be used with `repeated` fields. + repeated net.Url photo = 4 [(set_once) = true]; +} diff --git a/license-report.md b/license-report.md index db670ac280..878f342e50 100644 --- a/license-report.md +++ b/license-report.md @@ -1,6 +1,6 @@ -# Dependencies of `io.spine:spine-base:1.0.0-pre7` +# Dependencies of `io.spine:spine-base:1.0.0-SNAPSHOT` ## Runtime 1. **Group:** com.google.code.findbugs **Name:** jsr305 **Version:** 3.0.2 @@ -339,12 +339,12 @@ The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri May 10 14:49:46 EEST 2019** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Thu May 16 13:34:49 EEST 2019** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:spine-errorprone-checks:1.0.0-pre7` +# Dependencies of `io.spine.tools:spine-errorprone-checks:1.0.0-SNAPSHOT` ## Runtime 1. **Group:** com.github.kevinstern **Name:** software-and-algorithms **Version:** 1.0 @@ -736,12 +736,12 @@ This report was generated on **Fri May 10 14:49:46 EEST 2019** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri May 10 14:49:47 EEST 2019** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Thu May 16 13:34:50 EEST 2019** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:spine-javadoc-filter:1.0.0-pre7` +# Dependencies of `io.spine.tools:spine-javadoc-filter:1.0.0-SNAPSHOT` ## Runtime 1. **Group:** com.google.code.findbugs **Name:** jsr305 **Version:** 3.0.2 @@ -1082,12 +1082,12 @@ This report was generated on **Fri May 10 14:49:47 EEST 2019** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri May 10 14:49:47 EEST 2019** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Thu May 16 13:34:51 EEST 2019** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:spine-javadoc-prettifier:1.0.0-pre7` +# Dependencies of `io.spine.tools:spine-javadoc-prettifier:1.0.0-SNAPSHOT` ## Runtime 1. **Group:** com.google.code.findbugs **Name:** jsr305 **Version:** 3.0.2 @@ -1424,12 +1424,12 @@ This report was generated on **Fri May 10 14:49:47 EEST 2019** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri May 10 14:49:48 EEST 2019** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Thu May 16 13:34:51 EEST 2019** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:spine-model-compiler:1.0.0-pre7` +# Dependencies of `io.spine.tools:spine-model-compiler:1.0.0-SNAPSHOT` ## Runtime 1. **Group:** aopalliance **Name:** aopalliance **Version:** 1.0 @@ -1916,12 +1916,12 @@ This report was generated on **Fri May 10 14:49:48 EEST 2019** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri May 10 14:49:51 EEST 2019** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Thu May 16 13:34:52 EEST 2019** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:spine-mute-logging:1.0.0-pre7` +# Dependencies of `io.spine.tools:spine-mute-logging:1.0.0-SNAPSHOT` ## Runtime 1. **Group:** com.google.auto.value **Name:** auto-value-annotations **Version:** 1.6.3 @@ -2329,12 +2329,12 @@ This report was generated on **Fri May 10 14:49:51 EEST 2019** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri May 10 14:49:52 EEST 2019** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Thu May 16 13:34:53 EEST 2019** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:spine-plugin-base:1.0.0-pre7` +# Dependencies of `io.spine.tools:spine-plugin-base:1.0.0-SNAPSHOT` ## Runtime 1. **Group:** com.google.code.findbugs **Name:** jsr305 **Version:** 3.0.2 @@ -2671,12 +2671,12 @@ This report was generated on **Fri May 10 14:49:52 EEST 2019** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri May 10 14:49:52 EEST 2019** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Thu May 16 13:34:53 EEST 2019** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:spine-plugin-testlib:1.0.0-pre7` +# Dependencies of `io.spine.tools:spine-plugin-testlib:1.0.0-SNAPSHOT` ## Runtime 1. **Group:** com.google.auto.value **Name:** auto-value-annotations **Version:** 1.6.3 @@ -3088,12 +3088,12 @@ This report was generated on **Fri May 10 14:49:52 EEST 2019** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri May 10 14:49:53 EEST 2019** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Thu May 16 13:34:54 EEST 2019** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:spine-proto-js-plugin:1.0.0-pre7` +# Dependencies of `io.spine.tools:spine-proto-js-plugin:1.0.0-SNAPSHOT` ## Runtime 1. **Group:** com.google.code.findbugs **Name:** jsr305 **Version:** 3.0.2 @@ -3430,12 +3430,12 @@ This report was generated on **Fri May 10 14:49:53 EEST 2019** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri May 10 14:49:53 EEST 2019** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Thu May 16 13:34:54 EEST 2019** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:spine-protoc-api:1.0.0-pre7` +# Dependencies of `io.spine.tools:spine-protoc-api:1.0.0-SNAPSHOT` ## Runtime 1. **Group:** com.google.code.findbugs **Name:** jsr305 **Version:** 3.0.2 @@ -3764,12 +3764,12 @@ This report was generated on **Fri May 10 14:49:53 EEST 2019** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri May 10 14:49:53 EEST 2019** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Thu May 16 13:34:55 EEST 2019** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:spine-protoc-plugin:1.0.0-pre7` +# Dependencies of `io.spine.tools:spine-protoc-plugin:1.0.0-SNAPSHOT` ## Runtime 1. **Group:** com.google.code.findbugs **Name:** jsr305 **Version:** 3.0.2 @@ -4181,12 +4181,12 @@ This report was generated on **Fri May 10 14:49:53 EEST 2019** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri May 10 14:49:54 EEST 2019** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Thu May 16 13:34:55 EEST 2019** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine:spine-testlib:1.0.0-pre7` +# Dependencies of `io.spine:spine-testlib:1.0.0-SNAPSHOT` ## Runtime 1. **Group:** com.google.auto.value **Name:** auto-value-annotations **Version:** 1.6.3 @@ -4594,12 +4594,12 @@ This report was generated on **Fri May 10 14:49:54 EEST 2019** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri May 10 14:49:54 EEST 2019** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). +This report was generated on **Thu May 16 13:34:56 EEST 2019** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). -# Dependencies of `io.spine.tools:spine-tool-base:1.0.0-pre7` +# Dependencies of `io.spine.tools:spine-tool-base:1.0.0-SNAPSHOT` ## Runtime 1. **Group:** com.google.code.findbugs **Name:** jsr305 **Version:** 3.0.2 @@ -4940,4 +4940,4 @@ This report was generated on **Fri May 10 14:49:54 EEST 2019** using [Gradle-Lic The dependencies distributed under several licenses, are used according their commercial-use-friendly license. -This report was generated on **Fri May 10 14:49:55 EEST 2019** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). \ No newline at end of file +This report was generated on **Thu May 16 13:34:56 EEST 2019** using [Gradle-License-Report plugin](https://github.com/jk1/Gradle-License-Report) by Evgeny Naumenko, licensed under [Apache 2.0 License](https://github.com/jk1/Gradle-License-Report/blob/master/LICENSE). \ No newline at end of file diff --git a/tools/errorprone-checks/build.gradle b/tools/errorprone-checks/build.gradle index 44d41a8621..9cf8777f2c 100644 --- a/tools/errorprone-checks/build.gradle +++ b/tools/errorprone-checks/build.gradle @@ -27,7 +27,7 @@ repositories { dependencies { annotationProcessor deps.build.autoService.processor compileOnly deps.build.autoService.annotations - + implementation project(':base') implementation project(':plugin-base') @@ -59,5 +59,3 @@ task configureBootClasspath { } } } - -test.dependsOn configureBootClasspath diff --git a/tools/errorprone-checks/src/main/java/io/spine/tools/check/BugPatternMatcher.java b/tools/errorprone-checks/src/main/java/io/spine/tools/check/BugPatternMatcher.java index abb7867d99..3b2df87592 100644 --- a/tools/errorprone-checks/src/main/java/io/spine/tools/check/BugPatternMatcher.java +++ b/tools/errorprone-checks/src/main/java/io/spine/tools/check/BugPatternMatcher.java @@ -20,7 +20,9 @@ package io.spine.tools.check; +import com.google.common.collect.ImmutableList; import com.google.errorprone.VisitorState; +import com.google.errorprone.fixes.Fix; import com.sun.source.tree.Tree; import io.spine.annotation.Internal; @@ -49,10 +51,10 @@ public interface BugPatternMatcher { boolean matches(T tree, VisitorState state); /** - * Obtains a {@code Fixer} for the case of the {@link com.google.errorprone.BugPattern} + * Obtains {@code Fix}es for the case of the {@link com.google.errorprone.BugPattern} * processed by this class. * - * @return the {@code Fixer} for the processed bug pattern case + * @return {@code Fix}es for the processed bug pattern case */ - Fixer getFixer(); + ImmutableList fixes(T tree); } diff --git a/tools/errorprone-checks/src/main/java/io/spine/tools/check/Fixer.java b/tools/errorprone-checks/src/main/java/io/spine/tools/check/Fixer.java deleted file mode 100644 index b56046c05e..0000000000 --- a/tools/errorprone-checks/src/main/java/io/spine/tools/check/Fixer.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright 2019, TeamDev. All rights reserved. - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.tools.check; - -import com.google.errorprone.VisitorState; -import com.google.errorprone.fixes.Fix; -import com.sun.source.tree.Tree; -import io.spine.annotation.Internal; - -import java.util.Optional; - -/** - * Generates a {@link Fix} to be displayed to the user given the errored expression. - * - * @param the expression {@code Tree} - * @see com.google.errorprone.bugpatterns.BugChecker#describeMatch(Tree, Optional) - */ -@Internal -public interface Fixer { - - /** - * Creates a fix for the {@link com.google.errorprone.BugPattern} given the position where the - * error was found and the expression. - * - *

The method should be used in the {@link com.google.errorprone.bugpatterns.BugChecker} - * implementations where the tree and the state are provided by the Error Prone code scanners. - * - * @param tree the errored expression {@code Tree} - * @param state the current {@code VisitorState} - * @return the {@code Optional} containing the {@code Fix} or {@link Optional#EMPTY} if no fix - * can be created - */ - Optional createFix(T tree, VisitorState state); -} diff --git a/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/matcher/NewBuilderMatcher.java b/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuild/BuildMatcher.java similarity index 51% rename from tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/matcher/NewBuilderMatcher.java rename to tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuild/BuildMatcher.java index 9c00cf6355..62dacbf429 100644 --- a/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/matcher/NewBuilderMatcher.java +++ b/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuild/BuildMatcher.java @@ -18,41 +18,43 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package io.spine.tools.check.vbuilder.matcher; +package io.spine.tools.check.vbuild; +import com.google.common.collect.ImmutableList; import com.google.errorprone.VisitorState; +import com.google.errorprone.fixes.Fix; import com.google.errorprone.matchers.Matcher; import com.sun.source.tree.ExpressionTree; import com.sun.source.tree.MethodInvocationTree; -import io.spine.annotation.Internal; -import io.spine.tools.check.BugPatternMatcher; -import io.spine.tools.check.Fixer; -import io.spine.tools.check.vbuilder.fixer.NewBuilderFixer; -import static io.spine.protobuf.Messages.METHOD_NEW_BUILDER; +import java.util.stream.Stream; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static io.spine.tools.check.vbuild.UseVBuild.BUILD; /** - * A matcher for the {@link io.spine.tools.check.vbuilder.UseValidatingBuilder} bug pattern which - * tracks down the cases where the {@code Message.newBuilder()} or the - * {@code Message.newBuilder(prototype)} statement is used. - * - *

Both normally called and static-imported methods are handled. + * A matcher for the {@link io.spine.tools.check.vbuild.UseVBuild} bug pattern which tracks down + * the cases where the {@code builder.build()} statement is used. */ -@Internal -public class NewBuilderMatcher implements BugPatternMatcher { +enum BuildMatcher implements ContextualMatcher { - private final Matcher matcher = - CustomProtobufType.callingStaticMethod(METHOD_NEW_BUILDER); - private final Fixer fixer = new NewBuilderFixer(); + INSTANCE; + + @SuppressWarnings("ImmutableEnumChecker") + private static final Matcher builderBuild = + GeneratedValidatingBuilder.callingInstanceMethod(BUILD); @Override - public boolean matches(MethodInvocationTree tree, VisitorState state) { - boolean matches = matcher.matches(tree, state); - return matches; + public boolean outsideMessageContextMatches(MethodInvocationTree tree, VisitorState state) { + return builderBuild.matches(tree, state); } @Override - public Fixer getFixer() { - return fixer; + public ImmutableList fixes(MethodInvocationTree tree) { + ExpressionTree methodTree = tree.getMethodSelect(); + return Stream.of(BuildMethodAlternative.values()) + .map(alt -> alt.replace(methodTree)) + .collect(toImmutableList()); + } } diff --git a/base/src/test/java/io/spine/validate/builders/BuilderTest.java b/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuild/BuildMethodAlternative.java similarity index 59% rename from base/src/test/java/io/spine/validate/builders/BuilderTest.java rename to tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuild/BuildMethodAlternative.java index bb34a695c2..0028688c4d 100644 --- a/base/src/test/java/io/spine/validate/builders/BuilderTest.java +++ b/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuild/BuildMethodAlternative.java @@ -18,32 +18,26 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package io.spine.validate.builders; +package io.spine.tools.check.vbuild; -import com.google.protobuf.Message; -import io.spine.validate.ValidatingBuilder; -import org.junit.jupiter.api.BeforeEach; - -import static com.google.common.base.Preconditions.checkNotNull; +import com.google.errorprone.fixes.Fix; +import com.google.errorprone.fixes.SuggestedFix; +import com.sun.source.tree.Tree; /** - * Abstract base for testing default VBuilders. + * The alternatives to the {@code Builder.build()} method. * - * @param the type of the message produced by the builder - * @param the type of the validating builder + * @see io.spine.protobuf.ValidatingBuilder */ -abstract class BuilderTest> { - - private B builder; - - @BeforeEach - void setUp() { - builder = createBuilder(); - } +enum BuildMethodAlternative { - abstract B createBuilder(); + vBuild, + buildPartial; - protected B builder() { - return checkNotNull(builder, "builder is not initialized"); + /** + * Creates a fix which suggests to replace the given tree element with this method. + */ + public Fix replace(Tree tree) { + return SuggestedFix.replace(tree, name()); } } diff --git a/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuild/BuildReferenceMatcher.java b/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuild/BuildReferenceMatcher.java new file mode 100644 index 0000000000..83e97e2064 --- /dev/null +++ b/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuild/BuildReferenceMatcher.java @@ -0,0 +1,64 @@ +/* + * Copyright 2019, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.tools.check.vbuild; + +import com.google.common.collect.ImmutableList; +import com.google.errorprone.VisitorState; +import com.google.errorprone.fixes.Fix; +import com.google.errorprone.fixes.SuggestedFix; +import com.google.errorprone.matchers.Matcher; +import com.sun.source.tree.MemberReferenceTree; +import com.sun.source.tree.Tree; +import io.spine.protobuf.ValidatingBuilder; + +import java.util.stream.Stream; + +import static com.google.common.collect.ImmutableList.toImmutableList; +import static com.google.errorprone.matchers.Matchers.isSubtypeOf; +import static io.spine.tools.check.vbuild.UseVBuild.BUILD; + +/** + * A matcher for the {@link io.spine.tools.check.vbuild.UseVBuild} bug pattern which tracks down + * the cases where the {@code builder::build} statement is used. + */ +enum BuildReferenceMatcher implements ContextualMatcher { + + INSTANCE; + + private static final Matcher receiverMatcher = isSubtypeOf(ValidatingBuilder.class); + + @Override + public boolean outsideMessageContextMatches(MemberReferenceTree tree, VisitorState state) { + return receiverMatcher.matches(tree.getQualifierExpression(), state) + && tree.getName() + .contentEquals(BUILD); + } + + @Override + public ImmutableList fixes(MemberReferenceTree tree) { + String receiver = tree.getQualifierExpression().toString(); + return Stream.of(BuildMethodAlternative.values()) + .map(BuildMethodAlternative::name) + .map(name -> receiver + "::" + name) + .map(replacement -> SuggestedFix.replace(tree, replacement)) + .collect(toImmutableList()); + } +} diff --git a/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuild/ContextualMatcher.java b/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuild/ContextualMatcher.java new file mode 100644 index 0000000000..b8b96d99bc --- /dev/null +++ b/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuild/ContextualMatcher.java @@ -0,0 +1,79 @@ +/* + * Copyright 2019, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.tools.check.vbuild; + +import com.google.errorprone.VisitorState; +import com.google.errorprone.matchers.Matcher; +import com.google.protobuf.MessageOrBuilder; +import com.sun.source.tree.ClassTree; +import com.sun.source.tree.Tree; +import io.spine.tools.check.BugPatternMatcher; + +import static com.google.errorprone.matchers.Matchers.isSubtypeOf; + +/** + * A context-sensitive matcher. + * + *

If the matching element resides in a {@code Message} class or in {@code Builder} class, it is + * never matched. + */ +interface ContextualMatcher extends BugPatternMatcher { + + /** + * Applies this matcher. + * + *

It is guaranteed that when this method is called the matcher is not in a {@code Message} + * or a {@code Builder} class. + * + * @see #inMessageOrBuilder(VisitorState) + */ + boolean outsideMessageContextMatches(T tree, VisitorState state); + + /** + * {@inheritDoc} + * + *

If in the context of a {@code Message} or a {@code Builder}, always returns + * {@code false}. + * + * @param tree + * the expression {@code Tree} + * @param state + * the current {@code VisitorState} + * @return {@code true} if {@link #inMessageOrBuilder} is {@code false} and + * {@link #outsideMessageContextMatches} is {@code true}; {@code false} otherwise + */ + @Override + default boolean matches(T tree, VisitorState state) { + return !inMessageOrBuilder(state) && outsideMessageContextMatches(tree, state); + } + + /** + * Checks if the matcher is in the context of a {@code Message} or a {@code Builder}. + * + * @return {@code true} if currently matching statements inside a {@code Message} or + * a {@code Builder} descendant; {@code false} otherwise + */ + default boolean inMessageOrBuilder(VisitorState state) { + Matcher messageOrBuilder = isSubtypeOf(MessageOrBuilder.class); + ClassTree enclosingClass = state.findEnclosing(ClassTree.class); + return messageOrBuilder.matches(enclosingClass, state); + } +} diff --git a/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/matcher/CustomProtobufType.java b/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuild/GeneratedValidatingBuilder.java similarity index 54% rename from tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/matcher/CustomProtobufType.java rename to tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuild/GeneratedValidatingBuilder.java index 6fabbbf5b6..abaf4b75bc 100644 --- a/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/matcher/CustomProtobufType.java +++ b/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuild/GeneratedValidatingBuilder.java @@ -18,75 +18,52 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package io.spine.tools.check.vbuilder.matcher; +package io.spine.tools.check.vbuild; import com.google.errorprone.VisitorState; import com.google.errorprone.matchers.Matcher; import com.google.errorprone.predicates.TypePredicate; -import com.google.protobuf.Message; import com.sun.source.tree.ExpressionTree; -import com.sun.tools.javac.code.Symbol; import com.sun.tools.javac.code.Type; -import io.spine.code.java.ClassName; +import io.spine.protobuf.ValidatingBuilder; import static com.google.errorprone.matchers.method.MethodMatchers.instanceMethod; -import static com.google.errorprone.matchers.method.MethodMatchers.staticMethod; import static com.google.errorprone.predicates.TypePredicates.isDescendantOf; -import static io.spine.code.GooglePackage.notInGooglePackage; /** - * A predicate which matches custom (i.e. non-Google) Protobuf types. + * A predicate which matches builders of custom (i.e. non-Google) Protobuf messages. * - *

Any {@code final} Java class which descends from {@link Message} and does not belong - * to {@code com.google} or {@code google} package or its subpackage matches this predicate. + *

Any Java class which descends from {@link io.spine.validate.ValidatingBuilder} matches this + * predicate. */ -final class CustomProtobufType implements TypePredicate { +final class GeneratedValidatingBuilder implements TypePredicate { private static final long serialVersionUID = 0L; - private static final TypePredicate IS_MESSAGE = isDescendantOf(Message.class.getName()); + private static final TypePredicate IS_MESSAGE_BUILDER = + isDescendantOf(ValidatingBuilder.class.getName()); /** * Prevents direct instantiation. */ - private CustomProtobufType() { - } - - /** - * Obtains a static method invocation matcher for the methods in custom Protobuf types and with - * the given name. - * - * @param methodName the method name to match - */ - static Matcher callingStaticMethod(String methodName) { - return staticMethod() - .onClass(new CustomProtobufType()) - .named(methodName); + private GeneratedValidatingBuilder() { } /** * Obtains an instance method invocation matcher for the methods in custom Protobuf types and * with the given name. * - * @param methodName the method name to match + * @param methodName + * the method name to match */ static Matcher callingInstanceMethod(String methodName) { return instanceMethod() - .onClass(new CustomProtobufType()) + .onClass(new GeneratedValidatingBuilder()) .named(methodName); } @Override public boolean apply(Type type, VisitorState state) { - return type.isFinal() - && IS_MESSAGE.apply(type, state) - && notGoogle(type); - } - - private static boolean notGoogle(Type type) { - Symbol.TypeSymbol typeSymbol = type.asElement(); - String typeFqn = typeSymbol.getQualifiedName() - .toString(); - return notInGooglePackage(ClassName.of(typeFqn)); + return IS_MESSAGE_BUILDER.apply(type, state); } } diff --git a/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuild/UseVBuild.java b/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuild/UseVBuild.java new file mode 100644 index 0000000000..0be1982321 --- /dev/null +++ b/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuild/UseVBuild.java @@ -0,0 +1,94 @@ +/* + * Copyright 2019, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.tools.check.vbuild; + +import com.google.auto.service.AutoService; +import com.google.common.collect.ImmutableList; +import com.google.errorprone.BugPattern; +import com.google.errorprone.VisitorState; +import com.google.errorprone.bugpatterns.BugChecker; +import com.google.errorprone.bugpatterns.BugChecker.MemberReferenceTreeMatcher; +import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; +import com.google.errorprone.fixes.Fix; +import com.google.errorprone.matchers.Description; +import com.sun.source.tree.MemberReferenceTree; +import com.sun.source.tree.MethodInvocationTree; +import com.sun.source.tree.Tree; +import io.spine.tools.check.BugPatternMatcher; + +import static com.google.errorprone.BugPattern.LinkType.NONE; +import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; +import static com.google.errorprone.matchers.Description.NO_MATCH; + +/** + * An ErrorProne check which warns users to prefer + * {@link io.spine.protobuf.ValidatingBuilder#vBuild()} over + * {@link io.spine.protobuf.ValidatingBuilder#build()}. + * + *

Unlink {@code build()}, {@code vBuild()} ensures that the constructed message is valid. This + * is what the user wants in most cases. If, however, for some reason, the validation is unwanted, + * the user in encouraged to use {@code buildPartial()} in order to make the intent explicit. + */ +// TODO:2019-05-13:dmytro.dashenkov: Add a link to documentation. +@AutoService(BugChecker.class) +@BugPattern( + name = "UseVBuild", + summary = UseVBuild.SUMMARY, + severity = WARNING, + linkType = NONE +) +public class UseVBuild + extends BugChecker + implements MethodInvocationTreeMatcher, MemberReferenceTreeMatcher { + + private static final long serialVersionUID = 0L; + + static final String NAME = UseVBuild.class.getSimpleName(); + static final String SUMMARY = "Prefer using vBuild() instead of build()."; + + @SuppressWarnings("DuplicateStringLiteralInspection") // Used in other contexts. + static final String BUILD = "build"; + + @Override + public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { + return match(BuildMatcher.INSTANCE, tree, state); + } + + @Override + public Description matchMemberReference(MemberReferenceTree tree, VisitorState state) { + return match(BuildReferenceMatcher.INSTANCE, tree, state); + } + + private static Description + match(BugPatternMatcher matcher, T tree, VisitorState state) { + boolean matches = matcher.matches(tree, state); + if (matches) { + ImmutableList fixes = matcher.fixes(tree); + Description description = Description + .builder(tree, UseVBuild.class.getSimpleName(), null, WARNING, SUMMARY) + .addAllFixes(fixes) + .build(); + return description; + } else { + return NO_MATCH; + } + } +} diff --git a/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/fixer/package-info.java b/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuild/package-info.java similarity index 83% rename from tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/fixer/package-info.java rename to tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuild/package-info.java index 90b1eeae48..b571e12d82 100644 --- a/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/fixer/package-info.java +++ b/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuild/package-info.java @@ -19,12 +19,13 @@ */ /** - * This package contains classes for generating {@link com.google.errorprone.fixes.Fix} for the - * different cases of the {@link io.spine.tools.check.vbuilder.UseValidatingBuilder} bug pattern. + * This package contains the custom Error Prone check to detect usage of ordinary {@code build()} + * method for the Protobuf messages and advice using the {@code vBuild()} method added by Spine. */ + @CheckReturnValue @ParametersAreNonnullByDefault -package io.spine.tools.check.vbuilder.fixer; +package io.spine.tools.check.vbuild; import com.google.errorprone.annotations.CheckReturnValue; diff --git a/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/UseValidatingBuilder.java b/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/UseValidatingBuilder.java index d3dbef7e07..8b14f948ec 100644 --- a/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/UseValidatingBuilder.java +++ b/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/UseValidatingBuilder.java @@ -25,29 +25,14 @@ import com.google.errorprone.VisitorState; import com.google.errorprone.bugpatterns.BugChecker; import com.google.errorprone.bugpatterns.BugChecker.MethodInvocationTreeMatcher; -import com.google.errorprone.fixes.Fix; import com.google.errorprone.matchers.Description; -import com.google.errorprone.matchers.Matcher; -import com.google.protobuf.Message; -import com.sun.source.tree.ClassTree; import com.sun.source.tree.MethodInvocationTree; -import com.sun.source.tree.Tree; import io.spine.annotation.Internal; -import io.spine.tools.check.BugPatternMatcher; -import io.spine.tools.check.Fixer; -import io.spine.tools.check.vbuilder.matcher.NewBuilderForTypeMatcher; -import io.spine.tools.check.vbuilder.matcher.NewBuilderMatcher; -import io.spine.tools.check.vbuilder.matcher.ToBuilderMatcher; import io.spine.validate.ValidatingBuilder; -import java.util.ArrayList; -import java.util.List; -import java.util.Optional; - import static com.google.errorprone.BugPattern.LinkType.CUSTOM; import static com.google.errorprone.BugPattern.SeverityLevel.WARNING; import static com.google.errorprone.matchers.Description.NO_MATCH; -import static com.google.errorprone.matchers.Matchers.isSubtypeOf; /** * A custom Error Prone check that matches the usages of the ordinary @@ -76,6 +61,7 @@ link = UseValidatingBuilder.LINK ) @Internal +@Deprecated public class UseValidatingBuilder extends BugChecker implements MethodInvocationTreeMatcher { static final String SUMMARY = "Prefer using Spine Validating Builders instead of the " + @@ -86,43 +72,8 @@ public class UseValidatingBuilder extends BugChecker implements MethodInvocation private static final long serialVersionUID = 0L; - private static final List> matchers = matchers(); - @Override public Description matchMethodInvocation(MethodInvocationTree tree, VisitorState state) { - for (BugPatternMatcher matcher : matchers) { - if (matcher.matches(tree, state) && !isInVBuilderOrMessage(state)) { - Fixer fixer = matcher.getFixer(); - Optional fix = fixer.createFix(tree, state); - Description description = describeMatch(tree, fix); - return description; - } - } return NO_MATCH; } - - private static boolean isInVBuilderOrMessage(VisitorState state) { - ClassTree enclosingClass = state.findEnclosing(ClassTree.class); - boolean isInVBuilder = vBuilderMatcher().matches(enclosingClass, state); - boolean isInMessage = messageMatcher().matches(enclosingClass, state); - return isInVBuilder || isInMessage; - } - - private static Matcher vBuilderMatcher() { - Matcher matcher = isSubtypeOf(ValidatingBuilder.class); - return matcher; - } - - private static Matcher messageMatcher() { - Matcher matcher = isSubtypeOf(Message.class); - return matcher; - } - - private static List> matchers() { - List> matchers = new ArrayList<>(); - matchers.add(new NewBuilderMatcher()); - matchers.add(new NewBuilderForTypeMatcher()); - matchers.add(new ToBuilderMatcher()); - return matchers; - } } diff --git a/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/fixer/FixGenerator.java b/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/fixer/FixGenerator.java deleted file mode 100644 index 2b90113e2c..0000000000 --- a/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/fixer/FixGenerator.java +++ /dev/null @@ -1,167 +0,0 @@ -/* - * Copyright 2019, TeamDev. All rights reserved. - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.tools.check.vbuilder.fixer; - -import com.google.errorprone.VisitorState; -import com.google.errorprone.fixes.Fix; -import com.google.errorprone.fixes.SuggestedFix; -import com.sun.source.tree.ExpressionTree; -import com.sun.source.tree.MethodInvocationTree; -import com.sun.source.tree.Tree.Kind; -import com.sun.tools.javac.code.Symbol.ClassSymbol; -import com.sun.tools.javac.code.Symbol.MethodSymbol; -import com.sun.tools.javac.code.Type; -import com.sun.tools.javac.tree.JCTree.JCExpression; -import com.sun.tools.javac.tree.JCTree.JCFieldAccess; -import com.sun.tools.javac.tree.JCTree.JCIdent; -import io.spine.type.MessageType; - -import static com.google.errorprone.fixes.SuggestedFixes.prettyType; -import static com.google.errorprone.util.ASTHelpers.enclosingClass; -import static com.sun.source.tree.Tree.Kind.IDENTIFIER; -import static com.sun.source.tree.Tree.Kind.MEMBER_SELECT; -import static io.spine.util.Exceptions.newIllegalStateException; - -/** - * A generator for the common {@link com.google.errorprone.BugPattern} {@linkplain Fix fixes} - * related to the {@link io.spine.validate.ValidatingBuilder} usage. - * - *

This class should only be used from the Error Prone - * {@link com.google.errorprone.bugpatterns.BugChecker} context, where the code scanners can provide - * proper {@link MethodInvocationTree} and {@link VisitorState} for its initialization. - * - * @see io.spine.tools.check.vbuilder.UseValidatingBuilder - */ -@SuppressWarnings("DuplicateStringLiteralInspection") -// Method names where introducing constant doesn't seem reasonable. -class FixGenerator { - - private final MethodInvocationTree tree; - private final VisitorState state; - - private FixGenerator(MethodInvocationTree tree, VisitorState state) { - this.tree = tree; - this.state = state; - } - - /** - * Creates the {@code FixGenerator} instance for the given expression and visitor state. - * - * @param tree the expression {@code Tree} - * @param state the current {@code VisitorState} - * @return the {@code FixGenerator} instance for the given expression - */ - static FixGenerator createFor(MethodInvocationTree tree, VisitorState state) { - return new FixGenerator(tree, state); - } - - /** - * Creates a fix which replaces the current expression with the {@code ...VBuilder.newBuilder()} - * expression. - * - *

This method assumes that the {@linkplain #tree current expression} is the call on some of - * the {@link com.google.protobuf.Message} class descendants. - * - * @return the {@code Fix} which can be later displayed to the user via the Error Prone tools - */ - Fix newVBuilderCall() { - String newVBuilderCall = ".newBuilder()"; - Fix fix = callOnVBuilder(newVBuilderCall); - return fix; - } - - /** - * Creates a fix which replaces the current expression with the - * {@code ...VBuilder.newBuilder().mergeFrom(arg)} expression. - * - *

This method assumes that the {@linkplain #tree current expression} is the call that - * utilizes some of the {@link com.google.protobuf.Message} class instances for the field - * initialization. - * - * @param mergeFromArg the object from which the fields are taken for the - * {@link com.google.protobuf.Message.Builder} - * @return the {@code Fix} which can be later displayed to the user via the Error Prone tools - */ - Fix mergeFromCall(String mergeFromArg) { - String mergeFromCall = ".newBuilder().mergeFrom(" + mergeFromArg + ')'; - Fix fix = callOnVBuilder(mergeFromCall); - return fix; - } - - /** - * Generates an expression such that given {@code statement} is called on the - * {@link io.spine.validate.ValidatingBuilder} class. - * - *

The {@code ValidatingBuilder} class is calculated from the current expression. - * - * @param statement the statement to call on the validating builder class - * @return the statement with the call - */ - private Fix callOnVBuilder(String statement) { - String vBuilderName = generateVBuilderName(); - String fixedLine = vBuilderName + statement; - Fix fix = SuggestedFix.builder() - .replace(tree, fixedLine) - .build(); - return fix; - } - - /** - * Generates the {@link io.spine.validate.ValidatingBuilder} class name based on the current - * expression {@code Tree} and {@code VisitorState}. - */ - private String generateVBuilderName() { - Type type = getTypeOnWhichInvoked(); - String simpleName = prettyType(state, null, type); - String vBuilderName = simpleName + MessageType.VBUILDER_SUFFIX; - return vBuilderName; - } - - /** - * Obtains the {@code Type} on which the current expression is invoked. - */ - private Type getTypeOnWhichInvoked() { - ExpressionTree expression = tree.getMethodSelect(); - Kind kind = expression.getKind(); - if (kind == MEMBER_SELECT) { - return typeFromMethodCall((JCFieldAccess) expression); - } - if (kind == IDENTIFIER) { - return typeFromStaticImportedCall((JCIdent) expression); - } - throw newIllegalStateException("Expression of unexpected kind %s where method call " + - "or static-imported method call are expected", - expression.getKind()); - } - - private static Type typeFromMethodCall(JCFieldAccess expression) { - JCExpression invokedOn = expression.selected; - Type type = invokedOn.type; - return type; - } - - private static Type typeFromStaticImportedCall(JCIdent expression) { - MethodSymbol method = (MethodSymbol) expression.sym; - ClassSymbol classSymbol = enclosingClass(method); - Type type = classSymbol.type; - return type; - } -} diff --git a/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/fixer/NewBuilderFixer.java b/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/fixer/NewBuilderFixer.java deleted file mode 100644 index f81a0646f2..0000000000 --- a/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/fixer/NewBuilderFixer.java +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2019, TeamDev. All rights reserved. - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.tools.check.vbuilder.fixer; - -import com.google.common.collect.Iterators; -import com.google.errorprone.VisitorState; -import com.google.errorprone.fixes.Fix; -import com.sun.source.tree.ExpressionTree; -import com.sun.source.tree.MethodInvocationTree; -import io.spine.annotation.Internal; -import io.spine.tools.check.Fixer; - -import java.util.List; -import java.util.Optional; - -/** - * Creates a {@link Fix} for the {@link io.spine.tools.check.vbuilder.UseValidatingBuilder} bug - * pattern cases where the {@code Message.newBuilder()} or {@code Message.newBuilder(prototype)} - * statement is used. - * - *

Suggests the fix as follows: - * - *

- */ -@Internal -public class NewBuilderFixer implements Fixer { - - /** - * {@inheritDoc} - */ - @Override - public Optional createFix(MethodInvocationTree tree, VisitorState state) { - List methodCallArgs = tree.getArguments(); - if (methodCallArgs.isEmpty()) { - return fixForNoArgs(tree, state); - } - if (methodCallArgs.size() == 1) { - return fixForOneArg(tree, state); - } - return noFix(); - } - - private static Optional fixForNoArgs(MethodInvocationTree tree, VisitorState state) { - FixGenerator generator = FixGenerator.createFor(tree, state); - Fix fix = generator.newVBuilderCall(); - Optional result = Optional.of(fix); - return result; - } - - private static Optional fixForOneArg(MethodInvocationTree tree, VisitorState state) { - List args = tree.getArguments(); - ExpressionTree arg = Iterators.getOnlyElement(args.iterator()); - String argString = arg.toString(); - - FixGenerator generator = FixGenerator.createFor(tree, state); - Fix fix = generator.mergeFromCall(argString); - Optional result = Optional.of(fix); - return result; - } - - private static Optional noFix() { - return Optional.empty(); - } -} diff --git a/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/fixer/NewBuilderForTypeFixer.java b/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/fixer/NewBuilderForTypeFixer.java deleted file mode 100644 index 8216130cdb..0000000000 --- a/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/fixer/NewBuilderForTypeFixer.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2019, TeamDev. All rights reserved. - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.tools.check.vbuilder.fixer; - -import com.google.errorprone.VisitorState; -import com.google.errorprone.fixes.Fix; -import com.sun.source.tree.MethodInvocationTree; -import io.spine.annotation.Internal; -import io.spine.tools.check.Fixer; - -import java.util.Optional; - -/** - * Creates a {@link Fix} for the {@link io.spine.tools.check.vbuilder.UseValidatingBuilder} bug - * pattern cases where the {@code message.newBuilderForType()} statement is used. - * - *

Suggests the fix as follows: - * - *

- * {@code message.newBuilderForType()} -> {@code MessageVBuilder.newBuilder()}
- * 
- */ -@Internal -public class NewBuilderForTypeFixer implements Fixer { - - @Override - public Optional createFix(MethodInvocationTree tree, VisitorState state) { - FixGenerator generator = FixGenerator.createFor(tree, state); - Fix fix = generator.newVBuilderCall(); - Optional result = Optional.of(fix); - return result; - } -} diff --git a/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/fixer/ToBuilderFixer.java b/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/fixer/ToBuilderFixer.java deleted file mode 100644 index 217bd854c0..0000000000 --- a/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/fixer/ToBuilderFixer.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2019, TeamDev. All rights reserved. - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.tools.check.vbuilder.fixer; - -import com.google.errorprone.VisitorState; -import com.google.errorprone.fixes.Fix; -import com.sun.source.tree.ExpressionTree; -import com.sun.source.tree.MethodInvocationTree; -import com.sun.tools.javac.tree.JCTree.JCExpression; -import com.sun.tools.javac.tree.JCTree.JCFieldAccess; -import io.spine.annotation.Internal; -import io.spine.tools.check.Fixer; - -import java.util.Optional; - -/** - * Creates a {@link Fix} for the {@link io.spine.tools.check.vbuilder.UseValidatingBuilder} bug - * pattern cases where the {@code message.toBuilder()} statement is used. - * - *

Suggests the fix as follows: - * - *

- * {@code message.toBuilder()} -> {@code MessageVBuilder.newBuilder().mergeFrom(message)}
- * 
- */ -@Internal -public class ToBuilderFixer implements Fixer { - - @Override - public Optional createFix(MethodInvocationTree tree, VisitorState state) { - ExpressionTree expression = tree.getMethodSelect(); - JCFieldAccess fieldAccess = (JCFieldAccess) expression; - JCExpression invokedOn = fieldAccess.selected; - String invokedOnString = invokedOn.toString(); - - FixGenerator generator = FixGenerator.createFor(tree, state); - Fix fix = generator.mergeFromCall(invokedOnString); - Optional result = Optional.of(fix); - return result; - } -} diff --git a/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/matcher/NewBuilderForTypeMatcher.java b/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/matcher/NewBuilderForTypeMatcher.java deleted file mode 100644 index 5e8a054561..0000000000 --- a/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/matcher/NewBuilderForTypeMatcher.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2019, TeamDev. All rights reserved. - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.tools.check.vbuilder.matcher; - -import com.google.errorprone.VisitorState; -import com.google.errorprone.matchers.Matcher; -import com.google.protobuf.Message; -import com.sun.source.tree.ExpressionTree; -import com.sun.source.tree.MethodInvocationTree; -import io.spine.annotation.Internal; -import io.spine.tools.check.BugPatternMatcher; -import io.spine.tools.check.Fixer; -import io.spine.tools.check.vbuilder.fixer.NewBuilderForTypeFixer; - -/** - * A matcher for the {@link io.spine.tools.check.vbuilder.UseValidatingBuilder} bug pattern which - * tracks down the cases where the {@code message.newBuilderForType()} statement is used. - * - * @see Message#newBuilderForType() - */ -@Internal -public class NewBuilderForTypeMatcher implements BugPatternMatcher { - - @SuppressWarnings("DuplicateStringLiteralInspection") // Used in another context. - private static final String METHOD_NAME = "newBuilderForType"; - - private final Matcher matcher = - CustomProtobufType.callingInstanceMethod(METHOD_NAME); - private final Fixer fixer = new NewBuilderForTypeFixer(); - - @Override - public boolean matches(MethodInvocationTree tree, VisitorState state) { - boolean matches = matcher.matches(tree, state); - return matches; - } - - @Override - public Fixer getFixer() { - return fixer; - } -} diff --git a/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/matcher/ToBuilderMatcher.java b/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/matcher/ToBuilderMatcher.java deleted file mode 100644 index 526b318d49..0000000000 --- a/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/matcher/ToBuilderMatcher.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2019, TeamDev. All rights reserved. - * - * Redistribution and use in source and/or binary forms, with or without - * modification, must retain the above copyright notice and the following - * disclaimer. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS - * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT - * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR - * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT - * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE - * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - */ - -package io.spine.tools.check.vbuilder.matcher; - -import com.google.errorprone.VisitorState; -import com.google.errorprone.matchers.Matcher; -import com.google.protobuf.Message; -import com.sun.source.tree.ExpressionTree; -import com.sun.source.tree.MethodInvocationTree; -import io.spine.annotation.Internal; -import io.spine.tools.check.BugPatternMatcher; -import io.spine.tools.check.Fixer; -import io.spine.tools.check.vbuilder.fixer.ToBuilderFixer; - -/** - * A matcher for the {@link io.spine.tools.check.vbuilder.UseValidatingBuilder} bug pattern which - * tracks down the cases where the {@code message.toBuilder()} statement is used. - * - * @see Message#toBuilder() - */ -@Internal -public class ToBuilderMatcher implements BugPatternMatcher { - - private static final String METHOD_NAME = "toBuilder"; - - private final Matcher matcher = - CustomProtobufType.callingInstanceMethod(METHOD_NAME); - private final Fixer fixer = new ToBuilderFixer(); - - @Override - public boolean matches(MethodInvocationTree tree, VisitorState state) { - boolean matches = matcher.matches(tree, state); - return matches; - } - - @Override - public Fixer getFixer() { - return fixer; - } -} diff --git a/tools/errorprone-checks/src/test/java/io/spine/tools/check/vbuilder/UseValidatingBuilderTest.java b/tools/errorprone-checks/src/test/java/io/spine/tools/check/vbuild/UseVBuildTest.java similarity index 77% rename from tools/errorprone-checks/src/test/java/io/spine/tools/check/vbuilder/UseValidatingBuilderTest.java rename to tools/errorprone-checks/src/test/java/io/spine/tools/check/vbuild/UseVBuildTest.java index 029ef97a5e..e339eb4b95 100644 --- a/tools/errorprone-checks/src/test/java/io/spine/tools/check/vbuilder/UseValidatingBuilderTest.java +++ b/tools/errorprone-checks/src/test/java/io/spine/tools/check/vbuild/UseVBuildTest.java @@ -18,16 +18,16 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package io.spine.tools.check.vbuilder; +package io.spine.tools.check.vbuild; -import com.google.common.base.Predicates; import com.google.errorprone.CompilationTestHelper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; -import java.util.function.Predicate; +import static io.spine.tools.check.vbuild.UseVBuild.NAME; +import static io.spine.tools.check.vbuild.UseVBuild.SUMMARY; /** * This test requires configuring "-Xbootclasspath..." option with the path to the @@ -50,32 +50,30 @@ * * guide to testing the custom checks. */ -@DisplayName("UseValidatingBuilder should") -@Disabled("Until https://github.com/SpineEventEngine/base/issues/263 is resolved") -class UseValidatingBuilderTest { +@Disabled +@DisplayName("UseVBuild check should") +class UseVBuildTest { private CompilationTestHelper compilationTestHelper; @BeforeEach void setUp() { compilationTestHelper = - CompilationTestHelper.newInstance(UseValidatingBuilder.class, getClass()); + CompilationTestHelper.newInstance(UseVBuild.class, getClass()); } @Test @DisplayName("recognize positive cases") void recognizePositiveCases() { - Predicate predicate = - Predicates.containsPattern(UseValidatingBuilder.SUMMARY)::apply; - compilationTestHelper.expectErrorMessage("UseValidatingBuilderError", predicate::test) - .addSourceFile("UseValidatingBuilderPositives.java") + compilationTestHelper.expectErrorMessage(NAME, msg -> msg.contains(SUMMARY)) + .addSourceFile("UseVBuildPositives.java") .doTest(); } @Test @DisplayName("recognize negative cases") void recognizeNegativeCases() { - compilationTestHelper.addSourceFile("UseValidatingBuilderNegatives.java") + compilationTestHelper.addSourceFile("UseVBuildNegatives.java") .doTest(); } } diff --git a/tools/errorprone-checks/src/test/resources/io/spine/tools/check/vbuilder/UseValidatingBuilderNegatives.java b/tools/errorprone-checks/src/test/resources/io/spine/tools/check/vbuild/UseVBuildNegatives.java similarity index 51% rename from tools/errorprone-checks/src/test/resources/io/spine/tools/check/vbuilder/UseValidatingBuilderNegatives.java rename to tools/errorprone-checks/src/test/resources/io/spine/tools/check/vbuild/UseVBuildNegatives.java index 62a6d53093..12f368419c 100644 --- a/tools/errorprone-checks/src/test/resources/io/spine/tools/check/vbuilder/UseValidatingBuilderNegatives.java +++ b/tools/errorprone-checks/src/test/resources/io/spine/tools/check/vbuild/UseVBuildNegatives.java @@ -18,53 +18,69 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package io.spine.tools.check.vbuilder; +package io.spine.tools.check.vbuild; import com.google.protobuf.AbstractMessage; +import com.google.protobuf.Empty; import io.spine.base.FieldPath; -import io.spine.base.FieldPathVBuilder; -import io.spine.validate.AbstractValidatingBuilder; +import io.spine.protobuf.ValidatingBuilder; -import static io.spine.base.ErrorVBuilder.newBuilder; +import java.util.function.Supplier; /** - * Contains statements for which the {@link UseValidatingBuilder} bug pattern should + * Contains statements for which the {@link UseVBuild} bug pattern should * generate no warning. */ -abstract class UseValidatingBuilderNegatives { +abstract class UseVBuildNegatives { - /** This method calls generated VBuilder. */ + /** This method calls the generated vBuild() method. */ void callOnVBuilder() { - FieldPathVBuilder.newBuilder(); - } - - /** This method calls statically imported method of generated VBuilder. */ - void callOnVBuilderStaticImported() { - newBuilder(); + FieldPath.newBuilder() + .vBuild(); } /** This method is annotated suppressing the warning. */ - @SuppressWarnings("UseValidatingBuilder") + @SuppressWarnings("UseVBuild") void callUnderWarningSuppressed() { - FieldPath.newBuilder(); + FieldPath.newBuilder() + .build(); } - class SomeBuilder extends AbstractValidatingBuilder { + /** This method calls buildPartial() to explititly state that the message is not validated. */ + void callBuildPartial() { + FieldPath.newBuilder() + .buildPartial(); + } - /** - * This method contains a call from a method, which is inside a class derived - * from {@code AbstractValidatingBuilder}. - */ - void callInsideVBuilder() { - FieldPath.newBuilder(); + abstract class SomeBuilder implements ValidatingBuilder { + + /** The call to builder is made inside a builder class. */ + void callInsideBuilder() { + FieldPath.newBuilder() + .build(); } + + void useMethodRefInsimeBuilder() { + Supplier sup = FieldPath.newBuilder()::build; + sup.get(); + } + + /** Added to satisfy the compiler. Does not affect the ErrorProne checks. */ + @Override + public abstract SomeBuilder clone(); } abstract class SomeMessage extends AbstractMessage { /** The call to builder is made inside a message class. */ void callInsideMessage() { - FieldPath.newBuilder(); + FieldPath.newBuilder() + .build(); + } + + void useMethodRefInsimeMessage() { + Supplier sup = FieldPath.newBuilder()::build; + sup.get(); } } } diff --git a/tools/errorprone-checks/src/test/resources/io/spine/tools/check/vbuilder/UseValidatingBuilderPositives.java b/tools/errorprone-checks/src/test/resources/io/spine/tools/check/vbuild/UseVBuildPositives.java similarity index 51% rename from tools/errorprone-checks/src/test/resources/io/spine/tools/check/vbuilder/UseValidatingBuilderPositives.java rename to tools/errorprone-checks/src/test/resources/io/spine/tools/check/vbuild/UseVBuildPositives.java index 5bac3e3c6f..267a355223 100644 --- a/tools/errorprone-checks/src/test/resources/io/spine/tools/check/vbuilder/UseValidatingBuilderPositives.java +++ b/tools/errorprone-checks/src/test/resources/io/spine/tools/check/vbuild/UseVBuildPositives.java @@ -18,57 +18,42 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package io.spine.tools.check.vbuilder; +package io.spine.tools.check.vbuild; -import io.spine.base.FieldPath; +import com.google.protobuf.Message; import io.spine.base.Error; -import static io.spine.base.FieldPath.newBuilder; +import java.util.function.Supplier; /** - * Contains statements for which the {@link UseValidatingBuilder} bug pattern should return a match. + * Contains statements for which the {@link UseVBuild} bug pattern should return a match. * *

Comments in this file should not be modified as they serve as indicator for the * {@link com.google.errorprone.CompilationTestHelper} Error Prone tool. */ -class UseValidatingBuilderPositives { +class UseVBuildPositives { - Error value = Error.getDefaultInstance(); + void callBuild() { - void callNewBuilder() { - - // BUG: Diagnostic matches: UseValidatingBuilderError - Error.newBuilder(); - } - - void callNewBuilderWithArg() { - - // BUG: Diagnostic matches: UseValidatingBuilderError - Error.newBuilder(value); - } - - void callNewBuilderForType() { - - // BUG: Diagnostic matches: UseValidatingBuilderError - value.newBuilderForType(); - } - - void callToBuilder() { - - // BUG: Diagnostic matches: UseValidatingBuilderError - value.toBuilder(); + // BUG: Diagnostic matches: UseVBuild + Error.newBuilder().build(); } - void callNewBuilderStaticImported() { + void callAsMethodReference() { + Error.Builder builder = Error.newBuilder(); - // BUG: Diagnostic matches: UseValidatingBuilderError - newBuilder(); + // BUG: Diagnostic matches: UseVBuild + Supplier faultySupplier = builder::build; + faultySupplier.get(); } - void callNewBuilderWithArgStaticImported() { - FieldPath defaultInstance = FieldPath.getDefaultInstance(); + void callInLambda() { + Error.Builder builder = Error.newBuilder(); - // BUG: Diagnostic matches: UseValidatingBuilderError - newBuilder(defaultInstance); + Supplier faultySupplier = () -> { + // BUG: Diagnostic matches: UseVBuild + return builder.build(); + }; + faultySupplier.get(); } } diff --git a/tools/model-compiler/src/main/java/io/spine/tools/compiler/rejection/RejectionBuilderWriter.java b/tools/model-compiler/src/main/java/io/spine/tools/compiler/rejection/RejectionBuilderWriter.java index 2309855f5a..d238e38900 100644 --- a/tools/model-compiler/src/main/java/io/spine/tools/compiler/rejection/RejectionBuilderWriter.java +++ b/tools/model-compiler/src/main/java/io/spine/tools/compiler/rejection/RejectionBuilderWriter.java @@ -210,7 +210,6 @@ private MethodSpec fieldSetter(FieldDeclaration field, FieldType fieldType) { String parameterName = fieldName.javaCase(); String methodName = fieldType.primarySetterTemplate() .format(io.spine.code.gen.java.FieldName.from(fieldName)); - @SuppressWarnings("DuplicateStringLiteralInspection") // different semantics of gen'ed code. MethodSpec.Builder methodBuilder = MethodSpec .methodBuilder(methodName) .addModifiers(PUBLIC) diff --git a/tools/model-compiler/src/main/java/io/spine/tools/compiler/validation/ConvertStatement.java b/tools/model-compiler/src/main/java/io/spine/tools/compiler/validation/ConvertStatement.java index 2a418c95c9..b017e0081d 100644 --- a/tools/model-compiler/src/main/java/io/spine/tools/compiler/validation/ConvertStatement.java +++ b/tools/model-compiler/src/main/java/io/spine/tools/compiler/validation/ConvertStatement.java @@ -25,7 +25,7 @@ import io.spine.code.proto.FieldName; import static com.google.common.base.Preconditions.checkNotNull; -import static io.spine.validate.Validate.checkNameNotEmptyOrBlank; +import static io.spine.util.Preconditions2.checkNotEmptyOrBlank; /** * A statement to convert a {@linkplain String raw} value. @@ -38,7 +38,7 @@ final class ConvertStatement { private final TypeName type; private ConvertStatement(String variableName, TypeName type) { - this.variableName = checkNameNotEmptyOrBlank(variableName); + this.variableName = checkNotEmptyOrBlank(variableName); this.type = checkNotNull(type); } diff --git a/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/CompositeGenerator.java b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/CompositeGenerator.java new file mode 100644 index 0000000000..6f73f3f9b8 --- /dev/null +++ b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/CompositeGenerator.java @@ -0,0 +1,89 @@ +/* + * Copyright 2019, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.tools.protoc; + +import com.google.common.collect.ImmutableList; +import io.spine.type.Type; + +import java.util.Collection; +import java.util.List; + +import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableSet.toImmutableSet; +import static com.google.common.collect.Lists.newArrayList; + +/** + * A generator which calls other generators and merges their results. + */ +public final class CompositeGenerator extends SpineProtoGenerator { + + private final ImmutableList generators; + + private CompositeGenerator(Builder builder) { + super(); + this.generators = ImmutableList.copyOf(builder.generators); + } + + @Override + protected Collection generate(Type type) { + return generators.stream() + .flatMap(generator -> generator.generate(type).stream()) + .collect(toImmutableSet()); + } + + /** + * Creates a new instance of {@code Builder} for {@code CompositeGenerator} instances. + * + * @return new instance of {@code Builder} + */ + public static Builder builder() { + return new Builder(); + } + + /** + * A builder for the {@code CompositeGenerator} instances. + */ + public static final class Builder { + + private final List generators = newArrayList(); + + /** + * Prevents direct instantiation. + */ + private Builder() { + } + + public Builder add(SpineProtoGenerator generator) { + checkNotNull(generator); + generators.add(generator); + return this; + } + + /** + * Creates a new instance of {@code CompositeGenerator}. + * + * @return new instance of {@code CompositeGenerator} + */ + public CompositeGenerator build() { + return new CompositeGenerator(this); + } + } +} diff --git a/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/IdentityParameter.java b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/IdentityParameter.java similarity index 93% rename from tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/IdentityParameter.java rename to tools/protoc-plugin/src/main/java/io/spine/tools/protoc/IdentityParameter.java index 2bdfdbdeb4..fc33763e6f 100644 --- a/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/IdentityParameter.java +++ b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/IdentityParameter.java @@ -18,7 +18,7 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package io.spine.tools.protoc.iface; +package io.spine.tools.protoc; import com.google.errorprone.annotations.Immutable; import io.spine.type.Type; @@ -30,7 +30,7 @@ * parameter will be {@code ProjectId}. */ @Immutable -final class IdentityParameter implements MessageInterfaceParameter { +public final class IdentityParameter implements TypeParameter { @Override public String valueFor(Type type) { diff --git a/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/Plugin.java b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/Plugin.java index 0630f690ea..236b1cf7dc 100644 --- a/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/Plugin.java +++ b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/Plugin.java @@ -24,6 +24,7 @@ import com.google.protobuf.compiler.PluginProtos.CodeGeneratorRequest; import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse; import io.spine.code.proto.OptionExtensionRegistry; +import io.spine.tools.protoc.builder.BuilderGenerator; import io.spine.tools.protoc.iface.InterfaceGenerator; import io.spine.tools.protoc.method.MethodGenerator; @@ -60,9 +61,12 @@ private Plugin() { public static void main(String[] args) { CodeGeneratorRequest request = readRequest(); SpineProtocConfig config = readConfig(request); - SpineProtoGenerator generator = InterfaceGenerator - .instance(config) - .linkWith(MethodGenerator.instance(config)); + CompositeGenerator generator = CompositeGenerator + .builder() + .add(InterfaceGenerator.instance(config)) + .add(MethodGenerator.instance(config)) + .add(BuilderGenerator.instance()) + .build(); CodeGeneratorResponse response = generator.process(request); writeResponse(response); } diff --git a/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/SpineProtoGenerator.java b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/SpineProtoGenerator.java index 39be436076..bc3f9c1738 100644 --- a/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/SpineProtoGenerator.java +++ b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/SpineProtoGenerator.java @@ -20,7 +20,6 @@ package io.spine.tools.protoc; -import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.ImmutableSet; import com.google.protobuf.DescriptorProtos.DescriptorProto; import com.google.protobuf.DescriptorProtos.FileDescriptorProto; @@ -32,7 +31,6 @@ import io.spine.code.proto.FileSet; import io.spine.code.proto.TypeSet; import io.spine.type.Type; -import org.checkerframework.checker.nullness.qual.Nullable; import java.util.Collection; import java.util.List; @@ -42,7 +40,6 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.collect.Lists.newArrayListWithExpectedSize; -import static com.google.common.collect.Sets.newHashSet; import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.partitioningBy; import static java.util.stream.Collectors.reducing; @@ -84,8 +81,6 @@ */ public abstract class SpineProtoGenerator { - private @Nullable SpineProtoGenerator linkedGenerator; - protected SpineProtoGenerator() { } @@ -122,21 +117,6 @@ public final CodeGeneratorResponse process(CodeGeneratorRequest request) { return response; } - /** - * Links current proto generator with a next one and returns the current one. - * - *

A linked generator is activate prior to the current in the generation chain. - * - *

All generated files are than merged into one response. - * - * @see #process(TypeSet) - */ - public final SpineProtoGenerator linkWith(SpineProtoGenerator nextGenerator) { - checkNotNull(nextGenerator); - this.linkedGenerator = nextGenerator; - return this; - } - /** * Processes a single type and generates from zero to many {@link CompilerOutput} instances in * response to the type. @@ -182,17 +162,12 @@ private CodeGeneratorResponse process(TypeSet types) { * Generates code for the supplied types. */ private Set generate(TypeSet types) { - Set result = newHashSet(); - if (linkedGenerator != null) { - result.addAll(linkedGenerator.generate(types)); - } - Set rawOutput = types.allTypes() - .stream() - .map(this::generate) - .flatMap(Collection::stream) - .collect(toSet()); - result.addAll(rawOutput); - return result; + return types.allTypes() + .stream() + .map(this::generate) + .flatMap(Collection::stream) + .collect(toSet()); + } /** @@ -238,9 +213,4 @@ private static File concatContent(File left, File right) { .setContent(left.getContent() + right.getContent()) .build(); } - - @VisibleForTesting - @Nullable SpineProtoGenerator linkedGenerator() { - return linkedGenerator; - } } diff --git a/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/MessageInterfaceParameter.java b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/TypeParameter.java similarity index 93% rename from tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/MessageInterfaceParameter.java rename to tools/protoc-plugin/src/main/java/io/spine/tools/protoc/TypeParameter.java index 58e708914f..adcf8b35c0 100644 --- a/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/MessageInterfaceParameter.java +++ b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/TypeParameter.java @@ -18,9 +18,10 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package io.spine.tools.protoc.iface; +package io.spine.tools.protoc; import com.google.errorprone.annotations.Immutable; +import io.spine.tools.protoc.iface.MessageInterface; import io.spine.type.Type; /** @@ -29,7 +30,7 @@ *

Parameter value is presented as {@code String} for usage in the generated code. */ @Immutable -interface MessageInterfaceParameter { +public interface TypeParameter { /** * Obtains a parameter value based on who is the message interface descendant. diff --git a/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/MessageInterfaceParameters.java b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/TypeParameters.java similarity index 70% rename from tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/MessageInterfaceParameters.java rename to tools/protoc-plugin/src/main/java/io/spine/tools/protoc/TypeParameters.java index 7b6a7f741e..a1d17eac28 100644 --- a/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/MessageInterfaceParameters.java +++ b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/TypeParameters.java @@ -18,37 +18,38 @@ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ -package io.spine.tools.protoc.iface; +package io.spine.tools.protoc; import com.google.common.collect.ImmutableList; import com.google.errorprone.annotations.Immutable; +import io.spine.tools.protoc.iface.MessageInterface; import io.spine.type.Type; -import java.util.stream.Collectors; +import static java.util.stream.Collectors.joining; /** * The generic parameters of the {@link MessageInterface}. * *

Contrary to the type information contained in a {@link Class} instance, the - * {@code MessageInterfaceParameter} carries the logic on how to initialize itself based on the + * {@code TypeParameter} carries the logic on how to initialize itself based on the * message interface descendant. */ @Immutable -final class MessageInterfaceParameters { +public final class TypeParameters { - private final ImmutableList params; + private final ImmutableList params; - private MessageInterfaceParameters(ImmutableList params) { + private TypeParameters(ImmutableList params) { this.params = params; } - static MessageInterfaceParameters of(MessageInterfaceParameter... parameters) { - ImmutableList params = ImmutableList.copyOf(parameters); - return new MessageInterfaceParameters(params); + public static TypeParameters of(TypeParameter... parameters) { + ImmutableList params = ImmutableList.copyOf(parameters); + return new TypeParameters(params); } - static MessageInterfaceParameters empty() { - return new MessageInterfaceParameters(ImmutableList.of()); + public static TypeParameters empty() { + return new TypeParameters(ImmutableList.of()); } /** @@ -58,7 +59,7 @@ static MessageInterfaceParameters empty() { * *

Example output: {@code }. */ - String getAsStringFor(Type type) { + public String asStringFor(Type type) { if (params.isEmpty()) { return ""; } @@ -69,6 +70,6 @@ String getAsStringFor(Type type) { private String initParams(Type type) { return params.stream() .map(param -> param.valueFor(type)) - .collect(Collectors.joining(", ")); + .collect(joining(", ")); } } diff --git a/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/builder/BuilderGenerator.java b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/builder/BuilderGenerator.java new file mode 100644 index 0000000000..96a3998270 --- /dev/null +++ b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/builder/BuilderGenerator.java @@ -0,0 +1,62 @@ +/* + * Copyright 2019, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.tools.protoc.builder; + +import com.google.common.collect.ImmutableSet; +import io.spine.tools.protoc.CompilerOutput; +import io.spine.tools.protoc.SpineProtoGenerator; +import io.spine.type.MessageType; +import io.spine.type.Type; + +import java.util.Collection; + +import static io.spine.tools.protoc.builder.BuilderImplements.implementValidatingBuilder; + +/** + * A code generator which makes the generated message builders implement + * {@link io.spine.protobuf.ValidatingBuilder}. + */ +public final class BuilderGenerator extends SpineProtoGenerator { + + /** + * Prevents direct instantiation. + */ + private BuilderGenerator() { + super(); + } + + /** + * Creates a new instance of the generator. + */ + public static BuilderGenerator instance() { + return new BuilderGenerator(); + } + + @Override + protected Collection generate(Type type) { + if (type instanceof MessageType) { + CompilerOutput insertionPoint = implementValidatingBuilder((MessageType) type); + return ImmutableSet.of(insertionPoint); + } else { + return ImmutableSet.of(); + } + } +} diff --git a/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/builder/BuilderImplements.java b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/builder/BuilderImplements.java new file mode 100644 index 0000000000..6126a8ad3a --- /dev/null +++ b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/builder/BuilderImplements.java @@ -0,0 +1,61 @@ +/* + * Copyright 2019, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.tools.protoc.builder; + +import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse.File; +import io.spine.protobuf.ValidatingBuilder; +import io.spine.tools.protoc.AbstractCompilerOutput; +import io.spine.tools.protoc.IdentityParameter; +import io.spine.tools.protoc.InsertionPoint; +import io.spine.tools.protoc.ProtocPluginFiles; +import io.spine.tools.protoc.TypeParameters; +import io.spine.type.MessageType; + +import static java.lang.String.format; + +/** + * An insertion point which adds the {@link ValidatingBuilder} interface to the list of implemented + * interfaces of the {@code Builder} of the given message type. + */ +final class BuilderImplements extends AbstractCompilerOutput { + + private static final String INTERFACE_NAME_TEMPLATE = "%s%s,"; + + private BuilderImplements(File file) { + super(file); + } + + static BuilderImplements implementValidatingBuilder(MessageType targetType) { + String insertionPointName = InsertionPoint.builder_implements.forType(targetType); + String content = mixinFor(targetType); + File file = ProtocPluginFiles.prepareFile(targetType) + .setInsertionPoint(insertionPointName) + .setContent(content) + .build(); + return new BuilderImplements(file); + } + + private static String mixinFor(MessageType type) { + String generic = TypeParameters.of(new IdentityParameter()) + .asStringFor(type); + return format(INTERFACE_NAME_TEMPLATE, ValidatingBuilder.class.getName(), generic); + } +} diff --git a/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/builder/package-info.java b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/builder/package-info.java new file mode 100644 index 0000000000..6fd4895605 --- /dev/null +++ b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/builder/package-info.java @@ -0,0 +1,27 @@ +/* + * Copyright 2019, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +@CheckReturnValue +@ParametersAreNonnullByDefault +package io.spine.tools.protoc.builder; + +import com.google.errorprone.annotations.CheckReturnValue; + +import javax.annotation.ParametersAreNonnullByDefault; diff --git a/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/CustomMessageInterface.java b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/CustomMessageInterface.java index 8aa2f427fe..e4809f55ff 100644 --- a/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/CustomMessageInterface.java +++ b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/CustomMessageInterface.java @@ -25,6 +25,7 @@ import io.spine.code.fs.java.SourceFile; import io.spine.tools.protoc.AbstractCompilerOutput; import io.spine.tools.protoc.ProtocPluginFiles; +import io.spine.tools.protoc.TypeParameters; import static com.google.common.base.Preconditions.checkNotNull; @@ -71,7 +72,7 @@ public String name() { * Generic params are currently not supported for user-defined message interfaces. */ @Override - public MessageInterfaceParameters parameters() { - return MessageInterfaceParameters.empty(); + public TypeParameters parameters() { + return TypeParameters.empty(); } } diff --git a/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/GenerateUuidInterfaces.java b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/GenerateUuidInterfaces.java index 9619c148d6..4667f8d8b3 100644 --- a/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/GenerateUuidInterfaces.java +++ b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/GenerateUuidInterfaces.java @@ -22,6 +22,8 @@ import com.google.common.collect.ImmutableList; import io.spine.tools.protoc.CompilerOutput; +import io.spine.tools.protoc.IdentityParameter; +import io.spine.tools.protoc.TypeParameters; import io.spine.tools.protoc.UuidConfig; import io.spine.type.MessageType; @@ -50,7 +52,7 @@ public ImmutableList generateFor(MessageType type) { } @Override - MessageInterfaceParameters interfaceParameters() { - return MessageInterfaceParameters.of(new IdentityParameter()); + TypeParameters interfaceParameters() { + return TypeParameters.of(new IdentityParameter()); } } diff --git a/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/InterfaceGenerationTask.java b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/InterfaceGenerationTask.java index 06ba3c2d4b..ee83f6c49f 100644 --- a/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/InterfaceGenerationTask.java +++ b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/InterfaceGenerationTask.java @@ -24,6 +24,7 @@ import io.spine.code.java.ClassName; import io.spine.tools.protoc.CodeGenerationTask; import io.spine.tools.protoc.CompilerOutput; +import io.spine.tools.protoc.TypeParameters; import io.spine.type.MessageType; import static io.spine.tools.protoc.iface.MessageImplements.implementInterface; @@ -43,8 +44,8 @@ abstract class InterfaceGenerationTask implements CodeGenerationTask { /** * Creates {@link MessageInterface} parameters. */ - MessageInterfaceParameters interfaceParameters() { - return MessageInterfaceParameters.empty(); + TypeParameters interfaceParameters() { + return TypeParameters.empty(); } /** diff --git a/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/MessageImplements.java b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/MessageImplements.java index 4141ecb799..8224e09633 100644 --- a/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/MessageImplements.java +++ b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/MessageImplements.java @@ -20,11 +20,11 @@ package io.spine.tools.protoc.iface; -import com.google.common.annotations.VisibleForTesting; import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse.File; import io.spine.tools.protoc.AbstractCompilerOutput; import io.spine.tools.protoc.InsertionPoint; import io.spine.tools.protoc.ProtocPluginFiles; +import io.spine.tools.protoc.TypeParameters; import io.spine.type.MessageType; import io.spine.type.Type; @@ -34,9 +34,6 @@ */ final class MessageImplements extends AbstractCompilerOutput { - @VisibleForTesting - static final String INSERTION_POINT_IMPLEMENTS = "message_implements:%s"; - private MessageImplements(File file) { super(file); } @@ -74,8 +71,8 @@ private static String buildContent(MessageType type, MessageInterface messageInt private static String initGenericParams(MessageInterface messageInterface, Type type) { - MessageInterfaceParameters parameters = messageInterface.parameters(); - String result = parameters.getAsStringFor(type); + TypeParameters parameters = messageInterface.parameters(); + String result = parameters.asStringFor(type); return result; } } diff --git a/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/MessageInterface.java b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/MessageInterface.java index 9722071be3..3799120c04 100644 --- a/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/MessageInterface.java +++ b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/MessageInterface.java @@ -20,6 +20,8 @@ package io.spine.tools.protoc.iface; +import io.spine.tools.protoc.TypeParameters; + /** * An interface to be implemented by the Protobuf message. * @@ -36,5 +38,5 @@ public interface MessageInterface { /** * Obtains the generic params of the interface. */ - MessageInterfaceParameters parameters(); + TypeParameters parameters(); } diff --git a/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/PredefinedInterface.java b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/PredefinedInterface.java index 3e4954bb7a..b8a4a8ac06 100644 --- a/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/PredefinedInterface.java +++ b/tools/protoc-plugin/src/main/java/io/spine/tools/protoc/iface/PredefinedInterface.java @@ -21,6 +21,7 @@ package io.spine.tools.protoc.iface; import io.spine.code.java.ClassName; +import io.spine.tools.protoc.TypeParameters; /** * An interface which already exists. @@ -28,10 +29,10 @@ final class PredefinedInterface implements MessageInterface { private final ClassName name; - private final MessageInterfaceParameters parameters; + private final TypeParameters parameters; PredefinedInterface(ClassName name, - MessageInterfaceParameters parameters) { + TypeParameters parameters) { this.name = name; this.parameters = parameters; } @@ -42,7 +43,7 @@ public String name() { } @Override - public MessageInterfaceParameters parameters() { + public TypeParameters parameters() { return parameters; } } diff --git a/tools/protoc-plugin/src/test/java/io/spine/tools/protoc/PluginTest.java b/tools/protoc-plugin/src/test/java/io/spine/tools/protoc/PluginTest.java index 645de6cf51..e1bb9604cd 100644 --- a/tools/protoc-plugin/src/test/java/io/spine/tools/protoc/PluginTest.java +++ b/tools/protoc-plugin/src/test/java/io/spine/tools/protoc/PluginTest.java @@ -22,8 +22,11 @@ import com.google.protobuf.compiler.PluginProtos.CodeGeneratorRequest; import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse; +import com.google.protobuf.compiler.PluginProtos.CodeGeneratorResponse.File; +import io.spine.code.fs.java.SourceFile; import io.spine.code.java.ClassName; import io.spine.code.proto.OptionExtensionRegistry; +import io.spine.protobuf.ValidatingBuilder; import io.spine.tools.gradle.compiler.protoc.GeneratedInterfaces; import io.spine.tools.gradle.compiler.protoc.GeneratedMethods; import io.spine.tools.gradle.compiler.protoc.MessageSelectorFactory; @@ -32,11 +35,13 @@ import io.spine.tools.protoc.given.TestMethodFactory; import io.spine.tools.protoc.given.UuidMethodFactory; import io.spine.tools.protoc.method.TestMethodProtos; +import io.spine.type.MessageType; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.junitpioneer.jupiter.TempDirectory; +import org.junitpioneer.jupiter.TempDirectory.TempDir; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; @@ -45,13 +50,15 @@ import java.io.PrintStream; import java.nio.file.Path; import java.util.List; -import java.util.stream.Collectors; +import static com.google.common.collect.Iterables.getOnlyElement; +import static com.google.common.truth.Truth.assertThat; import static io.spine.tools.gradle.compiler.protoc.MessageSelectorFactory.prefix; import static io.spine.tools.gradle.compiler.protoc.MessageSelectorFactory.regex; import static io.spine.tools.gradle.compiler.protoc.MessageSelectorFactory.suffix; import static io.spine.tools.protoc.given.CodeGeneratorRequestGiven.protocConfig; import static io.spine.tools.protoc.given.CodeGeneratorRequestGiven.requestBuilder; +import static java.util.stream.Collectors.toList; import static org.junit.jupiter.api.Assertions.assertEquals; @ExtendWith(TempDirectory.class) @@ -62,11 +69,14 @@ final class PluginTest { private static final String TEST_PROTO_PREFIX = "spine/tools/protoc/test_"; private static final String TEST_PROTO_REGEX = ".*protoc/.*rators.pro.*"; private static final String TEST_PROTO_FILE = "spine/tools/protoc/test_generators.proto"; + private static final String TEST_MESSAGE_TYPE_PARAMETER = ""; + private static final String BUILDER_INTERFACE = + ValidatingBuilder.class.getName() + TEST_MESSAGE_TYPE_PARAMETER + ','; private Path testPluginConfig; @BeforeEach - void setUp(@TempDirectory.TempDir Path tempDirPath) { + void setUp(@TempDir Path tempDirPath) { testPluginConfig = tempDirPath.resolve("test-spine-protoc-plugin.pb"); } @@ -87,12 +97,7 @@ void processSuffixPatterns() { .build(); CodeGeneratorResponse response = runPlugin(request); - - assertEquals(2, response.getFileCount()); - CodeGeneratorResponse.File messageInterface = response.getFile(0); - CodeGeneratorResponse.File messageMethod = response.getFile(1); - assertEquals(TestInterface.class.getName() + ',', messageInterface.getContent()); - assertEquals(TestMethodFactory.TEST_METHOD.value(), messageMethod.getContent()); + checkGenerated(response); } @DisplayName("generate UUID message") @@ -110,7 +115,7 @@ void generateUuidMethod() { .build(); CodeGeneratorResponse response = runPlugin(request); - List messageMethods = + List messageMethods = filterMethods(response, InsertionPoint.class_scope); assertEquals(1, messageMethods.size()); } @@ -133,12 +138,7 @@ void processPrefixPatterns() { .build(); CodeGeneratorResponse response = runPlugin(request); - - assertEquals(2, response.getFileCount()); - CodeGeneratorResponse.File messageInterface = response.getFile(0); - CodeGeneratorResponse.File messageMethod = response.getFile(1); - assertEquals(TestInterface.class.getName() + ',', messageInterface.getContent()); - assertEquals(TestMethodFactory.TEST_METHOD.value(), messageMethod.getContent()); + checkGenerated(response); } @Test @@ -158,22 +158,36 @@ void processRegexPatterns() { .build(); CodeGeneratorResponse response = runPlugin(request); + checkGenerated(response); + } - assertEquals(2, response.getFileCount()); - CodeGeneratorResponse.File messageInterface = response.getFile(0); - CodeGeneratorResponse.File messageMethod = response.getFile(1); - assertEquals(TestInterface.class.getName() + ',', messageInterface.getContent()); - assertEquals(TestMethodFactory.TEST_METHOD.value(), messageMethod.getContent()); + @Test + @DisplayName("mark generated message builders with the ValidatingBuilder interface") + void markBuildersWithInterface() { + CodeGeneratorRequest request = requestBuilder() + .addProtoFile(TestGeneratorsProto.getDescriptor() + .toProto()) + .addFileToGenerate(TEST_PROTO_FILE) + .setParameter(protocConfig(new GeneratedInterfaces(), new GeneratedMethods(), + testPluginConfig)) + .build(); + CodeGeneratorResponse response = runPlugin(request); + File insertionPoint = getOnlyElement(response.getFileList()); + assertThat(insertionPoint.getContent()).isEqualTo(BUILDER_INTERFACE); + MessageType type = new MessageType(EnhancedWithCodeGeneration.getDescriptor()); + String expectedPointName = InsertionPoint.builder_implements.forType(type); + assertThat(insertionPoint.getInsertionPoint()).isEqualTo(expectedPointName); + SourceFile expectedFile = SourceFile.forType(type); + assertThat(insertionPoint.getName()).isEqualTo(expectedFile.toString()); } - private static List filterMethods(CodeGeneratorResponse response, - InsertionPoint insertionPoint) { + private static List filterMethods(CodeGeneratorResponse response, + InsertionPoint insertionPoint) { return response .getFileList() .stream() - .filter(file -> file.getInsertionPoint() - .contains(insertionPoint.getDefinition())) - .collect(Collectors.toList()); + .filter(file -> file.getInsertionPoint().contains(insertionPoint.getDefinition())) + .collect(toList()); } @SuppressWarnings("ZeroLengthArrayAllocation") @@ -203,4 +217,21 @@ private static void withSystemStreams(InputStream in, PrintStream os, Runnable a System.setOut(oldOut); } } + + private static void checkGenerated(CodeGeneratorResponse response) { + List responseFiles = response.getFileList(); + assertThat(responseFiles).hasSize(3); + List fileContents = contentsOf(responseFiles); + assertThat(fileContents).containsExactly( + TestInterface.class.getName() + ',', + TestMethodFactory.TEST_METHOD.value(), + BUILDER_INTERFACE + ); + } + + private static List contentsOf(List files) { + return files.stream() + .map(File::getContent) + .collect(toList()); + } } diff --git a/tools/protoc-plugin/src/test/java/io/spine/tools/protoc/SpineProtoGeneratorTest.java b/tools/protoc-plugin/src/test/java/io/spine/tools/protoc/SpineProtoGeneratorTest.java index 3f7e0cbc55..51c5258ade 100644 --- a/tools/protoc-plugin/src/test/java/io/spine/tools/protoc/SpineProtoGeneratorTest.java +++ b/tools/protoc-plugin/src/test/java/io/spine/tools/protoc/SpineProtoGeneratorTest.java @@ -47,7 +47,6 @@ import static io.spine.tools.protoc.given.CodeGeneratorRequestGiven.protocConfig; import static io.spine.tools.protoc.given.CodeGeneratorRequestGiven.requestBuilder; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertSame; @ExtendWith(TempDirectory.class) @@ -63,18 +62,6 @@ void setUp(@TempDirectory.TempDir Path tempDirPath) { testPluginConfig = tempDirPath.resolve("test-spine-protoc-plugin.pb"); } - @DisplayName("link with another generator") - @Test - void linkAnotherGenerator() { - TestGenerator generator = new TestGenerator(); - TestGenerator secondGenerator = new TestGenerator(); - - SpineProtoGenerator sameGenerator = generator.linkWith(secondGenerator); - assertSame(generator, sameGenerator); - assertSame(secondGenerator, generator.linkedGenerator()); - assertNull(secondGenerator.linkedGenerator()); - } - @DisplayName("process valid CodeGeneratorRequest") @Test void processValidRequest() { @@ -102,13 +89,9 @@ void processValidRequest() { .setContent("public void test(){}") .setInsertionPoint(InsertionPoint.class_scope.forType(type)) .build(); - TestGenerator firstGenerator = - new TestGenerator(ImmutableList.of(new TestCompilerOutput(firstFile))); - TestGenerator secondGenerator = - new TestGenerator(ImmutableList.of(new TestCompilerOutput(secondFile))); - - CodeGeneratorResponse result = firstGenerator.linkWith(secondGenerator) - .process(request); + TestGenerator firstGenerator = new TestGenerator(new TestCompilerOutput(firstFile), + new TestCompilerOutput(secondFile)); + CodeGeneratorResponse result = firstGenerator.process(request); assertEquals(2, result.getFileCount()); assertSame(firstFile, result.getFile(0)); assertSame(secondFile, result.getFile(1)); @@ -124,7 +107,7 @@ void concatenateGeneratedCode() { .addProtoFile(TestGeneratorsProto.getDescriptor() .toProto()) .addFileToGenerate(TEST_PROTO_FILE) - .setParameter(protocConfig(methods, testPluginConfig).toString()) + .setParameter(protocConfig(methods, testPluginConfig)) .build(); MessageType type = new MessageType(EnhancedWithCodeGeneration.getDescriptor()); String firstMethod = "public void test1(){}"; @@ -162,7 +145,7 @@ void dropCodeDuplicates() { .addProtoFile(TestGeneratorsProto.getDescriptor() .toProto()) .addFileToGenerate(TEST_PROTO_FILE) - .setParameter(protocConfig(methods, testPluginConfig).toString()) + .setParameter(protocConfig(methods, testPluginConfig)) .build(); MessageType type = new MessageType(EnhancedWithCodeGeneration.getDescriptor()); String method = "public void test1(){}"; @@ -211,6 +194,10 @@ private TestGenerator() { this(ImmutableList.of()); } + private TestGenerator(CompilerOutput... outputs) { + this(ImmutableList.copyOf(outputs)); + } + private TestGenerator(ImmutableList outputs) { compilerOutputs = outputs; } diff --git a/tools/protoc-plugin/src/test/java/io/spine/tools/protoc/iface/InterfaceGeneratorTest.java b/tools/protoc-plugin/src/test/java/io/spine/tools/protoc/iface/InterfaceGeneratorTest.java index 9614200b24..4805e60898 100644 --- a/tools/protoc-plugin/src/test/java/io/spine/tools/protoc/iface/InterfaceGeneratorTest.java +++ b/tools/protoc-plugin/src/test/java/io/spine/tools/protoc/iface/InterfaceGeneratorTest.java @@ -49,7 +49,6 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import static io.spine.tools.protoc.iface.MessageImplements.INSERTION_POINT_IMPLEMENTS; import static java.lang.String.format; import static java.util.regex.Pattern.compile; import static java.util.stream.Collectors.toSet; @@ -62,6 +61,7 @@ @DisplayName("InterfaceGenerator should") final class InterfaceGeneratorTest { + private static final String INSERTION_POINT_IMPLEMENTS = "message_implements:%s"; private static final String PROTO_PACKAGE = "spine.tools.protoc.iface."; private static final PackageName PACKAGE_NAME = diff --git a/tools/smoke-tests/validation-tests/src/test/java/io/spine/test/protobuf/ValidatingBuilderTest.java b/tools/smoke-tests/validation-tests/src/test/java/io/spine/test/protobuf/ValidatingBuilderTest.java new file mode 100644 index 0000000000..07f484b91f --- /dev/null +++ b/tools/smoke-tests/validation-tests/src/test/java/io/spine/test/protobuf/ValidatingBuilderTest.java @@ -0,0 +1,62 @@ +/* + * Copyright 2019, TeamDev. All rights reserved. + * + * Redistribution and use in source and/or binary forms, with or without + * modification, must retain the above copyright notice and the following + * disclaimer. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package io.spine.test.protobuf; + +import io.spine.validate.ValidationException; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static com.google.common.truth.Truth.assertThat; +import static io.spine.validate.Validate.checkValid; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@DisplayName("ValidatingBuilder should") +class ValidatingBuilderTest { + + private final CardNumber.Builder valid = CardNumber + .newBuilder() + .setDigits("0000 0000 0000 0000"); + private final CardNumber.Builder invalid = CardNumber + .newBuilder() + .setDigits("zazazazazazaz"); + + @Test + @DisplayName("obtain a valid message") + void obtainValid() { + CardNumber number = valid.vBuild(); + checkValid(number); + } + + @Test + @DisplayName("throw ValidationException if the message is not valid") + void throwIfInvalid() { + ValidationException exception = assertThrows(ValidationException.class, invalid::vBuild); + assertThat(exception.getConstraintViolations()).isNotEmpty(); + } + + @Test + @DisplayName("ignore invalid message when skipping validation intentionally") + void ignoreIfPartial() { + CardNumber number = invalid.buildPartial(); + assertThat(number).isNotNull(); + assertThrows(ValidationException.class, () -> checkValid(number)); + } +} diff --git a/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/matcher/package-info.java b/tools/smoke-tests/validation-tests/src/test/java/io/spine/test/protobuf/package-info.java similarity index 84% rename from tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/matcher/package-info.java rename to tools/smoke-tests/validation-tests/src/test/java/io/spine/test/protobuf/package-info.java index 4145275921..62a65765c9 100644 --- a/tools/errorprone-checks/src/main/java/io/spine/tools/check/vbuilder/matcher/package-info.java +++ b/tools/smoke-tests/validation-tests/src/test/java/io/spine/test/protobuf/package-info.java @@ -19,12 +19,12 @@ */ /** - * This package contains the {@linkplain io.spine.tools.check.BugPatternMatcher matchers} for the - * {@link io.spine.tools.check.vbuilder.UseValidatingBuilder} bug pattern. + * Contains the test suite for the Spine validating builder code generation. */ + @CheckReturnValue @ParametersAreNonnullByDefault -package io.spine.tools.check.vbuilder.matcher; +package io.spine.test.protobuf; import com.google.errorprone.annotations.CheckReturnValue; diff --git a/tools/smoke-tests/validation-tests/src/test/proto/spine/test/protobuf/validating_builder_test.proto b/tools/smoke-tests/validation-tests/src/test/proto/spine/test/protobuf/validating_builder_test.proto new file mode 100644 index 0000000000..3948723660 --- /dev/null +++ b/tools/smoke-tests/validation-tests/src/test/proto/spine/test/protobuf/validating_builder_test.proto @@ -0,0 +1,15 @@ +syntax = "proto3"; + +package spine.test.protobuf; + +import "spine/options.proto"; + +option (type_url_prefix) = "type.spine.op"; +option java_package = "io.spine.test.protobuf"; +option java_outer_classname = "ValidatingBuilderTestProto"; +option java_multiple_files = true; + +message CardNumber { + + string digits = 1 [(pattern).regex = "\\d{4}\\s?\\d{4}\\s?\\d{4}\\s?\\d{4}"]; +}