1717import static com .tngtech .archunit .core .domain .JavaClass .Predicates .resideInAPackage ;
1818import static com .tngtech .archunit .core .domain .JavaClass .Predicates .resideInAnyPackage ;
1919import static com .tngtech .archunit .core .domain .JavaClass .Predicates .simpleName ;
20+ import static com .tngtech .archunit .core .domain .JavaClass .Predicates .type ;
2021import static com .tngtech .archunit .core .domain .JavaModifier .PUBLIC ;
22+ import static com .tngtech .archunit .core .domain .properties .CanBeAnnotated .Predicates .annotatedWith ;
2123import static com .tngtech .archunit .core .domain .properties .HasModifiers .Predicates .modifier ;
2224import static com .tngtech .archunit .core .domain .properties .HasName .Predicates .name ;
2325import static com .tngtech .archunit .core .domain .properties .HasName .Predicates .nameContaining ;
26+ import static com .tngtech .archunit .core .domain .properties .HasName .Predicates .nameStartingWith ;
2427import static com .tngtech .archunit .lang .conditions .ArchPredicates .are ;
2528import static com .tngtech .archunit .lang .conditions .ArchPredicates .have ;
2629import static com .tngtech .archunit .lang .syntax .ArchRuleDefinition .classes ;
2730import static com .tngtech .archunit .library .dependencies .SlicesRuleDefinition .slices ;
2831import static org .junit .jupiter .api .Assertions .assertTrue ;
2932import static platform .tooling .support .Helper .loadJarFiles ;
3033
34+ import java .lang .annotation .Annotation ;
35+ import java .lang .annotation .Repeatable ;
36+ import java .lang .annotation .Retention ;
37+ import java .lang .annotation .Target ;
38+ import java .util .Arrays ;
3139import java .util .Set ;
40+ import java .util .function .BiPredicate ;
3241import java .util .stream .Collectors ;
3342
43+ import com .tngtech .archunit .base .DescribedPredicate ;
44+ import com .tngtech .archunit .core .domain .JavaClass ;
3445import com .tngtech .archunit .core .domain .JavaClasses ;
3546import com .tngtech .archunit .core .importer .Location ;
3647import com .tngtech .archunit .junit .AnalyzeClasses ;
3748import com .tngtech .archunit .junit .ArchTest ;
3849import com .tngtech .archunit .junit .LocationProvider ;
50+ import com .tngtech .archunit .lang .ArchCondition ;
3951import com .tngtech .archunit .lang .ArchRule ;
4052import com .tngtech .archunit .library .GeneralCodingRules ;
4153
4254import org .apiguardian .api .API ;
4355import org .junit .jupiter .api .Order ;
56+ import org .junit .jupiter .api .extension .ExtendWith ;
57+ import org .junit .jupiter .params .provider .ArgumentsSource ;
4458
4559@ Order (Integer .MAX_VALUE )
4660@ AnalyzeClasses (locations = ArchUnitTests .AllJars .class )
4761class ArchUnitTests {
4862
63+ @ SuppressWarnings ("unused" )
4964 @ ArchTest
5065 private final ArchRule allPublicTopLevelTypesHaveApiAnnotations = classes () //
5166 .that (have (modifier (PUBLIC ))) //
@@ -55,6 +70,17 @@ class ArchUnitTests {
5570 .and (not (describe ("are shadowed" , resideInAnyPackage ("..shadow.." )))) //
5671 .should ().beAnnotatedWith (API .class );
5772
73+ @ SuppressWarnings ("unused" )
74+ @ ArchTest // Consistency of @Documented and @Inherited is checked by the compiler but not for @Retention and @Target
75+ private final ArchRule repeatableAnnotationsShouldHaveMatchingContainerAnnotations = classes () //
76+ .that (nameStartingWith ("org.junit." )) //
77+ .and ().areAnnotations () //
78+ .and ().areAnnotatedWith (Repeatable .class ) //
79+ .and (are (not (type (ExtendWith .class )))) // to be resolved in https://github.com/junit-team/junit5/issues/4059
80+ .and (are (not (type (ArgumentsSource .class ).or (annotatedWith (ArgumentsSource .class ))))) // to be resolved in https://github.com/junit-team/junit5/issues/4063
81+ .should (haveContainerAnnotationWithSameRetentionPolicy ()) //
82+ .andShould (haveContainerAnnotationWithSameTargetTypes ());
83+
5884 @ ArchTest
5985 void allAreIn (JavaClasses classes ) {
6086 // about 928 classes found in all jars
@@ -94,6 +120,16 @@ void avoidAccessingStandardStreams(JavaClasses classes) {
94120 GeneralCodingRules .NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS .check (subset );
95121 }
96122
123+ private static ArchCondition <? super JavaClass > haveContainerAnnotationWithSameRetentionPolicy () {
124+ return ArchCondition .from (new RepeatableAnnotationPredicate <>(Retention .class ,
125+ (expectedTarget , actualTarget ) -> expectedTarget .value () == actualTarget .value ()));
126+ }
127+
128+ private static ArchCondition <? super JavaClass > haveContainerAnnotationWithSameTargetTypes () {
129+ return ArchCondition .from (new RepeatableAnnotationPredicate <>(Target .class ,
130+ (expectedTarget , actualTarget ) -> Arrays .equals (expectedTarget .value (), actualTarget .value ())));
131+ }
132+
97133 static class AllJars implements LocationProvider {
98134
99135 @ Override
@@ -103,4 +139,27 @@ public Set<Location> get(Class<?> testClass) {
103139
104140 }
105141
142+ private static class RepeatableAnnotationPredicate <T extends Annotation > extends DescribedPredicate <JavaClass > {
143+
144+ private final Class <T > annotationType ;
145+ private final BiPredicate <T , T > predicate ;
146+
147+ public RepeatableAnnotationPredicate (Class <T > annotationType , BiPredicate <T , T > predicate ) {
148+ super ("have identical @%s annotation as container annotation" , annotationType .getSimpleName ());
149+ this .annotationType = annotationType ;
150+ this .predicate = predicate ;
151+ }
152+
153+ @ Override
154+ public boolean test (JavaClass annotationClass ) {
155+ var containerAnnotationClass = (JavaClass ) annotationClass .getAnnotationOfType (
156+ Repeatable .class .getName ()).get ("value" ).orElseThrow ();
157+ var expectedAnnotation = annotationClass .tryGetAnnotationOfType (annotationType );
158+ var actualAnnotation = containerAnnotationClass .tryGetAnnotationOfType (annotationType );
159+ return expectedAnnotation .map (expectedTarget -> actualAnnotation //
160+ .map (actualTarget -> predicate .test (expectedTarget , actualTarget )) //
161+ .orElse (false )) //
162+ .orElse (actualAnnotation .isEmpty ());
163+ }
164+ }
106165}
0 commit comments