GH-594 - Deduplicate violations with the same message.
Moving away from exceptions as carriers for violations.
This commit is contained in:
@@ -37,6 +37,7 @@ import java.util.stream.Stream;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.modulith.core.Types.JMoleculesTypes;
|
||||
import org.springframework.modulith.core.Types.SpringTypes;
|
||||
import org.springframework.modulith.core.Violations.Violation;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.util.function.SingletonSupplier;
|
||||
@@ -994,7 +995,7 @@ public class ApplicationModule {
|
||||
.formatted(originModule.getName(), targetModule.getName(), source.getName(), target.getName(),
|
||||
declaredDependencies.toString());
|
||||
|
||||
return violations.and(new IllegalStateException(message));
|
||||
return violations.and(new Violation(message));
|
||||
}
|
||||
|
||||
// No explicitly allowed dependencies - check for general access
|
||||
@@ -1008,7 +1009,7 @@ public class ApplicationModule {
|
||||
var violationText = "Module '%s' depends on non-exposed type %s within module '%s'!"
|
||||
.formatted(originModule.getName(), target.getName(), targetModule.getName());
|
||||
|
||||
return violations.and(new IllegalStateException(violationText + lineSeparator() + description));
|
||||
return violations.and(new Violation(violationText + lineSeparator() + description));
|
||||
}
|
||||
|
||||
return violations;
|
||||
@@ -1184,7 +1185,7 @@ public class ApplicationModule {
|
||||
|
||||
ApplicationModule module = getExistingModuleOf(source.getOwner(), modules);
|
||||
|
||||
violations = violations.and(new IllegalStateException(
|
||||
violations = violations.and(new Violation(
|
||||
String.format("Module %s uses field injection in %s. Prefer constructor injection instead!",
|
||||
module.getDisplayName(), source.getFullName())));
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
import org.springframework.core.io.support.SpringFactoriesLoader;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.modulith.core.Types.JMoleculesTypes;
|
||||
import org.springframework.modulith.core.Violations.Violation;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.function.SingletonSupplier;
|
||||
@@ -401,7 +402,7 @@ public class ApplicationModules implements Iterable<ApplicationModule> {
|
||||
Violations violations = rootPackages.stream() //
|
||||
.map(this::assertNoCyclesFor) //
|
||||
.flatMap(it -> it.getDetails().stream()) //
|
||||
.map(IllegalStateException::new) //
|
||||
.map(Violation::new) //
|
||||
.collect(Violations.toViolations());
|
||||
|
||||
if (JMoleculesTypes.areRulesPresent()) {
|
||||
|
||||
@@ -34,18 +34,18 @@ public class Violations extends RuntimeException {
|
||||
|
||||
public static Violations NONE = new Violations(Collections.emptyList());
|
||||
|
||||
private final List<RuntimeException> exceptions;
|
||||
private final List<Violation> violations;
|
||||
|
||||
/**
|
||||
* Creates a new {@link Violations} from the given {@link RuntimeException}s.
|
||||
* Creates a new {@link Violations} from the given {@link ArchitecturalViolation}s.
|
||||
*
|
||||
* @param exceptions must not be {@literal null}.
|
||||
* @param violations must not be {@literal null}.
|
||||
*/
|
||||
private Violations(List<RuntimeException> exceptions) {
|
||||
private Violations(List<Violation> violations) {
|
||||
|
||||
Assert.notNull(exceptions, "Exceptions must not be null!");
|
||||
Assert.notNull(violations, "Exceptions must not be null!");
|
||||
|
||||
this.exceptions = exceptions;
|
||||
this.violations = violations;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -54,7 +54,7 @@ public class Violations extends RuntimeException {
|
||||
*
|
||||
* @return will never be {@literal null}.
|
||||
*/
|
||||
static Collector<RuntimeException, ?, Violations> toViolations() {
|
||||
static Collector<Violation, ?, Violations> toViolations() {
|
||||
return Collectors.collectingAndThen(Collectors.toList(), Violations::new);
|
||||
}
|
||||
|
||||
@@ -65,8 +65,8 @@ public class Violations extends RuntimeException {
|
||||
@Override
|
||||
public String getMessage() {
|
||||
|
||||
return exceptions.stream() //
|
||||
.map(RuntimeException::getMessage) //
|
||||
return violations.stream() //
|
||||
.map(Violation::message) //
|
||||
.collect(Collectors.joining("\n- ", "- ", ""));
|
||||
}
|
||||
|
||||
@@ -78,8 +78,8 @@ public class Violations extends RuntimeException {
|
||||
*/
|
||||
public List<String> getMessages() {
|
||||
|
||||
return exceptions.stream() //
|
||||
.map(RuntimeException::getMessage)
|
||||
return violations.stream() //
|
||||
.map(Violation::message)
|
||||
.toList();
|
||||
}
|
||||
|
||||
@@ -89,7 +89,7 @@ public class Violations extends RuntimeException {
|
||||
* @return
|
||||
*/
|
||||
public boolean hasViolations() {
|
||||
return !exceptions.isEmpty();
|
||||
return !violations.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -108,36 +108,45 @@ public class Violations extends RuntimeException {
|
||||
* @param exception must not be {@literal null}.
|
||||
* @return
|
||||
*/
|
||||
Violations and(RuntimeException exception) {
|
||||
Violations and(Violation violation) {
|
||||
|
||||
Assert.notNull(exception, "Exception must not be null!");
|
||||
Assert.notNull(violation, "Exception must not be null!");
|
||||
|
||||
List<RuntimeException> newExceptions = new ArrayList<>(exceptions.size() + 1);
|
||||
newExceptions.addAll(exceptions);
|
||||
newExceptions.add(exception);
|
||||
if (violations.isEmpty()) {
|
||||
return new Violations(List.of(violation));
|
||||
}
|
||||
|
||||
if (violations.contains(violation)) {
|
||||
return this;
|
||||
}
|
||||
|
||||
List<Violation> newExceptions = new ArrayList<>(violations.size() + 1);
|
||||
newExceptions.addAll(violations);
|
||||
newExceptions.add(violation);
|
||||
|
||||
return new Violations(newExceptions);
|
||||
}
|
||||
|
||||
Violations and(Violations other) {
|
||||
|
||||
List<RuntimeException> newExceptions = new ArrayList<>(exceptions.size() + other.exceptions.size());
|
||||
newExceptions.addAll(exceptions);
|
||||
newExceptions.addAll(other.exceptions);
|
||||
if (violations.isEmpty()) {
|
||||
return new Violations(other.violations);
|
||||
}
|
||||
|
||||
var newExceptions = new ArrayList<>(violations);
|
||||
|
||||
for (Violation candidate : other.violations) {
|
||||
if (!violations.contains(candidate)) {
|
||||
newExceptions.add(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
return new Violations(newExceptions);
|
||||
}
|
||||
|
||||
Violations and(String violation) {
|
||||
return and(new ArchitecturalViolation(violation));
|
||||
return and(new Violation(violation));
|
||||
}
|
||||
|
||||
private static class ArchitecturalViolation extends RuntimeException {
|
||||
|
||||
private static final long serialVersionUID = 3587887036508024142L;
|
||||
|
||||
public ArchitecturalViolation(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
static record Violation(String message) {}
|
||||
}
|
||||
|
||||
@@ -30,10 +30,25 @@ class ViolationsUnitTests {
|
||||
void combinesExceptionMessages() {
|
||||
|
||||
Violations violations = Violations.NONE //
|
||||
.and(new IllegalArgumentException("First")) //
|
||||
.and(new IllegalArgumentException("Second"));
|
||||
.and("First") //
|
||||
.and("Second");
|
||||
|
||||
assertThat(violations.getMessage()) //
|
||||
.isEqualTo("- First\n- Second");
|
||||
}
|
||||
|
||||
@Test
|
||||
void deduplicatesViolationsWithTheSameMessage() {
|
||||
|
||||
var violations = Violations.NONE
|
||||
.and("First")
|
||||
.and("First")
|
||||
.and("Second")
|
||||
.and(Violations.NONE.and("First")
|
||||
.and("Second")
|
||||
.and("Third"));
|
||||
|
||||
assertThat(violations.getMessages())
|
||||
.containsExactlyInAnyOrder("First", "Second", "Third");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user