Skip to content

Commit 8934497

Browse files
committed
GH-597 - Deduplicate violations with the same message.
Moving away from exceptions as carriers for violations.
1 parent bdd59fa commit 8934497

File tree

4 files changed

+64
-32
lines changed

4 files changed

+64
-32
lines changed

spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModule.java

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
import org.springframework.lang.Nullable;
3838
import org.springframework.modulith.core.Types.JMoleculesTypes;
3939
import org.springframework.modulith.core.Types.SpringTypes;
40+
import org.springframework.modulith.core.Violations.Violation;
4041
import org.springframework.util.Assert;
4142
import org.springframework.util.StringUtils;
4243
import org.springframework.util.function.SingletonSupplier;
@@ -952,7 +953,7 @@ Violations isValidDependencyWithin(ApplicationModules modules) {
952953
.formatted(originModule.getName(), targetModule.getName(), source.getName(), target.getName(),
953954
declaredDependencies.toString());
954955

955-
return violations.and(new IllegalStateException(message));
956+
return violations.and(new Violation(message));
956957
}
957958

958959
// No explicitly allowed dependencies - check for general access
@@ -962,7 +963,7 @@ Violations isValidDependencyWithin(ApplicationModules modules) {
962963
var violationText = "Module '%s' depends on non-exposed type %s within module '%s'!"
963964
.formatted(originModule.getName(), target.getName(), targetModule.getName());
964965

965-
return violations.and(new IllegalStateException(violationText + lineSeparator() + description));
966+
return violations.and(new Violation(violationText + lineSeparator() + description));
966967
}
967968

