Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add the possibility to allow empty should per rule #816

Merged
merged 10 commits into from
Feb 27, 2022
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ framework.
###### Gradle

```
testImplementation 'com.tngtech.archunit:archunit:0.22.0'
testImplementation 'com.tngtech.archunit:archunit:0.23.0'
```

###### Maven
Expand All @@ -26,7 +26,7 @@ testImplementation 'com.tngtech.archunit:archunit:0.22.0'
<dependency>
<groupId>com.tngtech.archunit</groupId>
<artifactId>archunit</artifactId>
<version>0.22.0</version>
<version>0.23.0</version>
<scope>test</scope>
</dependency>
```
Expand Down
3 changes: 2 additions & 1 deletion archunit/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@ dependencies {
runtimeOnly sourceSets.jdk9main.output
}

compileJdk9mainJava.with {
compileJdk9mainJava {
dependsOn(compileJava)
ext.minimumJavaVersion = JavaVersion.VERSION_1_9

destinationDir = compileJava.destinationDir
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ public abstract class JavaCodeUnit
extends JavaMember
implements HasParameterTypes, HasReturnType, HasTypeParameters<JavaCodeUnit>, HasThrowsClause<JavaCodeUnit> {

private final JavaType returnType;
private final ReturnType returnType;
private final Parameters parameters;
private final String fullName;
private final List<JavaTypeVariable<JavaCodeUnit>> typeParameters;
Expand All @@ -71,7 +71,7 @@ public abstract class JavaCodeUnit
JavaCodeUnit(JavaCodeUnitBuilder<?, ?> builder) {
super(builder);
typeParameters = builder.getTypeParameters(this);
returnType = builder.getReturnType(this);
returnType = new ReturnType(this, builder);
parameters = new Parameters(this, builder);
fullName = formatMethod(getOwner().getName(), getName(), namesOf(getRawParameterTypes()));
referencedClassObjects = ImmutableSet.copyOf(builder.getReferencedClassObjects(this));
Expand Down Expand Up @@ -157,13 +157,13 @@ public List<JavaClass> getExceptionTypes() {
@Override
@PublicAPI(usage = ACCESS)
public JavaType getReturnType() {
return returnType;
return returnType.get();
}

@Override
@PublicAPI(usage = ACCESS)
public JavaClass getRawReturnType() {
return returnType.toErasure();
return returnType.getRaw();
}

@PublicAPI(usage = ACCESS)
Expand Down Expand Up @@ -323,6 +323,24 @@ protected List<JavaParameter> delegate() {
}
}

private static class ReturnType {
private final JavaClass rawReturnType;
private final JavaType returnType;

ReturnType(JavaCodeUnit owner, JavaCodeUnitBuilder<?, ?> builder) {
rawReturnType = builder.getRawReturnType();
returnType = builder.getGenericReturnType(owner);
}

JavaClass getRaw() {
return rawReturnType;
}

JavaType get() {
return returnType;
}
}

public static final class Predicates {
private Predicates() {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ void resolve(ImportedClasses classes) {
}

private void logConfiguration() {
log.info("Automatically resolving transitive class dependencies with the following configuration:{}{}{}{}{}{}",
log.debug("Automatically resolving transitive class dependencies with the following configuration:{}{}{}{}{}{}",
formatConfigProperty(MAX_ITERATIONS_FOR_MEMBER_TYPES_PROPERTY_NAME, maxRunsForMemberTypes),
formatConfigProperty(MAX_ITERATIONS_FOR_ACCESSES_TO_TYPES_PROPERTY_NAME, maxRunsForAccessesToTypes),
formatConfigProperty(MAX_ITERATIONS_FOR_SUPERTYPES_PROPERTY_NAME, maxRunsForSupertypes),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,10 +305,14 @@ boolean hasNoParameters() {
return rawParameterTypes.isEmpty();
}

public JavaType getReturnType(JavaCodeUnit codeUnit) {
public JavaClass getRawReturnType() {
return get(rawReturnType.getFullyQualifiedClassName());
}

public JavaType getGenericReturnType(JavaCodeUnit codeUnit) {
return genericReturnType.isPresent()
? genericReturnType.get().finish(codeUnit, allTypeParametersInContextOf(codeUnit), importedClasses)
: get(rawReturnType.getFullyQualifiedClassName());
: getRawReturnType();
}

private Iterable<JavaTypeVariable<?>> allTypeParametersInContextOf(JavaCodeUnit codeUnit) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@
import com.tngtech.archunit.core.domain.JavaClassDescriptor;
import com.tngtech.archunit.core.domain.JavaCodeUnit;
import com.tngtech.archunit.core.domain.JavaFieldAccess.AccessType;
import com.tngtech.archunit.core.domain.properties.HasName;

import static com.google.common.base.Preconditions.checkNotNull;

Expand Down Expand Up @@ -139,8 +138,7 @@ public String toString() {

public boolean is(JavaCodeUnit method) {
return getName().equals(method.getName())
&& getRawParameterTypeNames().equals(HasName.Utils.namesOf(method.getRawParameterTypes()))
&& returnType.getFullyQualifiedClassName().equals(method.getRawReturnType().getName())
&& descriptor.equals(method.getDescriptor())
&& getDeclaringClassName().equals(method.getOwner().getName());
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
/*
* Copyright 2014-2022 TNG Technology Consulting GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.tngtech.archunit.lang;

import com.tngtech.archunit.ArchConfiguration;

enum AllowEmptyShould {
TRUE {
@Override
public boolean isAllowed() {
return true;
}
},
FALSE {
@Override
public boolean isAllowed() {
return false;
}
},
AS_CONFIGURED {
@Override
public boolean isAllowed() {
return ArchConfiguration.get().getPropertyOrDefault(FAIL_ON_EMPTY_SHOULD_PROPERTY_NAME, TRUE.toString())
.equalsIgnoreCase(FALSE.toString());
}
};

private static final String FAIL_ON_EMPTY_SHOULD_PROPERTY_NAME = "archRule.failOnEmptyShould";

abstract boolean isAllowed();

static AllowEmptyShould fromBoolean(boolean allow) {
return allow ? TRUE : FALSE;
}
}
47 changes: 31 additions & 16 deletions archunit/src/main/java/com/tngtech/archunit/lang/ArchRule.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@

import com.google.common.base.Predicate;
import com.google.common.collect.ImmutableSet;
import com.tngtech.archunit.ArchConfiguration;
import com.tngtech.archunit.Internal;
import com.tngtech.archunit.PublicAPI;
import com.tngtech.archunit.base.Optional;
Expand All @@ -41,7 +40,6 @@
import static com.google.common.io.Resources.readLines;
import static com.tngtech.archunit.PublicAPI.Usage.ACCESS;
import static com.tngtech.archunit.base.ClassLoaders.getCurrentClassLoader;
import static java.lang.Boolean.TRUE;
import static java.nio.charset.StandardCharsets.UTF_8;

/**
Expand All @@ -68,6 +66,18 @@ public interface ArchRule extends CanBeEvaluated, CanOverrideDescription<ArchRul
@PublicAPI(usage = ACCESS)
ArchRule because(String reason);

/**
* If set to {@code true} allows the should-clause of this rule to be checked against an empty set of elements.
* Otherwise, the rule will fail with a respective message. This is to prevent possible implementation errors,
* like filtering for a non-existing package in the that-clause causing an always-passing rule.<br>
* Note that this method will override the configuration property {@code archRule.failOnEmptyShould}.
*
* @param allowEmptyShould Whether the rule fails if the should-clause is evaluated with an empty set of elements
* @return A (new) {@link ArchRule} with adjusted {@code allowEmptyShould} behavior
*/
@PublicAPI(usage = ACCESS)
ArchRule allowEmptyShould(boolean allowEmptyShould);

@PublicAPI(usage = ACCESS)
final class Assertions {
private static final ArchUnitExtensions extensions = new ArchUnitExtensions();
Expand Down Expand Up @@ -166,7 +176,7 @@ public EvaluationResult getResult() {
@Internal
class Factory {
public static <T> ArchRule create(final ClassesTransformer<T> classesTransformer, final ArchCondition<T> condition, final Priority priority) {
return new SimpleArchRule<>(priority, classesTransformer, condition, Optional.<String>empty());
return new SimpleArchRule<>(priority, classesTransformer, condition, Optional.<String>empty(), AllowEmptyShould.AS_CONFIGURED);
}

public static ArchRule withBecause(ArchRule rule, String reason) {
Expand All @@ -184,18 +194,20 @@ private static class SimpleArchRule<T> implements ArchRule {
private final ClassesTransformer<T> classesTransformer;
private final ArchCondition<T> condition;
private final Optional<String> overriddenDescription;
private final AllowEmptyShould allowEmptyShould;

private SimpleArchRule(Priority priority, ClassesTransformer<T> classesTransformer, ArchCondition<T> condition,
Optional<String> overriddenDescription) {
Optional<String> overriddenDescription, AllowEmptyShould allowEmptyShould) {
this.priority = priority;
this.classesTransformer = classesTransformer;
this.condition = condition;
this.overriddenDescription = overriddenDescription;
this.allowEmptyShould = allowEmptyShould;
}

@Override
public ArchRule as(String newDescription) {
return new SimpleArchRule<>(priority, classesTransformer, condition, Optional.of(newDescription));
return new SimpleArchRule<>(priority, classesTransformer, condition, Optional.of(newDescription), allowEmptyShould);
}

@Override
Expand All @@ -208,6 +220,11 @@ public ArchRule because(String reason) {
return withBecause(this, reason);
}

@Override
public ArchRule allowEmptyShould(boolean allowEmptyShould) {
return new SimpleArchRule<>(priority, classesTransformer, condition, overriddenDescription, AllowEmptyShould.fromBoolean(allowEmptyShould));
}

@Override
public EvaluationResult evaluate(JavaClasses classes) {
Iterable<T> allObjects = classesTransformer.transform(classes);
Expand All @@ -223,20 +240,18 @@ public EvaluationResult evaluate(JavaClasses classes) {
}

private void verifyNoEmptyShouldIfEnabled(Iterable<T> allObjects) {
if (isEmpty(allObjects) && isFailOnEmptyShouldEnabled()) {
throw new AssertionError("Rule failed to check any classes. " +
"This means either that no classes have been passed to the rule at all, " +
"or that no classes passed to the rule matched the `that()` clause. " +
"To allow rules being evaluated without checking any classes you can set the ArchUnit property " +
FAIL_ON_EMPTY_SHOULD_PROPERTY_NAME + " = " + false);
if (isEmpty(allObjects) && !allowEmptyShould.isAllowed()) {
throw new AssertionError(String.format(
"Rule '%s' failed to check any classes. "
+ "This means either that no classes have been passed to the rule at all, "
+ "or that no classes passed to the rule matched the `that()` clause. "
+ "To allow rules being evaluated without checking any classes you can either "
+ "use `%s.allowEmptyShould(true)` on a single rule or set the configuration property `%s = false` "
+ "to change the behavior globally.",
getDescription(), ArchRule.class.getSimpleName(), FAIL_ON_EMPTY_SHOULD_PROPERTY_NAME));
}
}

private boolean isFailOnEmptyShouldEnabled() {
return ArchConfiguration.get().getPropertyOrDefault(FAIL_ON_EMPTY_SHOULD_PROPERTY_NAME, TRUE.toString())
.equals(TRUE.toString());
}

@Override
public String getDescription() {
return overriddenDescription.isPresent() ?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,16 @@ public CompositeArchRule because(String reason) {
return new CompositeArchRule(priority, rules, createBecauseDescription(this, reason));
}

@Override
@PublicAPI(usage = ACCESS)
public ArchRule allowEmptyShould(boolean allowEmptyShould) {
ImmutableList.Builder<ArchRule> rulesWithOverriddenAllowEmptyShould = ImmutableList.builder();
for (ArchRule rule : rules) {
rulesWithOverriddenAllowEmptyShould.add(rule.allowEmptyShould(allowEmptyShould));
}
return new CompositeArchRule(priority, rulesWithOverriddenAllowEmptyShould.build(), description);
}

@Override
@PublicAPI(usage = ACCESS)
public EvaluationResult evaluate(JavaClasses classes) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ public ArchRule because(String reason) {
return ArchRule.Factory.withBecause(this, reason);
}

@Override
public ArchRule allowEmptyShould(boolean allowEmptyShould) {
return finishedRule.get().allowEmptyShould(allowEmptyShould);
}

@Override
public ArchRule as(String newDescription) {
return finishedRule.get().as(newDescription);
Expand Down
Loading