Skip to content

Commit eb680be

Browse files
committed
add feature config
1 parent 3e22e89 commit eb680be

2 files changed

Lines changed: 195 additions & 124 deletions

File tree

src/main/java/it/aboutbits/archunit/toolbox/ArchitectureTestBase.java

Lines changed: 154 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
import com.tngtech.archunit.lang.ArchRule;
1010
import com.tngtech.archunit.lang.ConditionEvents;
1111
import com.tngtech.archunit.lang.SimpleConditionEvent;
12+
import lombok.Getter;
13+
import lombok.Setter;
14+
import lombok.experimental.Accessors;
1215
import lombok.extern.slf4j.Slf4j;
1316
import org.jspecify.annotations.NullMarked;
1417

@@ -24,6 +27,25 @@
2427
@Slf4j
2528
@NullMarked
2629
public abstract class ArchitectureTestBase {
30+
protected static final Features FEATURES = new Features();
31+
32+
@Setter
33+
@Getter
34+
@Accessors(fluent = true)
35+
protected static class Features {
36+
private Features.State blacklistMethods = Features.State.ENABLED;
37+
private Features.State blacklistClasses = Features.State.ENABLED;
38+
private Features.State blacklistAnnotations = Features.State.ENABLED;
39+
private Features.State enforceJspecify = Features.State.ENABLED;
40+
41+
protected Features() {
42+
}
43+
44+
public enum State {
45+
ENABLED, DISABLED
46+
}
47+
}
48+
2749
protected static final Set<String> BLACKLISTED_METHODS = new HashSet<>(
2850
Set.of(
2951
// We should use `assertThatExceptionOfType(...).isThrownBy(...)` instead of `assertThatThrownBy(...)`
@@ -194,152 +216,160 @@ void nested_test_classes_have_matching_production_method_name(JavaClasses classe
194216

195217
@SuppressWarnings("unused")
196218
@ArchTest
197-
static final ArchRule no_blacklisted_methods_are_used = classes()
198-
.should(new ArchCondition<>("not use blacklisted methods or statically import them") {
199-
@Override
200-
public void check(JavaClass javaClass, ConditionEvents events) {
201-
// Check all method calls from this class
202-
for (var method : javaClass.getMethods()) {
203-
for (var methodCall : method.getMethodCallsFromSelf()) {
204-
var fullMethodName = "%s.%s".formatted(
205-
methodCall.getTargetOwner().getFullName(),
206-
methodCall.getTarget().getName()
207-
);
208-
209-
if (BLACKLISTED_METHODS.contains(fullMethodName)) {
210-
var message = String.format(
211-
"Method %s calls blacklisted method %s (%s.java:%d)",
212-
method.getFullName(),
213-
fullMethodName,
214-
javaClass.getSimpleName(),
215-
methodCall.getSourceCodeLocation().getLineNumber()
216-
);
217-
events.add(SimpleConditionEvent.violated(method, message));
219+
static final ArchRule no_blacklisted_methods_are_used = FEATURES.blacklistMethods() == Features.State.DISABLED
220+
? new InnertRule()
221+
: classes()
222+
.should(new ArchCondition<>("not use blacklisted methods or statically import them") {
223+
@Override
224+
public void check(JavaClass javaClass, ConditionEvents events) {
225+
// Check all method calls from this class
226+
for (var method : javaClass.getMethods()) {
227+
for (var methodCall : method.getMethodCallsFromSelf()) {
228+
var fullMethodName = "%s.%s".formatted(
229+
methodCall.getTargetOwner().getFullName(),
230+
methodCall.getTarget().getName()
231+
);
232+
233+
if (BLACKLISTED_METHODS.contains(fullMethodName)) {
234+
var message = String.format(
235+
"Method %s calls blacklisted method %s (%s.java:%d)",
236+
method.getFullName(),
237+
fullMethodName,
238+
javaClass.getSimpleName(),
239+
methodCall.getSourceCodeLocation().getLineNumber()
240+
);
241+
events.add(SimpleConditionEvent.violated(method, message));
242+
}
243+
}
218244
}
219-
}
220-
}
221245

222-
// Check static initializers for method calls
223-
javaClass.getStaticInitializer().ifPresent(staticInitializer -> {
224-
for (var methodCall : staticInitializer.getMethodCallsFromSelf()) {
225-
var fullMethodName = "%s.%s".formatted(
226-
methodCall.getTargetOwner().getFullName(),
227-
methodCall.getTarget().getName()
228-
);
229-
230-
if (BLACKLISTED_METHODS.contains(fullMethodName)) {
231-
var message = String.format(
232-
"Static initializer in %s calls blacklisted method %s (%s.java:%d)",
233-
javaClass.getFullName(),
234-
fullMethodName,
235-
javaClass.getSimpleName(),
236-
methodCall.getSourceCodeLocation().getLineNumber()
237-
);
238-
events.add(SimpleConditionEvent.violated(staticInitializer, message));
239-
}
246+
// Check static initializers for method calls
247+
javaClass.getStaticInitializer().ifPresent(staticInitializer -> {
248+
for (var methodCall : staticInitializer.getMethodCallsFromSelf()) {
249+
var fullMethodName = "%s.%s".formatted(
250+
methodCall.getTargetOwner().getFullName(),
251+
methodCall.getTarget().getName()
252+
);
253+
254+
if (BLACKLISTED_METHODS.contains(fullMethodName)) {
255+
var message = String.format(
256+
"Static initializer in %s calls blacklisted method %s (%s.java:%d)",
257+
javaClass.getFullName(),
258+
fullMethodName,
259+
javaClass.getSimpleName(),
260+
methodCall.getSourceCodeLocation().getLineNumber()
261+
);
262+
events.add(SimpleConditionEvent.violated(staticInitializer, message));
263+
}
264+
}
265+
});
240266
}
241267
});
242-
}
243-
});
244268

245269
@SuppressWarnings("unused")
246270
@ArchTest
247-
static final ArchRule no_blacklisted_annotations_are_used = classes()
248-
.should(new ArchCondition<>(
249-
"not use blacklisted annotations on classes, methods, method parameters, or fields"
250-
) {
251-
@Override
252-
public void check(JavaClass javaClass, ConditionEvents events) {
253-
// Check annotations on the class itself
254-
for (var annotation : javaClass.getAnnotations()) {
255-
if (BLACKLISTED_ANNOTATIONS.contains(annotation.getRawType().getFullName())) {
256-
var message = String.format(
257-
"Class %s is annotated with blacklisted annotation @%s (%s.java:%d)",
258-
javaClass.getFullName(),
259-
annotation.getRawType().getFullName(),
260-
javaClass.getSimpleName(),
261-
javaClass.getSourceCodeLocation().getLineNumber()
262-
);
263-
events.add(SimpleConditionEvent.violated(javaClass, message));
264-
}
265-
}
266-
267-
// Check annotations on methods and their parameters
268-
for (var method : javaClass.getMethods()) {
269-
// Check method annotations
270-
for (var annotation : method.getAnnotations()) {
271-
if (BLACKLISTED_ANNOTATIONS.contains(annotation.getRawType().getFullName())) {
272-
var message = String.format(
273-
"Method %s is annotated with blacklisted annotation @%s (%s.java:%d)",
274-
method.getFullName(),
275-
annotation.getRawType().getFullName(),
276-
javaClass.getSimpleName(),
277-
method.getSourceCodeLocation().getLineNumber()
278-
);
279-
events.add(SimpleConditionEvent.violated(method, message));
280-
}
281-
}
282-
// Check method parameter annotations
283-
for (var parameter : method.getParameters()) {
284-
for (var annotation : parameter.getAnnotations()) {
271+
static final ArchRule no_blacklisted_annotations_are_used = FEATURES.blacklistAnnotations() == Features.State.DISABLED
272+
? new InnertRule()
273+
: classes()
274+
.should(new ArchCondition<>(
275+
"not use blacklisted annotations on classes, methods, method parameters, or fields"
276+
) {
277+
@Override
278+
public void check(JavaClass javaClass, ConditionEvents events) {
279+
// Check annotations on the class itself
280+
for (var annotation : javaClass.getAnnotations()) {
285281
if (BLACKLISTED_ANNOTATIONS.contains(annotation.getRawType().getFullName())) {
286282
var message = String.format(
287-
"Parameter %s of method %s is annotated with blacklisted annotation @%s (%s.java:%d)",
288-
parameter.getIndex(),
289-
method.getFullName(),
283+
"Class %s is annotated with blacklisted annotation @%s (%s.java:%d)",
284+
javaClass.getFullName(),
290285
annotation.getRawType().getFullName(),
291286
javaClass.getSimpleName(),
292-
method.getSourceCodeLocation().getLineNumber()
293-
); // Parameter doesn't have its own SLOC, use method's
294-
events.add(SimpleConditionEvent.violated(parameter, message));
287+
javaClass.getSourceCodeLocation().getLineNumber()
288+
);
289+
events.add(SimpleConditionEvent.violated(javaClass, message));
295290
}
296291
}
297-
}
298-
}
299292

300-
// Check annotations on fields (ArchUnit includes record components as fields)
301-
for (var field : javaClass.getFields()) {
302-
for (var annotation : field.getAnnotations()) {
303-
if (BLACKLISTED_ANNOTATIONS.contains(annotation.getRawType().getFullName())) {
304-
var message = String.format(
305-
"Field %s in class %s is annotated with blacklisted annotation @%s (%s.java:%d)",
306-
field.getName(),
307-
javaClass.getFullName(),
308-
annotation.getRawType().getFullName(),
309-
javaClass.getSimpleName(),
310-
field.getSourceCodeLocation().getLineNumber()
311-
);
312-
events.add(SimpleConditionEvent.violated(field, message));
293+
// Check annotations on methods and their parameters
294+
for (var method : javaClass.getMethods()) {
295+
// Check method annotations
296+
for (var annotation : method.getAnnotations()) {
297+
if (BLACKLISTED_ANNOTATIONS.contains(annotation.getRawType().getFullName())) {
298+
var message = String.format(
299+
"Method %s is annotated with blacklisted annotation @%s (%s.java:%d)",
300+
method.getFullName(),
301+
annotation.getRawType().getFullName(),
302+
javaClass.getSimpleName(),
303+
method.getSourceCodeLocation().getLineNumber()
304+
);
305+
events.add(SimpleConditionEvent.violated(method, message));
306+
}
307+
}
308+
// Check method parameter annotations
309+
for (var parameter : method.getParameters()) {
310+
for (var annotation : parameter.getAnnotations()) {
311+
if (BLACKLISTED_ANNOTATIONS.contains(annotation.getRawType().getFullName())) {
312+
var message = String.format(
313+
"Parameter %s of method %s is annotated with blacklisted annotation @%s (%s.java:%d)",
314+
parameter.getIndex(),
315+
method.getFullName(),
316+
annotation.getRawType().getFullName(),
317+
javaClass.getSimpleName(),
318+
method.getSourceCodeLocation().getLineNumber()
319+
); // Parameter doesn't have its own SLOC, use method's
320+
events.add(SimpleConditionEvent.violated(parameter, message));
321+
}
322+
}
323+
}
324+
}
325+
326+
// Check annotations on fields (ArchUnit includes record components as fields)
327+
for (var field : javaClass.getFields()) {
328+
for (var annotation : field.getAnnotations()) {
329+
if (BLACKLISTED_ANNOTATIONS.contains(annotation.getRawType().getFullName())) {
330+
var message = String.format(
331+
"Field %s in class %s is annotated with blacklisted annotation @%s (%s.java:%d)",
332+
field.getName(),
333+
javaClass.getFullName(),
334+
annotation.getRawType().getFullName(),
335+
javaClass.getSimpleName(),
336+
field.getSourceCodeLocation().getLineNumber()
337+
);
338+
events.add(SimpleConditionEvent.violated(field, message));
339+
}
340+
}
313341
}
314342
}
315-
}
316-
}
317-
});
343+
});
318344

319345
@SuppressWarnings("unused")
320346
@ArchTest
321-
static final ArchRule no_blacklisted_classes_are_used = noClasses()
322-
.should()
323-
.dependOnClassesThat(
324-
new DescribedPredicate<>("not use blacklisted classes") {
325-
@Override
326-
public boolean test(JavaClass javaClass) {
327-
return BLACKLISTED_CLASSES.contains(javaClass.getFullName());
328-
}
329-
}
330-
);
347+
static final ArchRule no_blacklisted_classes_are_used = FEATURES.blacklistClasses() == Features.State.DISABLED
348+
? new InnertRule()
349+
: noClasses()
350+
.should()
351+
.dependOnClassesThat(
352+
new DescribedPredicate<>("not use blacklisted classes") {
353+
@Override
354+
public boolean test(JavaClass javaClass) {
355+
return BLACKLISTED_CLASSES.contains(javaClass.getFullName());
356+
}
357+
}
358+
);
331359

332360
@SuppressWarnings("unused")
333361
@ArchTest
334-
static final ArchRule top_level_classes_must_be_annotated_with_jspecify = classes()
335-
.that()
336-
.areTopLevelClasses()
337-
.and()
338-
.areNotAnnotations()
339-
.should()
340-
.beAnnotatedWith(org.jspecify.annotations.NullMarked.class)
341-
.orShould()
342-
.beAnnotatedWith(org.jspecify.annotations.NullUnmarked.class);
362+
static final ArchRule top_level_classes_must_be_annotated_with_jspecify = FEATURES.enforceJspecify() == Features.State.DISABLED
363+
? new InnertRule()
364+
: classes()
365+
.that()
366+
.areTopLevelClasses()
367+
.and()
368+
.areNotAnnotations()
369+
.should()
370+
.beAnnotatedWith(org.jspecify.annotations.NullMarked.class)
371+
.orShould()
372+
.beAnnotatedWith(org.jspecify.annotations.NullUnmarked.class);
343373

344374
/* ****************************************************************** */
345375

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package it.aboutbits.archunit.toolbox;
2+
3+
import com.tngtech.archunit.core.domain.JavaClasses;
4+
import com.tngtech.archunit.lang.ArchRule;
5+
import com.tngtech.archunit.lang.EvaluationResult;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.jspecify.annotations.NullUnmarked;
8+
9+
@Slf4j
10+
@NullUnmarked
11+
final class InnertRule implements ArchRule {
12+
@Override
13+
public void check(JavaClasses javaClasses) {
14+
log.info("Rule disabled by config.");
15+
}
16+
17+
@Override
18+
public ArchRule because(String s) {
19+
return null;
20+
}
21+
22+
@Override
23+
public ArchRule allowEmptyShould(boolean b) {
24+
return null;
25+
}
26+
27+
@Override
28+
public ArchRule as(String s) {
29+
return null;
30+
}
31+
32+
@Override
33+
public EvaluationResult evaluate(JavaClasses javaClasses) {
34+
return null;
35+
}
36+
37+
@Override
38+
public String getDescription() {
39+
return "";
40+
}
41+
}

0 commit comments

Comments
 (0)