968969
return violations;
@@ -1138,7 +1139,7 @@ Violations isValidDependencyWithin(ApplicationModules modules) {
11381139

11391140
ApplicationModule module = getExistingModuleOf(source.getOwner(), modules);
11401141

1141-
violations = violations.and(new IllegalStateException(
1142+
violations = violations.and(new Violation(
11421143
String.format("Module %s uses field injection in %s. Prefer constructor injection instead!",
11431144
module.getDisplayName(), source.getFullName())));
11441145
}

spring-modulith-core/src/main/java/org/springframework/modulith/core/ApplicationModules.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
import org.springframework.modulith.Modulith;
4141
import org.springframework.modulith.Modulithic;
4242
import org.springframework.modulith.core.Types.JMoleculesTypes;
43+
import org.springframework.modulith.core.Violations.Violation;
4344
import org.springframework.util.Assert;
4445
import org.springframework.util.ClassUtils;
4546

@@ -375,7 +376,7 @@ public Violations detectViolations() {
375376
Violations violations = rootPackages.stream() //
376377
.map(this::assertNoCyclesFor) //
377378
.flatMap(it -> it.getDetails().stream()) //
378-
.map(IllegalStateException::new) //
379+
.map(Violation::new) //
379380
.collect(Violations.toViolations());
380381

381382
if (JMoleculesTypes.areRulesPresent()) {

spring-modulith-core/src/main/java/org/springframework/modulith/core/Violations.java

Lines changed: 41 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import java.util.List;
2121
import java.util.stream.Collector;
2222
import java.util.stream.Collectors;
23-
import java.util.stream.Stream;
2423

2524
import org.springframework.util.Assert;
2625

@@ -35,26 +34,26 @@ public class Violations extends RuntimeException {
3534

3635
public static Violations NONE = new Violations(Collections.emptyList());
3736

38-
private final List<RuntimeException> exceptions;
37+
private final List<Violation> violations;
3938

4039
/**
41-
* Creates a new {@link Violations} from the given {@link RuntimeException}s.
40+
* Creates a new {@link Violations} from the given {@link ArchitecturalViolation}s.
4241
*
43-
* @param exceptions must not be {@literal null}.
42+
* @param violations must not be {@literal null}.
4443
*/
45-
private Violations(List<RuntimeException> exceptions) {
44+
private Violations(List<Violation> violations) {
4645

47-
Assert.notNull(exceptions, "Exceptions must not be null!");
46+
Assert.notNull(violations, "Exceptions must not be null!");
4847

49-
this.exceptions = exceptions;
48+
this.violations = violations;
5049
}
5150

5251
/**
5352
* A {@link Collector} to turn a {@link Stream} of {@link RuntimeException}s into a {@link Violations} instance.
5453
*
5554
* @return will never be {@literal null}.
5655
*/
57-
static Collector<RuntimeException, ?, Violations> toViolations() {
56+
static Collector<Violation, ?, Violations> toViolations() {
5857
return Collectors.collectingAndThen(Collectors.toList(), Violations::new);
5958
}
6059

@@ -65,8 +64,8 @@ private Violations(List<RuntimeException> exceptions) {
6564
@Override
6665
public String getMessage() {
6766

68-
return exceptions.stream() //
69-
.map(RuntimeException::getMessage) //
67+
return violations.stream() //
68+
.map(Violation::message) //
7069
.collect(Collectors.joining("\n- ", "- ", ""));
7170
}
7271

@@ -76,7 +75,7 @@ public String getMessage() {
7675
* @return
7776
*/
7877
public boolean hasViolations() {
79-
return !exceptions.isEmpty();
78+
return !violations.isEmpty();
8079
}
8180

8281
/**
@@ -95,36 +94,52 @@ public void throwIfPresent() {
9594
* @param exception must not be {@literal null}.
9695
* @return
9796
*/
98-
Violations and(RuntimeException exception) {
97+
Violations and(Violation violation) {
9998

100-
Assert.notNull(exception, "Exception must not be null!");
99+
Assert.notNull(violation, "Exception must not be null!");
101100

102-
List<RuntimeException> newExceptions = new ArrayList<>(exceptions.size() + 1);
103-
newExceptions.addAll(exceptions);
104-
newExceptions.add(exception);
101+
if (violations.isEmpty()) {
102+
return new Violations(List.of(violation));
103+
}
104+
105+
if (violations.contains(violation)) {
106+
return this;
107+
}
108+
109+
List<Violation> newExceptions = new ArrayList<>(violations.size() + 1);
110+
newExceptions.addAll(violations);
111+
newExceptions.add(violation);
105112

106113
return new Violations(newExceptions);
107114
}
108115

109116
Violations and(Violations other) {
110117

111-
List<RuntimeException> newExceptions = new ArrayList<>(exceptions.size() + other.exceptions.size());
112-
newExceptions.addAll(exceptions);
113-
newExceptions.addAll(other.exceptions);
118+
if (violations.isEmpty()) {
119+
return new Violations(other.violations);
120+
}
121+
122+
var newExceptions = new ArrayList<>(violations);
123+
124+
for (Violation candidate : other.violations) {
125+
if (!violations.contains(candidate)) {
126+
newExceptions.add(candidate);
127+
}
128+
}
114129

115130
return new Violations(newExceptions);
116131
}
117132

118133
Violations and(String violation) {
119-
return and(new ArchitecturalViolation(violation));
134+
return and(new Violation(violation));
120135
}
121136

122-
private static class ArchitecturalViolation extends RuntimeException {
123-
124-
private static final long serialVersionUID = 3587887036508024142L;
137+
List<String> getMessages() {
125138

126-
public ArchitecturalViolation(String message) {
127-
super(message);
128-
}
139+
return violations.stream()
140+
.map(Violation::message)
141+
.toList();
129142
}
143+
144+
static record Violation(String message) {}
130145
}

spring-modulith-core/src/test/java/org/springframework/modulith/core/ViolationsUnitTests.java

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,25 @@ class ViolationsUnitTests {
3030
void combinesExceptionMessages() {
3131

3232
Violations violations = Violations.NONE //
33-
.and(new IllegalArgumentException("First")) //
34-
.and(new IllegalArgumentException("Second"));
33+
.and("First") //
34+
.and("Second");
3535

3636
assertThat(violations.getMessage()) //
3737
.isEqualTo("- First\n- Second");
3838
}
39+
40+
@Test
41+
void deduplicatesViolationsWithTheSameMessage() {
42+
43+
var violations = Violations.NONE
44+
.and("First")
45+
.and("First")
46+
.and("Second")
47+
.and(Violations.NONE.and("First")
48+
.and("Second")
49+
.and("Third"));
50+
51+
assertThat(violations.getMessages())
52+
.containsExactlyInAnyOrder("First", "Second", "Third");
53+
}
3954
}

0 commit comments

Comments
 (0)