package org.kohsuke.github; import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.core.domain.*; import com.tngtech.archunit.core.domain.properties.HasName; import com.tngtech.archunit.core.domain.properties.HasOwner; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.core.importer.ImportOption; import com.tngtech.archunit.lang.ArchCondition; import com.tngtech.archunit.lang.ArchRule; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.ReflectionToStringBuilder; import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; import org.junit.BeforeClass; import org.junit.Test; import java.io.Closeable; import java.io.InputStream; import java.io.Reader; import java.lang.reflect.Field; import java.nio.charset.Charset; import static com.google.common.base.Preconditions.checkNotNull; import static com.tngtech.archunit.core.domain.JavaCall.Predicates.target; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideInAPackage; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.type; import static com.tngtech.archunit.core.domain.JavaClass.namesOf; import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.name; import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.nameContaining; import static com.tngtech.archunit.core.domain.properties.HasOwner.Predicates.With.owner; import static com.tngtech.archunit.core.domain.properties.HasParameterTypes.Predicates.rawParameterTypes; import static com.tngtech.archunit.lang.conditions.ArchConditions.*; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.fields; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.greaterThan; public class ArchTests { private static final JavaClasses classFiles = new ClassFileImporter() .withImportOption(new ImportOption.DoNotIncludeTests()) .withImportOption(new ImportOption.DoNotIncludeJars()) .importPackages("org.kohsuke.github"); private static final JavaClasses apacheCommons = new ClassFileImporter().importPackages("org.apache.commons.lang3"); private static final JavaClasses testClassFiles = new ClassFileImporter() .withImportOption(new ImportOption.OnlyIncludeTests()) .withImportOption(new ImportOption.DoNotIncludeJars()) .importPackages("org.kohsuke.github"); private static final DescribedPredicate> previewAnnotationWithNoMediaType = new DescribedPredicate>( "preview has no required media types defined") { @Override public boolean apply(JavaAnnotation javaAnnotation) { boolean isPreview = javaAnnotation.getRawType().isEquivalentTo(Preview.class); Object[] values = (Object[]) javaAnnotation.getProperties().get("value"); return isPreview && values != null && values.length < 1; } }; @BeforeClass public static void beforeClass() { assertThat(classFiles.size(), greaterThan(0)); } @Test public void testPreviewsAreFlaggedAsDeprecated() { String reason = "all preview APIs must be annotated as @Deprecated until they are promoted to stable"; ArchRule classRule = classes().that() .areAnnotatedWith(Preview.class) .should() .beAnnotatedWith(Deprecated.class) .andShould(not(beAnnotatedWith(previewAnnotationWithNoMediaType))) .because(reason); ArchRule methodRule = methods().that() .areAnnotatedWith(Preview.class) .should() .beAnnotatedWith(Deprecated.class) .andShould(not(beAnnotatedWith(previewAnnotationWithNoMediaType))) .because(reason); ArchRule enumFieldsRule = fields().that() .areDeclaredInClassesThat() .areEnums() .and() .areAnnotatedWith(Preview.class) .should() .beAnnotatedWith(Deprecated.class) .andShould(not(beAnnotatedWith(previewAnnotationWithNoMediaType))) .because(reason); classRule.check(classFiles); enumFieldsRule.check(classFiles); methodRule.check(classFiles); } @Test public void testBetaApisAreFlaggedAsDeprecated() { String reason = "all beta APIs must be annotated as @Deprecated until they are promoted to stable"; ArchRule classRule = classes().that() .areAnnotatedWith(BetaApi.class) .should() .beAnnotatedWith(Deprecated.class) .because(reason); ArchRule methodRule = methods().that() .areAnnotatedWith(BetaApi.class) .should() .beAnnotatedWith(Deprecated.class) .because(reason); ArchRule enumFieldsRule = fields().that() .areDeclaredInClassesThat() .areEnums() .and() .areAnnotatedWith(BetaApi.class) .should() .beAnnotatedWith(Deprecated.class) .because(reason); classRule.check(classFiles); enumFieldsRule.check(classFiles); methodRule.check(classFiles); } @Test public void testRequireUseOfAssertThat() { final String reason = "This project uses `assertThat(...)` instead of other `assert*()` methods."; final DescribedPredicate assertMethodOtherThanAssertThat = nameContaining("assert") .and(DescribedPredicate.not(name("assertThat"))); final ArchRule onlyAssertThatRule = classes() .should(not(callMethodWhere(target(assertMethodOtherThanAssertThat)))) .because(reason); onlyAssertThatRule.check(testClassFiles); } @Test public void testRequireUseOfOnlySpecificApacheCommons() { final ArchRule onlyApprovedApacheCommonsLang3Methods = classes() .should(notCallMethodsInPackageUnless("org.apache.commons..", // unless it is one of these methods targetMethodIs(StringUtils.class, "capitalize", String.class), targetMethodIs(StringUtils.class, "defaultString", String.class, String.class), targetMethodIs(StringUtils.class, "equals", CharSequence.class, CharSequence.class), targetMethodIs(StringUtils.class, "isBlank", CharSequence.class), targetMethodIs(StringUtils.class, "isEmpty", CharSequence.class), targetMethodIs(StringUtils.class, "join", Iterable.class, String.class), targetMethodIs(StringUtils.class, "prependIfMissing", String.class, CharSequence.class, CharSequence[].class), targetMethodIs(ToStringBuilder.class, "toString"), targetMethodIs(ToStringBuilder.class, "append", String.class, Object.class), targetMethodIs(ToStringBuilder.class, "append", String.class, long.class), targetMethodIs(ToStringBuilder.class, "append", String.class, int.class), targetMethodIs(ToStringBuilder.class, "isEmpty"), targetMethodIs(ToStringBuilder.class, "equals"), targetMethodIs(ToStringBuilder.class, "capitalize"), targetMethodIs(ToStringStyle.class, "append", StringBuffer.class, String.class, Object.class, Boolean.class), targetMethodIs(ReflectionToStringBuilder.class, "accept", Field.class), targetMethodIs(IOUtils.class, "closeQuietly", InputStream.class), targetMethodIs(IOUtils.class, "closeQuietly", Closeable.class), targetMethodIs(IOUtils.class, "toString", InputStream.class, Charset.class), targetMethodIs(IOUtils.class, "toString", Reader.class), targetMethodIs(IOUtils.class, "toByteArray", InputStream.class))) .because( "Commons methods must be manually verified to be compatible with commons-io:2.4 or earlier and commons-lang3:3.9 or earlier should be used."); onlyApprovedApacheCommonsLang3Methods.check(classFiles); } public static ArchCondition notCallMethodsInPackageUnless(final String packageIdentifier, final DescribedPredicate>... unlessPredicates) { DescribedPredicate> restrictedPackageCalls = target( HasOwner.Predicates.With.owner(resideInAPackage(packageIdentifier))); if (unlessPredicates.length > 0) { DescribedPredicate> allowed = unlessPredicates[0]; for (int x = 1; x < unlessPredicates.length; x++) { allowed = allowed.or(unlessPredicates[x]); } restrictedPackageCalls = unless(restrictedPackageCalls, allowed); } return not(callMethodWhere(restrictedPackageCalls)); } public static DescribedPredicate> targetMethodIs(Class owner, String methodName, Class... parameterTypes) { return JavaCall.Predicates.target(owner(type(owner))) .and(JavaCall.Predicates.target(name(methodName))) .and(JavaCall.Predicates.target(rawParameterTypes(parameterTypes))) .as("method is %s", Formatters.formatMethodSimple(owner.getSimpleName(), methodName, namesOf(parameterTypes))); } public static DescribedPredicate unless(DescribedPredicate first, DescribedPredicate second) { return new UnlessPredicate(first, second); } private static class UnlessPredicate extends DescribedPredicate { private final DescribedPredicate current; private final DescribedPredicate other; UnlessPredicate(DescribedPredicate current, DescribedPredicate other) { super(current.getDescription() + " unless " + other.getDescription()); this.current = checkNotNull(current); this.other = checkNotNull(other); } @Override public boolean apply(T input) { return current.apply(input) && !other.apply(input); } } }