package com.tngtech.archunit;
import java.lang.annotation.Annotation;
import com.google.common.base.Joiner;
import com.google.common.collect.ImmutableSet;
import com.tngtech.archunit.base.DescribedIterable;
import com.tngtech.archunit.base.DescribedPredicate;
import com.tngtech.archunit.base.Guava;
import com.tngtech.archunit.core.domain.Formatters;
import com.tngtech.archunit.core.domain.JavaAnnotation;
import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.domain.JavaConstructor;
import com.tngtech.archunit.core.domain.JavaMember;
import com.tngtech.archunit.core.domain.JavaMethod;
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.junit.ArchUnitRunner;
import com.tngtech.archunit.lang.ArchCondition;
import com.tngtech.archunit.lang.ArchRule;
import com.tngtech.archunit.lang.ClassesTransformer;
import com.tngtech.archunit.lang.ConditionEvents;
import com.tngtech.archunit.lang.SimpleConditionEvent;
import static com.tngtech.archunit.ArchUnitArchitectureTest.THIRDPARTY_PACKAGE_IDENTIFIER;
import static com.tngtech.archunit.PublicAPI.Usage.INHERITANCE;
import static com.tngtech.archunit.base.DescribedPredicate.dont;
import static com.tngtech.archunit.base.DescribedPredicate.not;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.assignableTo;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.equivalentTo;
import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAPackage;
import static com.tngtech.archunit.core.domain.JavaMember.Predicates.declaredIn;
import static com.tngtech.archunit.core.domain.JavaModifier.FINAL;
import static com.tngtech.archunit.core.domain.JavaModifier.PUBLIC;
import static com.tngtech.archunit.core.domain.properties.CanBeAnnotated.Predicates.annotatedWith;
import static com.tngtech.archunit.core.domain.properties.HasModifiers.Predicates.modifier;
import static com.tngtech.archunit.lang.conditions.ArchPredicates.are;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.all;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes;
public class PublicAPIRules {
@ArchTest
public static final ArchRule only_public_API_classes_or_classes_explicitly_marked_as_internal_are_accessible =
classes()
.that(are(not(publicAPI())))
.and(are(not(internal())))
.and().areNotAssignableTo(Annotation.class)
.and(are(not(enclosedInANonPublicClass())))
.and().resideOutsideOfPackage(THIRDPARTY_PACKAGE_IDENTIFIER)
.should().notBePublic()
.as("classes that are not explicitly designed as API should not be public")
.because("we risk extensibility and maintainability of ArchUnit, if internal classes leak to users");
@ArchTest
public static final ArchRule only_members_that_are_public_API_or_explicitly_marked_as_internal_are_accessible =
// TODO: all(members()) should instead be members() and part of the fluent API
all(members())
.that(are(withoutAPIMarking()))
.and(dont(inheritPublicAPI()))
.and(are(relevantArchUnitMembers()))
.should(notBePublic())
.because("users of ArchUnit should only access intended members, to preserve maintainability");
@ArchTest
public static final ArchRule all_public_classes_that_are_not_meant_for_inheritance_or_internal_are_final =
classes()
.that().arePublic()
.and(haveAPublicConstructor())
.and(are(not(internal())))
.and(are(not(enclosedInANonPublicClass())))
.and().resideOutsideOfPackage(THIRDPARTY_PACKAGE_IDENTIFIER)
.and(are(not(equivalentTo(ArchUnitRunner.class))))
.should(bePublicAPIForInheritance())
.orShould(beInterfaces())
.orShould().haveModifier(FINAL)
.as("all public classes not meant for inheritance should be final")
.because("users of ArchUnit should only inherit from intended classes, to preserve maintainability");
private static DescribedPredicate<JavaClass> publicAPI() {
return annotatedWith(PublicAPI.class).<JavaClass>forSubType()
.or(haveMemberThatBelongsToPublicApi())
.or(markedAsPublicAPIForInheritance());
}
private static DescribedPredicate<JavaClass> internal() {
return annotatedWith(Internal.class).<JavaClass>forSubType()
.or(equivalentTo(Internal.class));
}
private static DescribedPredicate<JavaClass> enclosedInANonPublicClass() {
return new DescribedPredicate<JavaClass>("enclosed in a non-public class") {
@Override
public boolean apply(JavaClass input) {
return input.getEnclosingClass().isPresent() &&
!input.getEnclosingClass().get().getModifiers().contains(PUBLIC);
}
};
}
private static DescribedPredicate<JavaMember> inheritedFromObjectOrEnum() {
return new DescribedPredicate<JavaMember>("inherited from %s or %s",
Object.class.getName(), Enum.class.getName()) {
@Override
public boolean apply(JavaMember input) {
if (!(input instanceof JavaMethod)) {
return false;
}
JavaMethod methodToCheck = (JavaMethod) input;
return equivalentMethod(methodToCheck, "toString") ||
equivalentMethod(methodToCheck, "hashCode") ||
equivalentMethod(methodToCheck, "equals", Object.class) ||
enumMethod(methodToCheck, "values") ||
enumMethod(methodToCheck, "valueOf", String.class);
}
private boolean equivalentMethod(JavaMethod method, String name, Class<?>... paramTypes) {
return method.getName().equals(name) &&
method.getParameters().getNames().equals(JavaClass.namesOf(paramTypes));
}
private boolean enumMethod(JavaMethod methodToCheck, String name, Class<?>... paramTypes) {
return methodToCheck.getOwner().isAssignableTo(Enum.class)
&& equivalentMethod(methodToCheck, name, paramTypes);
}
};
}
private static DescribedPredicate<JavaClass> anonymousClass() {
return new DescribedPredicate<JavaClass>("anonymous class") {
@Override
public boolean apply(JavaClass input) {
return input.isAnonymous();
}
};
}
private static DescribedPredicate<JavaMember> declaredInClassIn(String packageIdentifier) {
return declaredIn(resideInAPackage(packageIdentifier).as("class in '%s'", packageIdentifier));
}
// TODO: Would be a nice feature, to record the line numbers of members as well
private static ArchCondition<JavaMember> notBePublic() {
return new ArchCondition<JavaMember>("not be public") {
@Override
public void check(JavaMember item, ConditionEvents events) {
boolean satisfied = !item.getModifiers().contains(PUBLIC);
events.add(new SimpleConditionEvent<>(item, satisfied,
String.format("member %s.%s is %spublic in %s",
item.getOwner().getName(),
item.getName(),
satisfied ? "not " : "",
Formatters.formatLocation(item.getOwner(), 0))));
}
};
}
private static DescribedPredicate<JavaClass> haveAPublicConstructor() {
return new DescribedPredicate<JavaClass>("have a public constructor") {
@Override
public boolean apply(JavaClass input) {
for (JavaConstructor constructor : input.getConstructors()) {
if (constructor.getModifiers().contains(PUBLIC)) {
return true;
}
}
return input.getConstructors().isEmpty() &&
input.getSuperClass().isPresent() &&
haveAPublicConstructor().apply(input.getSuperClass().get());
}
};
}
private static DescribedPredicate<JavaClass> haveMemberThatBelongsToPublicApi() {
return new DescribedPredicate<JavaClass>("have member that belongs to public API") {
@Override
public boolean apply(JavaClass input) {
for (JavaMember member : input.getAllMembers()) {
if (member.isAnnotatedWith(PublicAPI.class)) {
return true;
}
}
return false;
}
};
}
private static DescribedPredicate<JavaMember> withoutAPIMarking() {
return not(annotatedWith(PublicAPI.class)).<JavaMember>forSubType()
.and(not(annotatedWith(Internal.class)).<JavaMember>forSubType())
.and(declaredIn(modifier(PUBLIC)))
.as("without API marking");
}
private static DescribedPredicate<JavaMember> inheritPublicAPI() {
return new DescribedPredicate<JavaMember>("inherit public API") {
@Override
public boolean apply(JavaMember input) {
return declaredIn(markedAsPublicAPIForInheritance()).apply(input) ||
inheritsFromSuperMethod(input);
}
private boolean inheritsFromSuperMethod(JavaMember input) {
if (!(input instanceof JavaMethod)) {
return false;
}
JavaMethod methodToCheck = (JavaMethod) input;
for (JavaMethod candidate : input.getOwner().getAllMethods()) {
if (isPublicAPISuperMethod(candidate, methodToCheck)) {
return true;
}
}
return false;
}
private boolean isPublicAPISuperMethod(JavaMethod candidate, JavaMethod methodToCheck) {
return candidate.getName().equals(methodToCheck.getName()) &&
candidate.getParameters().equals(methodToCheck.getParameters()) &&
candidate.isAnnotatedWith(PublicAPI.class);
}
};
}
private static DescribedPredicate<JavaMember> relevantArchUnitMembers() {
return not(inheritedFromObjectOrEnum())
.and(not(declaredIn(assignableTo(Annotation.class))))
.and(not(declaredIn(anonymousClass())))
.and(not(declaredIn(internal())))
.and(not(declaredInClassIn(THIRDPARTY_PACKAGE_IDENTIFIER)))
.as("relevant members");
}
private static DescribedPredicate<JavaClass> markedAsPublicAPIForInheritance() {
return new DescribedPredicate<JavaClass>("inherit public API") {
@Override
public boolean apply(JavaClass input) {
for (JavaClass clazz : input.getAllClassesSelfIsAssignableTo()) {
if (clazz.isAnnotatedWith(publicApiForInheritance())) {
return true;
}
}
return false;
}
};
}
private static DescribedPredicate<JavaAnnotation> publicApiForInheritance() {
return new DescribedPredicate<JavaAnnotation>("@%s(usage = %s)", PublicAPI.class.getSimpleName(), INHERITANCE) {
@Override
public boolean apply(JavaAnnotation input) {
return input.getType().isEquivalentTo(PublicAPI.class) &&
input.as(PublicAPI.class).usage() == INHERITANCE;
}
};
}
private static ArchCondition<? super JavaClass> beInterfaces() {
return new ArchCondition<JavaClass>("be interfaces") {
@Override
public void check(JavaClass item, ConditionEvents events) {
boolean satisfied = item.isInterface();
events.add(new SimpleConditionEvent<>(item, satisfied,
String.format("class %s is %sinterface", item.getName(), satisfied ? "" : "no ")));
}
};
}
private static ArchCondition<JavaClass> bePublicAPIForInheritance() {
return new ArchCondition<JavaClass>("be public API for inheritance") {
@Override
public void check(JavaClass item, ConditionEvents events) {
boolean satisfied = item.isAnnotatedWith(publicApiForInheritance()) ||
markedAsPublicAPIForInheritance().apply(item);
events.add(new SimpleConditionEvent<>(item, satisfied,
String.format("class %s is %smeant for inheritance", item.getName(), satisfied ? "" : "not ")));
}
};
}
private static ClassesTransformer<JavaMember> members() {
return new ToMembersTransformer();
}
private static class ToMembersTransformer implements ClassesTransformer<JavaMember> {
private String description;
private DescribedPredicate<JavaMember> selected;
private ToMembersTransformer() {
this("members", DescribedPredicate.<JavaMember>alwaysTrue());
}
private ToMembersTransformer(String description, DescribedPredicate<JavaMember> selected) {
this.description = description;
this.selected = selected;
}
@Override
public DescribedIterable<JavaMember> transform(JavaClasses collection) {
ImmutableSet.Builder<JavaMember> result = ImmutableSet.builder();
for (JavaClass javaClass : collection) {
result.addAll(Guava.Iterables.filter(javaClass.getMembers(), selected));
}
return DescribedIterable.From.iterable(result.build(), description);
}
@Override
public ClassesTransformer<JavaMember> that(DescribedPredicate<? super JavaMember> predicate) {
String newDescription = Joiner.on(" that ").join(description, predicate.getDescription());
return new ToMembersTransformer(newDescription, predicate.<JavaMember>forSubType());
}
@Override
public ClassesTransformer<JavaMember> as(String description) {
return new ToMembersTransformer(description, selected);
}
@Override
public String getDescription() {
return description;
}
}
}