Skip to content

Commit

Permalink
Improve subclass exploration do not include generic interfaces add ja…
Browse files Browse the repository at this point in the history
…vadoc
  • Loading branch information
sarpsahinalp committed Aug 1, 2024
1 parent 7ff0b65 commit 50c2f40
Show file tree
Hide file tree
Showing 4 changed files with 52 additions and 42 deletions.
Original file line number Diff line number Diff line change
@@ -1,34 +1,41 @@
package de.tum.cit.ase.ares.api.architecturetest.java.postcompile;

import com.tngtech.archunit.core.domain.JavaClass;
import com.tngtech.archunit.core.domain.JavaClasses;
import com.tngtech.archunit.core.importer.ClassFileImporter;
import com.tngtech.archunit.core.importer.ImportOption;

import java.net.URL;
import java.util.Optional;

/**
* Custom class resolver to resolve classes that are outside classpath to be able to analyze them transitively.
*/
public class CustomClassResolver {

private CustomClassResolver() {
throw new IllegalStateException("Utility class");
}

/**
* Class file importer to import the class files.
* This is used to import the class files from the URL.
*/
private static final ClassFileImporter classFileImporter = new ClassFileImporter();
private final JavaClasses allClasses;

public CustomClassResolver() {
allClasses = new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.withImportOption(location -> location.toString().contains("jrt:/"))
.importClasspath();
}

/**
* Try to resolve the class by the given type name.
*
* @param typeName The type name of the class to resolve.
* @return The resolved class if it exists.
*/
public static Optional<JavaClass> tryResolve(String typeName) {
URL url = CustomClassResolver.class.getResource("/" + typeName.replace(".", "/") + ".class");
return url != null ? Optional.of(classFileImporter.importUrl(url).get(typeName)) : Optional.empty();
public Optional<JavaClass> tryResolve(String typeName) {
try {
return Optional.ofNullable(allClasses.get(typeName));
} catch (IllegalArgumentException e) {
return Optional.empty();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,14 +42,12 @@ private JavaArchitectureTestCaseCollection() {
"java.nio.file",
"java.util.prefs",
"sun.print",
"sun.security",
"java.util.jar",
"java.util.zip",
"sun.awt.X11",
"javax.imageio",
"javax.sound.midi",
"javax.swing.filechooser",
"java.awt.desktop");
"javax.swing.filechooser");

/**
* Load pre file contents
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,10 @@ public class TransitivelyAccessesMethodsCondition extends ArchCondition<JavaClas
/**
* Set of classes that does not lead to a violation if they are accessed
*/
private final static Set<String> bannedClasses = Set.of(
private static final Set<String> bannedClasses = Set.of(
"java.lang.Object",
"java.lang.String",
"java.security.AccessControl",
"java.util",
"sun.util"
);
Expand All @@ -48,17 +49,15 @@ public class TransitivelyAccessesMethodsCondition extends ArchCondition<JavaClas
private final TransitiveAccessPath transitiveAccessPath = new TransitiveAccessPath();

/**
* Map to store the resolved classes
* This maximizes the performance of the condition by resolving the classes only once
* Custom class resolver to resolve classes that are outside classpath to be able to analyze them transitively
*/
private final Map<String, JavaClass> resolvedClasses;
private final CustomClassResolver customClassResolver;

/**
* @param conditionPredicate Predicate to match the accessed methods
*/
public TransitivelyAccessesMethodsCondition(DescribedPredicate<? super JavaAccess<?>> conditionPredicate, JavaSupportedArchitectureTestCase javaSupportedArchitectureTestCase) {
super("transitively depend on classes that " + conditionPredicate.getDescription());
this.resolvedClasses = new HashMap<>();
switch (javaSupportedArchitectureTestCase) {
case FILESYSTEM_INTERACTION -> {
try {
Expand All @@ -75,6 +74,7 @@ public TransitivelyAccessesMethodsCondition(DescribedPredicate<? super JavaAcces
case null, default -> throw new IllegalStateException("JavaSupportedArchitecture cannot be null");
}
this.conditionPredicate = checkNotNull(conditionPredicate);
this.customClassResolver = new CustomClassResolver();
}

/**
Expand Down Expand Up @@ -163,38 +163,42 @@ private boolean addAccessesToPathFrom(
* @return all accesses to the same target as the supplied item that are not in the analyzed classes
*/
private Set<JavaAccess<?>> getDirectAccessTargetsOutsideOfAnalyzedClasses(JavaAccess<?> item) {
if (bannedClasses.contains(item.getTargetOwner().getFullName())) {
// If the target owner is in the banned classes, return an empty set
if (bannedClasses.stream().anyMatch(p -> item.getTargetOwner().getFullName().startsWith(p)) || item.getTargetOwner().isAssignableTo(Exception.class) || item.getTargetOwner().isAssignableTo(Error.class)) {
return Collections.emptySet();
}

Set<JavaClass> subclasses = new HashSet<>(item.getTargetOwner().getSubclasses());
subclasses.add(item.getTargetOwner());
// Get all subclasses of the target owner including the target owner
JavaClass resolvedTarget = resolveTargetOwner(item.getTargetOwner());

Set<JavaAccess<?>> accesses = new HashSet<>();
// Match the accesses to the target
Set<JavaClass> subclasses = resolvedTarget.getSubclasses().stream().map(this::resolveTargetOwner).collect(toSet());
subclasses.add(resolvedTarget);

for (JavaClass subclass : subclasses) {
Optional<JavaClass> resolvedTarget;
if (resolvedClasses.get(subclass.getFullName()) != null) {
resolvedTarget = Optional.of(item.getTargetOwner());
} else {
resolvedTarget = CustomClassResolver.tryResolve(item.getTargetOwner().getFullName());
resolvedTarget.ifPresent(javaClass -> resolvedClasses.put(javaClass.getFullName(), javaClass));
}

accesses.addAll(resolvedTarget.map(javaClass -> javaClass.getAccessesFromSelf()
.stream()
.filter(a -> a
.getOrigin()
.getFullName()
.equals(item
.getTarget()
.getFullName()
)
)
.collect(toSet())).orElseGet(Set::of));
if (subclasses.size() > 20) {
return Collections.emptySet();
}

return accesses;
return subclasses.stream()
.map(javaClass -> getAccessesFromClass(javaClass, item.getTarget().getName()))
.flatMap(Set::stream)
.collect(toSet());
}

private Set<JavaAccess<?>> getAccessesFromClass(JavaClass javaClass, String methodName) {
return javaClass.getAccessesFromSelf()
.stream()
.filter(a -> a
.getOrigin()
.getName()
.equals(methodName))
.filter(a -> bannedClasses.stream().noneMatch(p -> a.getTargetOwner().getFullName().startsWith(p)))
.collect(toSet());
}

private JavaClass resolveTargetOwner(JavaClass targetOwner) {
Optional<JavaClass> resolvedTarget = customClassResolver.tryResolve(targetOwner.getFullName());
return resolvedTarget.orElse(targetOwner);
}
}
}
1 change: 1 addition & 0 deletions src/main/resources/archunit.properties
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
# Set to false to ignore missing dependencies in the classpath, as they are resolved manually by the de.tum.cit.ase.ares.api.architecturetest.java.postcompile.CustomClassResolver
resolveMissingDependenciesFromClassPath=false

0 comments on commit 50c2f40

Please sign in to comment.