diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index a2f0fdd0ef..dfab58f59d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -8,20 +8,21 @@ on: pull_request: env: - build_java_version: 17 + build_java_version: 21 jobs: build: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Java - uses: actions/setup-java@v1 + uses: actions/setup-java@v4.7.0 with: + distribution: 'zulu' java-version: ${{ env.build_java_version }} - name: Build - uses: gradle/gradle-build-action@v2 + uses: gradle/actions/setup-gradle@v3 with: arguments: build - name: Check project files unmodified @@ -44,27 +45,44 @@ jobs: - windows-latest test_java_version: - 8 - - 9 - - 10 - 11 - - 12 - - 13 - - 14 - - 15 - - 16 - 17 + - 21 runs-on: ${{ matrix.os }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Build JDK - uses: actions/setup-java@v1 + uses: actions/setup-java@v4.7.0 with: + distribution: 'zulu' java-version: ${{ env.build_java_version }} + - name: Set up Test JDK + uses: actions/setup-java@v4.7.0 + with: + distribution: 'zulu' + java-version: ${{ matrix.test_java_version }} + - name: Provide installed JDKs + uses: actions/github-script@v7 + id: provideJdkPaths + with: + script: | + for ( let envVarName in process.env ) { + if (/JAVA_HOME_\d.*64/.test(envVarName)) { + const version = envVarName.match(/JAVA_HOME_(\d+).*64/)[1]; + if (version === "${{ matrix.test_java_version }}") { + core.exportVariable('test_jdk_path', process.env[envVarName]); + } else if (version === "${{ env.build_java_version }}") { + core.exportVariable('build_jdk_path', process.env[envVarName]); + } + } + } - name: Test - uses: gradle/gradle-build-action@v2 + uses: gradle/actions/setup-gradle@v3 + env: + JAVA_HOME: ${{ env.build_jdk_path }} with: - arguments: test -PallTests -PtestJavaVersion=${{ matrix.test_java_version }} + arguments: test -PallTests -PtestJavaVersion=${{ matrix.test_java_version }} -Porg.gradle.java.installations.paths=${{ env.test_jdk_path }} cache-disabled: true integration-test: @@ -77,29 +95,25 @@ jobs: - windows-latest test_java_version: - 8 - - 9 - - 10 - 11 - - 12 - - 13 - - 14 - - 15 - - 16 - 17 + - 21 runs-on: ${{ matrix.os }} steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Set up Build JDK - uses: actions/setup-java@v1 + uses: actions/setup-java@v4.7.0 with: + distribution: 'zulu' java-version: ${{ env.build_java_version }} - name: Set up Test JDK - uses: actions/setup-java@v1 + uses: actions/setup-java@v4.7.0 with: + distribution: 'zulu' java-version: ${{ matrix.test_java_version }} - name: Provide installed JDKs - uses: actions/github-script@v6 + uses: actions/github-script@v7 id: provideJdkPaths with: script: | @@ -114,14 +128,14 @@ jobs: } } - name: Publish to Maven Local - uses: gradle/gradle-build-action@v2 + uses: gradle/actions/setup-gradle@v3 env: JAVA_HOME: ${{ env.build_jdk_path }} with: arguments: build -xtest -xspotbugsMain -xjavadoc publishToMavenLocal - name: Integration test - uses: gradle/gradle-build-action@v2 + uses: gradle/actions/setup-gradle@v3 env: JAVA_HOME: ${{ env.build_jdk_path }} with: - arguments: build -xtest -xspotbugsMain -xjavadoc runMavenTest -PtestJavaVersion=${{ matrix.test_java_version }} -Porg.gradle.java.installations.paths=${{ env.test_jdk_path }} + arguments: runMavenTest -PtestJavaVersion=${{ matrix.test_java_version }} -Porg.gradle.java.installations.paths=${{ env.test_jdk_path }} diff --git a/.github/workflows/check-dependencies.yml b/.github/workflows/check-dependencies.yml index 00fc3758cc..2f2d3ebda3 100644 --- a/.github/workflows/check-dependencies.yml +++ b/.github/workflows/check-dependencies.yml @@ -10,13 +10,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Check for dependency updates - uses: gradle/gradle-build-action@v2 + uses: gradle/actions/setup-gradle@v3 with: arguments: checkDependencyUpdates -DoutputFormatter=plain,json - name: Create issue/comment if ASM is not up-to-date - uses: actions/github-script@v6 + uses: actions/github-script@v7 env: GITHUB_SERVER_URL: ${{github.server_url}} GITHUB_REPOSITORY: ${{github.repository}} diff --git a/.github/workflows/gradle-wrapper-validation.yml b/.github/workflows/gradle-wrapper-validation.yml index 2938df61ec..5a1717a324 100644 --- a/.github/workflows/gradle-wrapper-validation.yml +++ b/.github/workflows/gradle-wrapper-validation.yml @@ -12,5 +12,5 @@ jobs: name: "Validation" runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: gradle/wrapper-validation-action@v1.0.4 + - uses: actions/checkout@v4 + - uses: gradle/actions/wrapper-validation@v3.3.1 diff --git a/.github/workflows/update-gradle-wrapper.yml b/.github/workflows/update-gradle-wrapper.yml index b4570c8c59..a15e91ce22 100644 --- a/.github/workflows/update-gradle-wrapper.yml +++ b/.github/workflows/update-gradle-wrapper.yml @@ -9,14 +9,15 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install Gradle JDK - uses: actions/setup-java@v1 + uses: actions/setup-java@v4.7.0 with: - java-version: 14 + distribution: 'zulu' + java-version: 17 - name: Update Gradle Wrapper - uses: gradle-update/update-gradle-wrapper-action@v1.0.18 + uses: gradle-update/update-gradle-wrapper-action@v2.0.1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d39a90dfa6..66b11a5bd3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -33,13 +33,35 @@ To submit a contribution, please follow the following workflow: ### Commits Commit messages should be clear and fully elaborate the context and the reason of a change. -If your commit refers to an issue, please post-fix it with the issue number, e.g. +Each commit message should follow the following conventions: + +* it may use markdown to improve readability on GitHub +* it must start with a title + * less than 70 characters + * starting lowercase + * written in imperative style as to complete the statement "if applied this commit will" (e.g. "fix race condition when loading import plugins") +* if the commit is not trivial the title should be followed by a body + * separated from the title by a blank line + * explaining all necessary context and reasons for the change +* if your commit refers to an issue, please post-fix it with the issue number, e.g. `Issue: #123` or `Resolves: #123` + +A full example: ``` -Issue: #123 +report classes contained in multiple PlantUML components as violation + +So far when checking an `ArchRule` based on `PlantUmlArchCondition` we were throwing an exception +if a class was contained in multiple diagram components. +This causes problems for legacy code bases where some classes might violate the conventions +in such a way, which should be frozen as violations to be iteratively fixed. +But throwing a `ComponentIntersectionException` prevents any such approach. +We thus replace the `ComponentIntersectionException` by a regular rule violation that can be treated +like any other violation of the architecture and in particular be frozen via `FreezingArchRule`. + +Resolves: #960 ``` -Furthermore, commits should be signed off according to the [DCO](DCO). +Furthermore, commits must be signed off according to the [DCO](DCO). ### Pull Requests diff --git a/NOTICE b/NOTICE index 9ae20482e5..645da63af7 100644 --- a/NOTICE +++ b/NOTICE @@ -1,5 +1,5 @@ ArchUnit -Copyright 2016 and onwards Peter Gafert +Copyright 2016 and onwards Peter Gafert This product includes software developed at TNG Technology Consulting GmbH (https://www.tngtech.com/). \ No newline at end of file diff --git a/README.md b/README.md index 1e53109207..cb90295897 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ framework. ###### Gradle ``` -testImplementation 'com.tngtech.archunit:archunit:1.0.0-rc1' +testImplementation 'com.tngtech.archunit:archunit:1.3.0' ``` ###### Maven @@ -26,7 +26,7 @@ testImplementation 'com.tngtech.archunit:archunit:1.0.0-rc1' com.tngtech.archunit archunit - 1.0.0-rc1 + 1.3.0 test ``` diff --git a/archunit-3rd-party-test/build.gradle b/archunit-3rd-party-test/build.gradle new file mode 100644 index 0000000000..90ff8bf6f5 --- /dev/null +++ b/archunit-3rd-party-test/build.gradle @@ -0,0 +1,15 @@ +plugins { + id 'archunit.java-conventions' +} + +ext.moduleName = 'com.tngtech.archunit.thirdpartytest' + +dependencies { + testImplementation project(path: ':archunit', configuration: 'shadow') + testImplementation project(path: ':archunit', configuration: 'tests') + testImplementation dependency.springBootLoader + dependency.addGuava { dependencyNotation, config -> testImplementation(dependencyNotation, config) } + testImplementation dependency.junit4 + testImplementation dependency.junit_dataprovider + testImplementation dependency.assertj +} diff --git a/archunit-3rd-party-test/src/test/java/com/tngtech/archunit/core/importer/SpringLocationsTest.java b/archunit-3rd-party-test/src/test/java/com/tngtech/archunit/core/importer/SpringLocationsTest.java new file mode 100644 index 0000000000..bcdc624d47 --- /dev/null +++ b/archunit-3rd-party-test/src/test/java/com/tngtech/archunit/core/importer/SpringLocationsTest.java @@ -0,0 +1,96 @@ +package com.tngtech.archunit.core.importer; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.Arrays; +import java.util.Iterator; +import java.util.function.Function; +import java.util.jar.JarFile; +import java.util.stream.Stream; + +import com.tngtech.archunit.core.importer.testexamples.SomeEnum; +import com.tngtech.archunit.testutil.SystemPropertiesRule; +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.springframework.boot.loader.LaunchedURLClassLoader; +import org.springframework.boot.loader.archive.Archive; +import org.springframework.boot.loader.archive.JarFileArchive; + +import static com.google.common.collect.Iterators.getOnlyElement; +import static com.google.common.collect.MoreCollectors.onlyElement; +import static com.google.common.collect.Streams.stream; +import static com.google.common.io.ByteStreams.toByteArray; +import static com.tngtech.archunit.core.importer.LocationTest.classFileEntry; +import static com.tngtech.archunit.core.importer.LocationTest.urlOfClass; +import static com.tngtech.archunit.core.importer.LocationsTest.unchecked; +import static com.tngtech.java.junit.dataprovider.DataProviders.testForEach; +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(DataProviderRunner.class) +public class SpringLocationsTest { + /** + * Spring Boot configures some system properties that we want to reset afterward (e.g. custom URL stream handler) + */ + @Rule + public final SystemPropertiesRule systemPropertiesRule = new SystemPropertiesRule(); + + @DataProvider + public static Object[][] springBootJars() { + Function, TestJarFile> createSpringBootJar = setUpJarFile -> setUpJarFile.apply(new TestJarFile()) + .withNestedClassFilesDirectory("BOOT-INF/classes") + .withEntry(classFileEntry(SomeEnum.class).toAbsolutePath()); + + return testForEach( + createSpringBootJar.apply(TestJarFile::withDirectoryEntries), + createSpringBootJar.apply(TestJarFile::withoutDirectoryEntries) + ); + } + + @Test + @UseDataProvider("springBootJars") + public void finds_locations_of_packages_from_Spring_Boot_ClassLoader_for_JARs(TestJarFile jarFileToTest) throws Exception { + try (JarFile jarFile = jarFileToTest.create()) { + + configureSpringBootContextClassLoaderKnowingOnly(jarFile); + + String jarUri = new File(jarFile.getName()).toURI().toString(); + Location location = Locations.ofPackage(SomeEnum.class.getPackage().getName()).stream() + .filter(it -> it.contains(jarUri)) + .collect(onlyElement()); + + byte[] expectedClassContent = toByteArray(urlOfClass(SomeEnum.class).openStream()); + Stream actualClassContents = stream(location.asClassFileSource(new ImportOptions())) + .map(it -> unchecked(() -> toByteArray(it.openStream()))); + + boolean containsExpectedContent = actualClassContents.anyMatch(it -> Arrays.equals(it, expectedClassContent)); + assertThat(containsExpectedContent) + .as("one of the found class files has the expected class file content") + .isTrue(); + } + } + + private static void configureSpringBootContextClassLoaderKnowingOnly(JarFile jarFile) throws IOException { + // This hooks in Spring Boot's own JAR URL protocol handler which knows how to handle URLs with + // multiple separators (e.g. "jar:file:/dir/some.jar!/BOOT-INF/classes!/pkg/some.class") + org.springframework.boot.loader.jar.JarFile.registerUrlProtocolHandler(); + + try (JarFileArchive jarFileArchive = new JarFileArchive(new File(jarFile.getName()))) { + JarFileArchive bootInfClassArchive = getNestedJarFileArchive(jarFileArchive, "BOOT-INF/classes/"); + + Thread.currentThread().setContextClassLoader( + new LaunchedURLClassLoader(false, bootInfClassArchive, new URL[]{bootInfClassArchive.getUrl()}, null) + ); + } + } + + @SuppressWarnings("SameParameterValue") + private static JarFileArchive getNestedJarFileArchive(JarFileArchive jarFileArchive, String path) throws IOException { + Iterator archiveCandidates = jarFileArchive.getNestedArchives(entry -> entry.getName().equals(path), entry -> true); + return (JarFileArchive) getOnlyElement(archiveCandidates); + } +} diff --git a/archunit-example/example-junit4/build.gradle b/archunit-example/example-junit4/build.gradle index 9e3c9aea43..7b216adf83 100644 --- a/archunit-example/example-junit4/build.gradle +++ b/archunit-example/example-junit4/build.gradle @@ -7,15 +7,11 @@ ext.moduleName = 'com.tngtech.archunit.example.junit4' dependencies { testImplementation project(path: ':archunit-junit4') testImplementation project(path: ':archunit-example:example-plain') - - testRuntimeOnly dependency.log4j_api - testRuntimeOnly dependency.log4j_core - testRuntimeOnly dependency.log4j_slf4j } test { - if (!project.hasProperty('example')) { - useJUnit { + useJUnit { + if (!project.hasProperty('example')) { excludeCategories 'com.tngtech.archunit.exampletest.junit4.Example' } } diff --git a/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/ModulesTest.java b/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/ModulesTest.java new file mode 100644 index 0000000000..5db5b8447a --- /dev/null +++ b/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/ModulesTest.java @@ -0,0 +1,193 @@ +package com.tngtech.archunit.exampletest.junit4; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; + +import com.tngtech.archunit.base.DescribedFunction; +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaPackage; +import com.tngtech.archunit.example.AppModule; +import com.tngtech.archunit.example.ModuleApi; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.junit.ArchUnitRunner; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.library.modules.AnnotationDescriptor; +import com.tngtech.archunit.library.modules.ArchModule; +import com.tngtech.archunit.library.modules.ModuleDependency; +import com.tngtech.archunit.library.modules.syntax.DescriptorFunction; +import org.junit.experimental.categories.Category; +import org.junit.runner.RunWith; + +import static com.tngtech.archunit.base.DescribedPredicate.alwaysTrue; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.belongToAnyOf; +import static com.tngtech.archunit.library.modules.syntax.AllowedModuleDependencies.allow; +import static com.tngtech.archunit.library.modules.syntax.ModuleDependencyScope.consideringOnlyDependenciesInAnyPackage; +import static com.tngtech.archunit.library.modules.syntax.ModuleRuleDefinition.modules; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toList; + +@Category(Example.class) +@RunWith(ArchUnitRunner.class) +@AnalyzeClasses(packages = "com.tngtech.archunit.example") +public class ModulesTest { + + /** + * This example demonstrates how to derive modules from a package pattern. + * The `..` stands for arbitrary many packages and the `(*)` captures one specific subpackage name within the + * package tree. + */ + @ArchTest + public static ArchRule modules_should_respect_their_declared_dependencies__use_package_API = + modules() + .definedByPackages("..shopping.(*)..") + .should().respectTheirAllowedDependencies( + allow() + .fromModule("catalog").toModules("product") + .fromModule("customer").toModules("address") + .fromModule("importer").toModules("catalog", "xml") + .fromModule("order").toModules("customer", "product"), + consideringOnlyDependenciesInAnyPackage("..example..")) + .ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class)); + + /** + * This example demonstrates how to easily derive modules from classes annotated with a certain annotation, + * and also test for allowed dependencies and correct access to exposed packages by declared descriptor annotation properties. + * Within the example those are simply package-info files which denote the root of the modules by + * being annotated with @AppModule. + */ + @ArchTest + public static ArchRule modules_should_respect_their_declared_dependencies_and_exposed_packages = + modules() + .definedByAnnotation(AppModule.class) + .should().respectTheirAllowedDependenciesDeclaredIn("allowedDependencies", + consideringOnlyDependenciesInAnyPackage("..example..")) + .ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class)) + .andShould().onlyDependOnEachOtherThroughPackagesDeclaredIn("exposedPackages"); + + /** + * This example demonstrates how to easily derive modules from classes annotated with a certain annotation, + * and also test for allowed dependencies using the descriptor annotation. + * Within the example those are simply package-info files which denote the root of the modules by + * being annotated with @AppModule. + */ + @ArchTest + public static ArchRule modules_should_respect_their_declared_dependencies__use_annotation_API = + modules() + .definedByAnnotation(AppModule.class) + .should().respectTheirAllowedDependencies( + declaredByDescriptorAnnotation(), + consideringOnlyDependenciesInAnyPackage("..example..") + ) + .ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class)); + + /** + * This example demonstrates how to use the slightly more generic root class API to define modules. + * While the result in this example is the same as the above, this API in general can be used to + * use arbitrary classes as roots of modules. + * For example if there is always a central interface denoted in some way, + * the modules could be derived from these interfaces. + */ + @ArchTest + public static ArchRule modules_should_respect_their_declared_dependencies__use_root_class_API = + modules() + .definedByRootClasses( + DescribedPredicate.describe("annotated with @" + AppModule.class.getSimpleName(), (JavaClass rootClass) -> + rootClass.isAnnotatedWith(AppModule.class)) + ) + .derivingModuleFromRootClassBy( + DescribedFunction.describe("annotation @" + AppModule.class.getSimpleName(), (JavaClass rootClass) -> { + AppModule module = rootClass.getAnnotationOfType(AppModule.class); + return new AnnotationDescriptor<>(module.name(), module); + }) + ) + .should().respectTheirAllowedDependencies( + declaredByDescriptorAnnotation(), + consideringOnlyDependenciesInAnyPackage("..example..") + ) + .ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class)); + + /** + * This example demonstrates how to use the generic API to define modules. + * The result in this example again is the same as the above, however in general the generic API + * allows to derive modules in a completely customizable way. + */ + @ArchTest + public static ArchRule modules_should_respect_their_declared_dependencies__use_generic_API = + modules() + .definedBy(identifierFromModulesAnnotation()) + .derivingModule(fromModulesAnnotation()) + .should().respectTheirAllowedDependencies( + declaredByDescriptorAnnotation(), + consideringOnlyDependenciesInAnyPackage("..example..") + ) + .ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class)); + + /** + * This example demonstrates how to check that modules only depend on each other through a specific API. + */ + @ArchTest + public static ArchRule modules_should_only_depend_on_each_other_through_module_API = + modules() + .definedByAnnotation(AppModule.class) + .should().onlyDependOnEachOtherThroughClassesThat().areAnnotatedWith(ModuleApi.class); + + /** + * This example demonstrates how to check for cyclic dependencies between modules. + */ + @ArchTest + public static ArchRule modules_should_be_free_of_cycles = + modules() + .definedByAnnotation(AppModule.class) + .should().beFreeOfCycles(); + + private static DescribedPredicate>> declaredByDescriptorAnnotation() { + return DescribedPredicate.describe("declared by descriptor annotation", moduleDependency -> { + AppModule descriptor = moduleDependency.getOrigin().getDescriptor().getAnnotation(); + List allowedDependencies = stream(descriptor.allowedDependencies()).collect(toList()); + return allowedDependencies.contains(moduleDependency.getTarget().getName()); + }); + } + + private static IdentifierFromAnnotation identifierFromModulesAnnotation() { + return new IdentifierFromAnnotation(); + } + + private static DescriptorFunction> fromModulesAnnotation() { + return DescriptorFunction.describe(String.format("from @%s(name)", AppModule.class.getSimpleName()), + (ArchModule.Identifier identifier, Set containedClasses) -> { + JavaClass rootClass = containedClasses.stream().filter(it -> it.isAnnotatedWith(AppModule.class)).findFirst().get(); + AppModule module = rootClass.getAnnotationOfType(AppModule.class); + return new AnnotationDescriptor<>(module.name(), module); + }); + } + + private static class IdentifierFromAnnotation extends DescribedFunction { + IdentifierFromAnnotation() { + super("root classes with annotation @" + AppModule.class.getSimpleName()); + } + + @Override + public ArchModule.Identifier apply(JavaClass javaClass) { + return getIdentifierOfPackage(javaClass.getPackage()); + } + + private ArchModule.Identifier getIdentifierOfPackage(JavaPackage javaPackage) { + Optional identifierInCurrentPackage = javaPackage.getClasses().stream() + .filter(it -> it.isAnnotatedWith(AppModule.class)) + .findFirst() + .map(annotatedClassInPackage -> ArchModule.Identifier.from(annotatedClassInPackage.getAnnotationOfType(AppModule.class).name())); + + return identifierInCurrentPackage.orElseGet(identifierInParentPackageOf(javaPackage)); + } + + private Supplier identifierInParentPackageOf(JavaPackage javaPackage) { + return () -> javaPackage.getParent() + .map(this::getIdentifierOfPackage) + .orElseGet(ArchModule.Identifier::ignore); + } + } +} diff --git a/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/PlantUmlArchitectureTest.java b/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/PlantUmlArchitectureTest.java index a0240f786e..f8cf2a70fe 100644 --- a/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/PlantUmlArchitectureTest.java +++ b/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/PlantUmlArchitectureTest.java @@ -3,9 +3,9 @@ import java.net.URL; import com.tngtech.archunit.core.domain.PackageMatchers; -import com.tngtech.archunit.example.plantuml.catalog.ProductCatalog; -import com.tngtech.archunit.example.plantuml.order.Order; -import com.tngtech.archunit.example.plantuml.product.Product; +import com.tngtech.archunit.example.shopping.catalog.ProductCatalog; +import com.tngtech.archunit.example.shopping.order.Order; +import com.tngtech.archunit.example.shopping.product.Product; import com.tngtech.archunit.junit.AnalyzeClasses; import com.tngtech.archunit.junit.ArchTest; import com.tngtech.archunit.junit.ArchUnitRunner; @@ -17,14 +17,14 @@ import static com.tngtech.archunit.core.domain.JavaClass.Functions.GET_PACKAGE_NAME; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.equivalentTo; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; -import static com.tngtech.archunit.library.plantuml.PlantUmlArchCondition.Configuration.consideringAllDependencies; -import static com.tngtech.archunit.library.plantuml.PlantUmlArchCondition.Configuration.consideringOnlyDependenciesInAnyPackage; -import static com.tngtech.archunit.library.plantuml.PlantUmlArchCondition.Configuration.consideringOnlyDependenciesInDiagram; -import static com.tngtech.archunit.library.plantuml.PlantUmlArchCondition.adhereToPlantUmlDiagram; +import static com.tngtech.archunit.library.plantuml.rules.PlantUmlArchCondition.Configuration.consideringAllDependencies; +import static com.tngtech.archunit.library.plantuml.rules.PlantUmlArchCondition.Configuration.consideringOnlyDependenciesInAnyPackage; +import static com.tngtech.archunit.library.plantuml.rules.PlantUmlArchCondition.Configuration.consideringOnlyDependenciesInDiagram; +import static com.tngtech.archunit.library.plantuml.rules.PlantUmlArchCondition.adhereToPlantUmlDiagram; @Category(Example.class) @RunWith(ArchUnitRunner.class) -@AnalyzeClasses(packages = "com.tngtech.archunit.example.plantuml") +@AnalyzeClasses(packages = "com.tngtech.archunit.example.shopping") public class PlantUmlArchitectureTest { private static final URL plantUmlDiagram = PlantUmlArchitectureTest.class.getResource("shopping_example.puml"); diff --git a/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/SlicesIsolationTest.java b/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/SlicesIsolationTest.java index b072cb7837..3ac1d30fbc 100644 --- a/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/SlicesIsolationTest.java +++ b/archunit-example/example-junit4/src/test/java/com/tngtech/archunit/exampletest/junit4/SlicesIsolationTest.java @@ -38,7 +38,7 @@ public class SlicesIsolationTest { .ignoreDependency(UseCaseOneTwoController.class, UseCaseTwoController.class) .ignoreDependency(nameMatching(".*controller\\.three.*"), alwaysTrue()); - private static DescribedPredicate containDescription(final String descriptionPart) { + private static DescribedPredicate containDescription(String descriptionPart) { return new DescribedPredicate("contain description '%s'", descriptionPart) { @Override public boolean test(Slice input) { diff --git a/archunit-example/example-junit5/build.gradle b/archunit-example/example-junit5/build.gradle index 343f424c78..a0bac6e726 100644 --- a/archunit-example/example-junit5/build.gradle +++ b/archunit-example/example-junit5/build.gradle @@ -8,10 +8,6 @@ ext.minimumJavaVersion = JavaVersion.VERSION_1_8 dependencies { testImplementation project(path: ':archunit-junit5') testImplementation project(path: ':archunit-example:example-plain') - - testRuntimeOnly dependency.log4j_api - testRuntimeOnly dependency.log4j_core - testRuntimeOnly dependency.log4j_slf4j } test { diff --git a/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/ModulesTest.java b/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/ModulesTest.java new file mode 100644 index 0000000000..dde3b13576 --- /dev/null +++ b/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/ModulesTest.java @@ -0,0 +1,190 @@ +package com.tngtech.archunit.exampletest.junit5; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; + +import com.tngtech.archunit.base.DescribedFunction; +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaPackage; +import com.tngtech.archunit.example.AppModule; +import com.tngtech.archunit.example.ModuleApi; +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTag; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.library.modules.AnnotationDescriptor; +import com.tngtech.archunit.library.modules.ArchModule; +import com.tngtech.archunit.library.modules.ModuleDependency; +import com.tngtech.archunit.library.modules.syntax.DescriptorFunction; + +import static com.tngtech.archunit.base.DescribedPredicate.alwaysTrue; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.belongToAnyOf; +import static com.tngtech.archunit.library.modules.syntax.AllowedModuleDependencies.allow; +import static com.tngtech.archunit.library.modules.syntax.ModuleDependencyScope.consideringOnlyDependenciesInAnyPackage; +import static com.tngtech.archunit.library.modules.syntax.ModuleRuleDefinition.modules; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toList; + +@ArchTag("example") +@AnalyzeClasses(packages = "com.tngtech.archunit.example") +public class ModulesTest { + + /** + * This example demonstrates how to derive modules from a package pattern. + * The `..` stands for arbitrary many packages and the `(*)` captures one specific subpackage name within the + * package tree. + */ + @ArchTest + static ArchRule modules_should_respect_their_declared_dependencies__use_package_API = + modules() + .definedByPackages("..shopping.(*)..") + .should().respectTheirAllowedDependencies( + allow() + .fromModule("catalog").toModules("product") + .fromModule("customer").toModules("address") + .fromModule("importer").toModules("catalog", "xml") + .fromModule("order").toModules("customer", "product"), + consideringOnlyDependenciesInAnyPackage("..example..")) + .ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class)); + + /** + * This example demonstrates how to easily derive modules from classes annotated with a certain annotation, + * and also test for allowed dependencies and correct access to exposed packages by declared descriptor annotation properties. + * Within the example those are simply package-info files which denote the root of the modules by + * being annotated with @AppModule. + */ + @ArchTest + static ArchRule modules_should_respect_their_declared_dependencies_and_exposed_packages = + modules() + .definedByAnnotation(AppModule.class) + .should().respectTheirAllowedDependenciesDeclaredIn("allowedDependencies", + consideringOnlyDependenciesInAnyPackage("..example..")) + .ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class)) + .andShould().onlyDependOnEachOtherThroughPackagesDeclaredIn("exposedPackages"); + + /** + * This example demonstrates how to easily derive modules from classes annotated with a certain annotation, + * and also test for allowed dependencies using the descriptor annotation. + * Within the example those are simply package-info files which denote the root of the modules by + * being annotated with @AppModule. + */ + @ArchTest + static ArchRule modules_should_respect_their_declared_dependencies__use_annotation_API = + modules() + .definedByAnnotation(AppModule.class) + .should().respectTheirAllowedDependencies( + declaredByDescriptorAnnotation(), + consideringOnlyDependenciesInAnyPackage("..example..") + ) + .ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class)); + + /** + * This example demonstrates how to use the slightly more generic root class API to define modules. + * While the result in this example is the same as the above, this API in general can be used to + * use arbitrary classes as roots of modules. + * For example if there is always a central interface denoted in some way, + * the modules could be derived from these interfaces. + */ + @ArchTest + static ArchRule modules_should_respect_their_declared_dependencies__use_root_class_API = + modules() + .definedByRootClasses( + DescribedPredicate.describe("annotated with @" + AppModule.class.getSimpleName(), (JavaClass rootClass) -> + rootClass.isAnnotatedWith(AppModule.class)) + ) + .derivingModuleFromRootClassBy( + DescribedFunction.describe("annotation @" + AppModule.class.getSimpleName(), (JavaClass rootClass) -> { + AppModule module = rootClass.getAnnotationOfType(AppModule.class); + return new AnnotationDescriptor<>(module.name(), module); + }) + ) + .should().respectTheirAllowedDependencies( + declaredByDescriptorAnnotation(), + consideringOnlyDependenciesInAnyPackage("..example..") + ) + .ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class)); + + /** + * This example demonstrates how to use the generic API to define modules. + * The result in this example again is the same as the above, however in general the generic API + * allows to derive modules in a completely customizable way. + */ + @ArchTest + static ArchRule modules_should_respect_their_declared_dependencies__use_generic_API = + modules() + .definedBy(identifierFromModulesAnnotation()) + .derivingModule(fromModulesAnnotation()) + .should().respectTheirAllowedDependencies( + declaredByDescriptorAnnotation(), + consideringOnlyDependenciesInAnyPackage("..example..") + ) + .ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class)); + + /** + * This example demonstrates how to check that modules only depend on each other through a specific API. + */ + @ArchTest + static ArchRule modules_should_only_depend_on_each_other_through_module_API = + modules() + .definedByAnnotation(AppModule.class) + .should().onlyDependOnEachOtherThroughClassesThat().areAnnotatedWith(ModuleApi.class); + + /** + * This example demonstrates how to check for cyclic dependencies between modules. + */ + @ArchTest + static ArchRule modules_should_be_free_of_cycles = + modules() + .definedByAnnotation(AppModule.class) + .should().beFreeOfCycles(); + + private static DescribedPredicate>> declaredByDescriptorAnnotation() { + return DescribedPredicate.describe("declared by descriptor annotation", moduleDependency -> { + AppModule descriptor = moduleDependency.getOrigin().getDescriptor().getAnnotation(); + List allowedDependencies = stream(descriptor.allowedDependencies()).collect(toList()); + return allowedDependencies.contains(moduleDependency.getTarget().getName()); + }); + } + + private static IdentifierFromAnnotation identifierFromModulesAnnotation() { + return new IdentifierFromAnnotation(); + } + + private static DescriptorFunction> fromModulesAnnotation() { + return DescriptorFunction.describe(String.format("from @%s(name)", AppModule.class.getSimpleName()), + (ArchModule.Identifier identifier, Set containedClasses) -> { + JavaClass rootClass = containedClasses.stream().filter(it -> it.isAnnotatedWith(AppModule.class)).findFirst().get(); + AppModule module = rootClass.getAnnotationOfType(AppModule.class); + return new AnnotationDescriptor<>(module.name(), module); + }); + } + + private static class IdentifierFromAnnotation extends DescribedFunction { + IdentifierFromAnnotation() { + super("root classes with annotation @" + AppModule.class.getSimpleName()); + } + + @Override + public ArchModule.Identifier apply(JavaClass javaClass) { + return getIdentifierOfPackage(javaClass.getPackage()); + } + + private ArchModule.Identifier getIdentifierOfPackage(JavaPackage javaPackage) { + Optional identifierInCurrentPackage = javaPackage.getClasses().stream() + .filter(it -> it.isAnnotatedWith(AppModule.class)) + .findFirst() + .map(annotatedClassInPackage -> ArchModule.Identifier.from(annotatedClassInPackage.getAnnotationOfType(AppModule.class).name())); + + return identifierInCurrentPackage.orElseGet(identifierInParentPackageOf(javaPackage)); + } + + private Supplier identifierInParentPackageOf(JavaPackage javaPackage) { + return () -> javaPackage.getParent() + .map(this::getIdentifierOfPackage) + .orElseGet(ArchModule.Identifier::ignore); + } + } +} diff --git a/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/PlantUmlArchitectureTest.java b/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/PlantUmlArchitectureTest.java index 9b1abe0ef4..db64b7d984 100644 --- a/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/PlantUmlArchitectureTest.java +++ b/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/PlantUmlArchitectureTest.java @@ -3,9 +3,9 @@ import java.net.URL; import com.tngtech.archunit.core.domain.PackageMatchers; -import com.tngtech.archunit.example.plantuml.catalog.ProductCatalog; -import com.tngtech.archunit.example.plantuml.order.Order; -import com.tngtech.archunit.example.plantuml.product.Product; +import com.tngtech.archunit.example.shopping.catalog.ProductCatalog; +import com.tngtech.archunit.example.shopping.order.Order; +import com.tngtech.archunit.example.shopping.product.Product; import com.tngtech.archunit.junit.AnalyzeClasses; import com.tngtech.archunit.junit.ArchTag; import com.tngtech.archunit.junit.ArchTest; @@ -15,13 +15,13 @@ import static com.tngtech.archunit.core.domain.JavaClass.Functions.GET_PACKAGE_NAME; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.equivalentTo; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; -import static com.tngtech.archunit.library.plantuml.PlantUmlArchCondition.Configuration.consideringAllDependencies; -import static com.tngtech.archunit.library.plantuml.PlantUmlArchCondition.Configuration.consideringOnlyDependenciesInAnyPackage; -import static com.tngtech.archunit.library.plantuml.PlantUmlArchCondition.Configuration.consideringOnlyDependenciesInDiagram; -import static com.tngtech.archunit.library.plantuml.PlantUmlArchCondition.adhereToPlantUmlDiagram; +import static com.tngtech.archunit.library.plantuml.rules.PlantUmlArchCondition.Configuration.consideringAllDependencies; +import static com.tngtech.archunit.library.plantuml.rules.PlantUmlArchCondition.Configuration.consideringOnlyDependenciesInAnyPackage; +import static com.tngtech.archunit.library.plantuml.rules.PlantUmlArchCondition.Configuration.consideringOnlyDependenciesInDiagram; +import static com.tngtech.archunit.library.plantuml.rules.PlantUmlArchCondition.adhereToPlantUmlDiagram; @ArchTag("example") -@AnalyzeClasses(packages = "com.tngtech.archunit.example.plantuml") +@AnalyzeClasses(packages = "com.tngtech.archunit.example.shopping") public class PlantUmlArchitectureTest { private static final URL plantUmlDiagram = PlantUmlArchitectureTest.class.getResource("shopping_example.puml"); diff --git a/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/SlicesIsolationTest.java b/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/SlicesIsolationTest.java index ba454d0036..44a27035b0 100644 --- a/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/SlicesIsolationTest.java +++ b/archunit-example/example-junit5/src/test/java/com/tngtech/archunit/exampletest/junit5/SlicesIsolationTest.java @@ -35,7 +35,7 @@ public class SlicesIsolationTest { .ignoreDependency(UseCaseOneTwoController.class, UseCaseTwoController.class) .ignoreDependency(nameMatching(".*controller\\.three.*"), alwaysTrue()); - private static DescribedPredicate containDescription(final String descriptionPart) { + private static DescribedPredicate containDescription(String descriptionPart) { return new DescribedPredicate("contain description '%s'", descriptionPart) { @Override public boolean test(Slice input) { diff --git a/archunit-example/example-plain/build.gradle b/archunit-example/example-plain/build.gradle index 1dffc9b90a..cec9903a34 100644 --- a/archunit-example/example-plain/build.gradle +++ b/archunit-example/example-plain/build.gradle @@ -13,9 +13,9 @@ dependencies { } test { - if (!project.hasProperty('example')) { - useJUnit { - excludeCategories 'com.tngtech.archunit.exampletest.Example' + useJUnitPlatform { + if (!project.hasProperty('example')) { + excludeTags('com.tngtech.archunit.exampletest.Example') } } } diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/AppModule.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/AppModule.java new file mode 100644 index 0000000000..289b2e2541 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/AppModule.java @@ -0,0 +1,9 @@ +package com.tngtech.archunit.example; + +public @interface AppModule { + String name(); + + String[] allowedDependencies() default {}; + + String[] exposedPackages() default {}; +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/ModuleApi.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/ModuleApi.java new file mode 100644 index 0000000000..cabcafc4aa --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/ModuleApi.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.example; + +public @interface ModuleApi { +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/ClassViolatingInjectionRules.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/ClassViolatingInjectionRules.java index 8f77353642..ac70bf7dcd 100644 --- a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/ClassViolatingInjectionRules.java +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/ClassViolatingInjectionRules.java @@ -5,8 +5,6 @@ import java.util.Map; import java.util.Set; -import javax.annotation.Resource; - import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; @@ -21,46 +19,58 @@ public class ClassViolatingInjectionRules { private Set badBecauseJavaxInjectField; @com.google.inject.Inject private Map badBecauseComGoogleInjectField; - @Resource - private File badBecauseResourceField; + @javax.annotation.Resource + private File badBecauseJavaxResourceField; + @jakarta.inject.Inject + private Object badBecauseJakartaInjectField; + @jakarta.annotation.Resource + private Object badBecauseJakartaResourceField; ClassViolatingInjectionRules(String okayBecauseNotInjected) { } @Autowired - ClassViolatingInjectionRules(Object badBecauseAutowiredField) { + ClassViolatingInjectionRules(Object okayBecauseAutowiredConstructor) { } - ClassViolatingInjectionRules(@Value("${name}") List badBecauseValueField) { + ClassViolatingInjectionRules(@Value("${name}") List okayBecauseValueConstructorParameter) { } @javax.inject.Inject - ClassViolatingInjectionRules(Set badBecauseJavaxInjectField) { + ClassViolatingInjectionRules(Set okayBecauseJavaxInjectConstructor) { } @com.google.inject.Inject - ClassViolatingInjectionRules(Map badBecauseComGoogleInjectField) { + ClassViolatingInjectionRules(Map okayBecauseComGoogleInjectConstructor) { } void someMethod(String okayBecauseNotInjected) { } @Autowired - void someMethod(Object badBecauseAutowiredField) { + void someMethod(Object okayBecauseAutowiredMethod) { } - void someMethod(@Value("${name}") List badBecauseValueField) { + void someMethod(@Value("${name}") List okayBecauseValueMethodParameter) { } @javax.inject.Inject - void someMethod(Set badBecauseJavaxInjectField) { + void someMethod(Set okayBecauseJavaxInjectMethod) { } @com.google.inject.Inject - void someMethod(Map badBecauseComGoogleInjectField) { + void someMethod(Map okayBecauseComGoogleInjectMethod) { + } + + @javax.annotation.Resource + void someMethod(File okayBecauseJavaxResourceMethod) { + } + + @jakarta.inject.Inject + void someMethod(Void okayBecauseJakartaInjectMethod) { } - @Resource - void someMethod(File badBecauseResourceField) { + @jakarta.annotation.Resource + void someMethod(Integer okayBecauseJavaxResourceMethod) { } } diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/persistence/first/dao/domain/PersistentObject.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/persistence/first/dao/domain/PersistentObject.java index 56dc774674..82d3500339 100644 --- a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/persistence/first/dao/domain/PersistentObject.java +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/persistence/first/dao/domain/PersistentObject.java @@ -31,7 +31,7 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } - final PersistentObject other = (PersistentObject) obj; + PersistentObject other = (PersistentObject) obj; return Objects.equals(this.id, other.id); } } diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/persistence/second/dao/domain/OtherPersistentObject.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/persistence/second/dao/domain/OtherPersistentObject.java index 6f989f1c00..b2680eb316 100644 --- a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/persistence/second/dao/domain/OtherPersistentObject.java +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/layers/persistence/second/dao/domain/OtherPersistentObject.java @@ -38,7 +38,7 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } - final OtherPersistentObject other = (OtherPersistentObject) obj; + OtherPersistentObject other = (OtherPersistentObject) obj; return Objects.equals(this.id, other.id); } } diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/address/Address.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/address/Address.java deleted file mode 100644 index 0ffe9c0c8b..0000000000 --- a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/address/Address.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.tngtech.archunit.example.plantuml.address; - -import com.tngtech.archunit.example.plantuml.catalog.ProductCatalog; - -public class Address { - private ProductCatalog productCatalog; -} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/customer/Customer.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/customer/Customer.java deleted file mode 100644 index d8524dfee8..0000000000 --- a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/customer/Customer.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.tngtech.archunit.example.plantuml.customer; - -import com.tngtech.archunit.example.plantuml.address.Address; -import com.tngtech.archunit.example.plantuml.order.Order; - -public class Customer { - private Address address; - - void addOrder(Order order) { - // simply having such a parameter violates the specified UML diagram - } - - public Address getAddress() { - return address; - } -} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/importer/ProductImport.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/importer/ProductImport.java deleted file mode 100644 index 5862864668..0000000000 --- a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/importer/ProductImport.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.tngtech.archunit.example.plantuml.importer; - -import com.tngtech.archunit.example.plantuml.catalog.ProductCatalog; -import com.tngtech.archunit.example.plantuml.customer.Customer; -import com.tngtech.archunit.example.plantuml.xml.processor.XmlProcessor; -import com.tngtech.archunit.example.plantuml.xml.types.XmlTypes; - -public class ProductImport { - public ProductCatalog productCatalog; - public XmlTypes xmlType; - public XmlProcessor xmlProcessor; - - public Customer getCustomer() { - return new Customer(); // violates diagram -> product import may not directly know Customer - } - - ProductCatalog parse(byte[] xml) { - return new ProductCatalog(); - } -} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/product/Product.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/product/Product.java deleted file mode 100644 index 14f1d4ca4b..0000000000 --- a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/product/Product.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.tngtech.archunit.example.plantuml.product; - -import com.tngtech.archunit.example.plantuml.customer.Customer; -import com.tngtech.archunit.example.plantuml.order.Order; - -public class Product { - public Customer customer; - - Order getOrder() { - return null; // the return type violates the specified UML diagram - } - - public void register() { - } - - public void report() { - } -} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/xml/processor/XmlProcessor.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/xml/processor/XmlProcessor.java deleted file mode 100644 index 57f2a1d50c..0000000000 --- a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/xml/processor/XmlProcessor.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.tngtech.archunit.example.plantuml.xml.processor; - -public class XmlProcessor { -} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/xml/types/XmlTypes.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/xml/types/XmlTypes.java deleted file mode 100644 index 1ba7ad07e3..0000000000 --- a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/xml/types/XmlTypes.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.tngtech.archunit.example.plantuml.xml.types; - -public class XmlTypes { -} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/xml/utils/XmlUtils.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/xml/utils/XmlUtils.java deleted file mode 100644 index 8674a9c93f..0000000000 --- a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/xml/utils/XmlUtils.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.tngtech.archunit.example.plantuml.xml.utils; - -public class XmlUtils { -} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/address/Address.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/address/Address.java new file mode 100644 index 0000000000..2f04a96ed8 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/address/Address.java @@ -0,0 +1,10 @@ +package com.tngtech.archunit.example.shopping.address; + +import com.tngtech.archunit.example.ModuleApi; +import com.tngtech.archunit.example.shopping.catalog.ProductCatalog; + +@ModuleApi +@SuppressWarnings("unused") +public class Address { + private ProductCatalog productCatalog; +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/address/AddressController.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/address/AddressController.java new file mode 100644 index 0000000000..c1460b32ad --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/address/AddressController.java @@ -0,0 +1,12 @@ +package com.tngtech.archunit.example.shopping.address; + +import com.tngtech.archunit.example.ModuleApi; +import com.tngtech.archunit.example.layers.AbstractController; + +@ModuleApi +@SuppressWarnings("unused") +public class AddressController extends AbstractController { + void handleAddress(Address address) { + // do something + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/address/package-info.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/address/package-info.java new file mode 100644 index 0000000000..33ebb21853 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/address/package-info.java @@ -0,0 +1,7 @@ +@AppModule( + name = "Address", + exposedPackages = "com.tngtech.archunit.example.shopping.address" +) +package com.tngtech.archunit.example.shopping.address; + +import com.tngtech.archunit.example.AppModule; diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/catalog/ProductCatalog.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/catalog/ProductCatalog.java similarity index 63% rename from archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/catalog/ProductCatalog.java rename to archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/catalog/ProductCatalog.java index 342e383f7b..d8c29f81cf 100644 --- a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/catalog/ProductCatalog.java +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/catalog/ProductCatalog.java @@ -1,9 +1,9 @@ -package com.tngtech.archunit.example.plantuml.catalog; +package com.tngtech.archunit.example.shopping.catalog; import java.util.Set; -import com.tngtech.archunit.example.plantuml.order.Order; -import com.tngtech.archunit.example.plantuml.product.Product; +import com.tngtech.archunit.example.shopping.order.Order; +import com.tngtech.archunit.example.shopping.product.Product; public class ProductCatalog { private Set allProducts; diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/catalog/package-info.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/catalog/package-info.java new file mode 100644 index 0000000000..f5074454ea --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/catalog/package-info.java @@ -0,0 +1,7 @@ +@AppModule( + name = "Catalog", + allowedDependencies = {"Product"} +) +package com.tngtech.archunit.example.shopping.catalog; + +import com.tngtech.archunit.example.AppModule; diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/customer/Customer.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/customer/Customer.java new file mode 100644 index 0000000000..82af19e656 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/customer/Customer.java @@ -0,0 +1,19 @@ +package com.tngtech.archunit.example.shopping.customer; + +import com.tngtech.archunit.example.ModuleApi; +import com.tngtech.archunit.example.shopping.address.Address; +import com.tngtech.archunit.example.shopping.order.Order; + +@ModuleApi +@SuppressWarnings("unused") +public class Customer { + private Address address; + + void addOrder(Order order) { + // simply having such a parameter violates the specified UML diagram + } + + public Address getAddress() { + return address; + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/customer/package-info.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/customer/package-info.java new file mode 100644 index 0000000000..29d7754335 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/customer/package-info.java @@ -0,0 +1,8 @@ +@AppModule( + name = "Customer", + allowedDependencies = {"Address"}, + exposedPackages = "com.tngtech.archunit.example.shopping.customer" +) +package com.tngtech.archunit.example.shopping.customer; + +import com.tngtech.archunit.example.AppModule; diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/importer/ProductImport.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/importer/ProductImport.java new file mode 100644 index 0000000000..8989528619 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/importer/ProductImport.java @@ -0,0 +1,23 @@ +package com.tngtech.archunit.example.shopping.importer; + +import com.tngtech.archunit.example.ModuleApi; +import com.tngtech.archunit.example.shopping.catalog.ProductCatalog; +import com.tngtech.archunit.example.shopping.customer.Customer; +import com.tngtech.archunit.example.shopping.xml.processor.XmlProcessor; +import com.tngtech.archunit.example.shopping.xml.types.XmlTypes; + +@ModuleApi +@SuppressWarnings("unused") +public class ProductImport { + public ProductCatalog productCatalog; + public XmlTypes xmlType; + public XmlProcessor xmlProcessor; + + public Customer getCustomer() { + return new Customer(); // violates diagram -> product import may not directly know Customer + } + + ProductCatalog parse(byte[] xml) { + return new ProductCatalog(); + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/importer/package-info.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/importer/package-info.java new file mode 100644 index 0000000000..457f44c654 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/importer/package-info.java @@ -0,0 +1,8 @@ +@AppModule( + name = "Importer", + allowedDependencies = {"Catalog", "XML"}, + exposedPackages = "com.tngtech.archunit.example.shopping.importer" +) +package com.tngtech.archunit.example.shopping.importer; + +import com.tngtech.archunit.example.AppModule; diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/order/Order.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/order/Order.java similarity index 55% rename from archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/order/Order.java rename to archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/order/Order.java index cf7b5f756d..2e1ddc13c4 100644 --- a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/plantuml/order/Order.java +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/order/Order.java @@ -1,11 +1,14 @@ -package com.tngtech.archunit.example.plantuml.order; +package com.tngtech.archunit.example.shopping.order; import java.util.Set; -import com.tngtech.archunit.example.plantuml.address.Address; -import com.tngtech.archunit.example.plantuml.customer.Customer; -import com.tngtech.archunit.example.plantuml.product.Product; +import com.tngtech.archunit.example.ModuleApi; +import com.tngtech.archunit.example.shopping.address.Address; +import com.tngtech.archunit.example.shopping.customer.Customer; +import com.tngtech.archunit.example.shopping.product.Product; +@ModuleApi +@SuppressWarnings("unused") public class Order { public Customer customer; private Set products; diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/order/package-info.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/order/package-info.java new file mode 100644 index 0000000000..350bd44e73 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/order/package-info.java @@ -0,0 +1,8 @@ +@AppModule( + name = "Order", + allowedDependencies = {"Customer", "Product"}, + exposedPackages = "com.tngtech.archunit.example.shopping.order" +) +package com.tngtech.archunit.example.shopping.order; + +import com.tngtech.archunit.example.AppModule; diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/product/Product.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/product/Product.java new file mode 100644 index 0000000000..709fb7a33f --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/product/Product.java @@ -0,0 +1,21 @@ +package com.tngtech.archunit.example.shopping.product; + +import com.tngtech.archunit.example.ModuleApi; +import com.tngtech.archunit.example.shopping.customer.Customer; +import com.tngtech.archunit.example.shopping.order.Order; + +@ModuleApi +@SuppressWarnings("unused") +public class Product { + public Customer customer; + + Order getOrder() { + return null; // the return type violates the specified UML diagram + } + + public void register() { + } + + public void report() { + } +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/product/package-info.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/product/package-info.java new file mode 100644 index 0000000000..1404352f4c --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/product/package-info.java @@ -0,0 +1,7 @@ +@AppModule( + name = "Product", + exposedPackages = "com.tngtech.archunit.example.shopping.product" +) +package com.tngtech.archunit.example.shopping.product; + +import com.tngtech.archunit.example.AppModule; diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/xml/package-info.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/xml/package-info.java new file mode 100644 index 0000000000..2bd971cf42 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/xml/package-info.java @@ -0,0 +1,7 @@ +@AppModule( + name = "XML", + exposedPackages = "com.tngtech.archunit.example.shopping.xml.processor" +) +package com.tngtech.archunit.example.shopping.xml; + +import com.tngtech.archunit.example.AppModule; diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/xml/processor/XmlProcessor.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/xml/processor/XmlProcessor.java new file mode 100644 index 0000000000..c1fdfef1e9 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/xml/processor/XmlProcessor.java @@ -0,0 +1,7 @@ +package com.tngtech.archunit.example.shopping.xml.processor; + +import com.tngtech.archunit.example.ModuleApi; + +@ModuleApi +public class XmlProcessor { +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/xml/types/XmlTypes.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/xml/types/XmlTypes.java new file mode 100644 index 0000000000..5569db3057 --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/xml/types/XmlTypes.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.example.shopping.xml.types; + +public class XmlTypes { +} diff --git a/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/xml/utils/XmlUtils.java b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/xml/utils/XmlUtils.java new file mode 100644 index 0000000000..20f9aa2cdd --- /dev/null +++ b/archunit-example/example-plain/src/main/java/com/tngtech/archunit/example/shopping/xml/utils/XmlUtils.java @@ -0,0 +1,4 @@ +package com.tngtech.archunit.example.shopping.xml.utils; + +public class XmlUtils { +} diff --git a/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/ModulesTest.java b/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/ModulesTest.java new file mode 100644 index 0000000000..b68c09c05a --- /dev/null +++ b/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/ModulesTest.java @@ -0,0 +1,204 @@ +package com.tngtech.archunit.exampletest; + +import java.util.List; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; + +import com.tngtech.archunit.base.DescribedFunction; +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaClasses; +import com.tngtech.archunit.core.domain.JavaPackage; +import com.tngtech.archunit.core.importer.ClassFileImporter; +import com.tngtech.archunit.example.AppModule; +import com.tngtech.archunit.example.ModuleApi; +import com.tngtech.archunit.library.modules.AnnotationDescriptor; +import com.tngtech.archunit.library.modules.ArchModule; +import com.tngtech.archunit.library.modules.ModuleDependency; +import com.tngtech.archunit.library.modules.syntax.DescriptorFunction; +import org.junit.Test; +import org.junit.experimental.categories.Category; + +import static com.tngtech.archunit.base.DescribedPredicate.alwaysTrue; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.belongToAnyOf; +import static com.tngtech.archunit.library.modules.syntax.AllowedModuleDependencies.allow; +import static com.tngtech.archunit.library.modules.syntax.ModuleDependencyScope.consideringOnlyDependenciesInAnyPackage; +import static com.tngtech.archunit.library.modules.syntax.ModuleRuleDefinition.modules; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toList; + +@Category(Example.class) +public class ModulesTest { + private final JavaClasses classes = new ClassFileImporter().importPackages("com.tngtech.archunit.example"); + + /** + * This example demonstrates how to derive modules from a package pattern. + * The `..` stands for arbitrary many packages and the `(*)` captures one specific subpackage name within the + * package tree. + */ + @Test + public void modules_should_respect_their_declared_dependencies__use_package_API() { + modules() + .definedByPackages("..shopping.(*)..") + .should().respectTheirAllowedDependencies( + allow() + .fromModule("catalog").toModules("product") + .fromModule("customer").toModules("address") + .fromModule("importer").toModules("catalog", "xml") + .fromModule("order").toModules("customer", "product"), + consideringOnlyDependenciesInAnyPackage("..example..")) + .ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class)) + .check(classes); + } + + /** + * This example demonstrates how to easily derive modules from classes annotated with a certain annotation, + * and also test for allowed dependencies and correct access to exposed packages by declared descriptor annotation properties. + * Within the example those are simply package-info files which denote the root of the modules by + * being annotated with @AppModule. + */ + @Test + public void modules_should_respect_their_declared_dependencies_and_exposed_packages() { + modules() + .definedByAnnotation(AppModule.class) + .should().respectTheirAllowedDependenciesDeclaredIn("allowedDependencies", + consideringOnlyDependenciesInAnyPackage("..example..")) + .ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class)) + .andShould().onlyDependOnEachOtherThroughPackagesDeclaredIn("exposedPackages") + .check(classes); + } + + /** + * This example demonstrates how to easily derive modules from classes annotated with a certain annotation, + * and also test for allowed dependencies using the descriptor annotation. + * Within the example those are simply package-info files which denote the root of the modules by + * being annotated with @AppModule. + */ + @Test + public void modules_should_respect_their_declared_dependencies__use_annotation_API() { + modules() + .definedByAnnotation(AppModule.class) + .should().respectTheirAllowedDependencies( + declaredByDescriptorAnnotation(), + consideringOnlyDependenciesInAnyPackage("..example..") + ) + .ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class)) + .check(classes); + } + + /** + * This example demonstrates how to use the slightly more generic root class API to define modules. + * While the result in this example is the same as the above, this API in general can be used to + * use arbitrary classes as roots of modules. + * For example if there is always a central interface denoted in some way, + * the modules could be derived from these interfaces. + */ + @Test + public void modules_should_respect_their_declared_dependencies__use_root_class_API() { + modules() + .definedByRootClasses( + DescribedPredicate.describe("annotated with @" + AppModule.class.getSimpleName(), (JavaClass rootClass) -> + rootClass.isAnnotatedWith(AppModule.class)) + ) + .derivingModuleFromRootClassBy( + DescribedFunction.describe("annotation @" + AppModule.class.getSimpleName(), (JavaClass rootClass) -> { + AppModule module = rootClass.getAnnotationOfType(AppModule.class); + return new AnnotationDescriptor<>(module.name(), module); + }) + ) + .should().respectTheirAllowedDependencies( + declaredByDescriptorAnnotation(), + consideringOnlyDependenciesInAnyPackage("..example..") + ) + .ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class)) + .check(classes); + } + + /** + * This example demonstrates how to use the generic API to define modules. + * The result in this example again is the same as the above, however in general the generic API + * allows to derive modules in a completely customizable way. + */ + @Test + public void modules_should_respect_their_declared_dependencies__use_generic_API() { + modules() + .definedBy(identifierFromModulesAnnotation()) + .derivingModule(fromModulesAnnotation()) + .should().respectTheirAllowedDependencies( + declaredByDescriptorAnnotation(), + consideringOnlyDependenciesInAnyPackage("..example..") + ) + .ignoreDependency(alwaysTrue(), belongToAnyOf(AppModule.class, ModuleApi.class)) + .check(classes); + } + + /** + * This example demonstrates how to check that modules only depend on each other through a specific API. + */ + @Test + public void modules_should_only_depend_on_each_other_through_module_API() { + modules() + .definedByAnnotation(AppModule.class) + .should().onlyDependOnEachOtherThroughClassesThat().areAnnotatedWith(ModuleApi.class) + .check(classes); + } + + /** + * This example demonstrates how to check for cyclic dependencies between modules. + */ + @Test + public void modules_should_be_free_of_cycles() { + modules() + .definedByAnnotation(AppModule.class) + .should().beFreeOfCycles() + .check(classes); + } + + private static DescribedPredicate>> declaredByDescriptorAnnotation() { + return DescribedPredicate.describe("declared by descriptor annotation", moduleDependency -> { + AppModule descriptor = moduleDependency.getOrigin().getDescriptor().getAnnotation(); + List allowedDependencies = stream(descriptor.allowedDependencies()).collect(toList()); + return allowedDependencies.contains(moduleDependency.getTarget().getName()); + }); + } + + private static IdentifierFromAnnotation identifierFromModulesAnnotation() { + return new IdentifierFromAnnotation(); + } + + private static DescriptorFunction> fromModulesAnnotation() { + return DescriptorFunction.describe(String.format("from @%s(name)", AppModule.class.getSimpleName()), + (ArchModule.Identifier identifier, Set containedClasses) -> { + JavaClass rootClass = containedClasses.stream().filter(it -> it.isAnnotatedWith(AppModule.class)).findFirst().get(); + AppModule module = rootClass.getAnnotationOfType(AppModule.class); + return new AnnotationDescriptor<>(module.name(), module); + }); + } + + private static class IdentifierFromAnnotation extends DescribedFunction { + IdentifierFromAnnotation() { + super("root classes with annotation @" + AppModule.class.getSimpleName()); + } + + @Override + public ArchModule.Identifier apply(JavaClass javaClass) { + return getIdentifierOfPackage(javaClass.getPackage()); + } + + private ArchModule.Identifier getIdentifierOfPackage(JavaPackage javaPackage) { + Optional identifierInCurrentPackage = javaPackage.getClasses().stream() + .filter(it -> it.isAnnotatedWith(AppModule.class)) + .findFirst() + .map(annotatedClassInPackage -> ArchModule.Identifier.from(annotatedClassInPackage.getAnnotationOfType(AppModule.class).name())); + + return identifierInCurrentPackage.orElseGet(identifierInParentPackageOf(javaPackage)); + } + + private Supplier identifierInParentPackageOf(JavaPackage javaPackage) { + return () -> javaPackage.getParent() + .map(this::getIdentifierOfPackage) + .orElseGet(ArchModule.Identifier::ignore); + } + } +} diff --git a/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/PlantUmlArchitectureTest.java b/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/PlantUmlArchitectureTest.java index 516e0084b7..ce70ceae1d 100644 --- a/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/PlantUmlArchitectureTest.java +++ b/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/PlantUmlArchitectureTest.java @@ -5,9 +5,9 @@ import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.domain.PackageMatchers; import com.tngtech.archunit.core.importer.ClassFileImporter; -import com.tngtech.archunit.example.plantuml.catalog.ProductCatalog; -import com.tngtech.archunit.example.plantuml.order.Order; -import com.tngtech.archunit.example.plantuml.product.Product; +import com.tngtech.archunit.example.shopping.catalog.ProductCatalog; +import com.tngtech.archunit.example.shopping.order.Order; +import com.tngtech.archunit.example.shopping.product.Product; import org.junit.Test; import org.junit.experimental.categories.Category; @@ -15,14 +15,14 @@ import static com.tngtech.archunit.core.domain.JavaClass.Functions.GET_PACKAGE_NAME; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.equivalentTo; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; -import static com.tngtech.archunit.library.plantuml.PlantUmlArchCondition.Configuration.consideringAllDependencies; -import static com.tngtech.archunit.library.plantuml.PlantUmlArchCondition.Configuration.consideringOnlyDependenciesInAnyPackage; -import static com.tngtech.archunit.library.plantuml.PlantUmlArchCondition.Configuration.consideringOnlyDependenciesInDiagram; -import static com.tngtech.archunit.library.plantuml.PlantUmlArchCondition.adhereToPlantUmlDiagram; +import static com.tngtech.archunit.library.plantuml.rules.PlantUmlArchCondition.Configuration.consideringAllDependencies; +import static com.tngtech.archunit.library.plantuml.rules.PlantUmlArchCondition.Configuration.consideringOnlyDependenciesInAnyPackage; +import static com.tngtech.archunit.library.plantuml.rules.PlantUmlArchCondition.Configuration.consideringOnlyDependenciesInDiagram; +import static com.tngtech.archunit.library.plantuml.rules.PlantUmlArchCondition.adhereToPlantUmlDiagram; @Category(Example.class) public class PlantUmlArchitectureTest { - private final JavaClasses classes = new ClassFileImporter().importPackages("com.tngtech.archunit.example.plantuml"); + private final JavaClasses classes = new ClassFileImporter().importPackages("com.tngtech.archunit.example.shopping"); private final URL plantUmlDiagram = PlantUmlArchitectureTest.class.getResource("shopping_example.puml"); @Test diff --git a/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/SecurityTest.java b/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/SecurityTest.java index c0a8197e42..23776064eb 100644 --- a/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/SecurityTest.java +++ b/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/SecurityTest.java @@ -2,7 +2,7 @@ import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.importer.ClassFileImporter; -import com.tngtech.archunit.core.importer.ImportOptions; +import com.tngtech.archunit.core.importer.ImportOption; import com.tngtech.archunit.lang.ArchRule; import org.junit.Test; import org.junit.experimental.categories.Category; @@ -27,15 +27,15 @@ public void only_security_infrastructure_should_use_java_security_on_whole_class ArchRule rule = classes().that().resideInAPackage("java.security.cert..") .should().onlyBeAccessed().byAnyPackage("..example.layers.security..", "java..", "..sun..", "javax..", "apple.security..", "org.jcp.."); - JavaClasses classes = new ClassFileImporter().importClasspath(onlyAppAndRuntime()); + JavaClasses classes = new ClassFileImporter().withImportOption(onlyAppAndRuntime()).importClasspath(); rule.check(classes); } - private ImportOptions onlyAppAndRuntime() { - return new ImportOptions().with(location -> + private ImportOption onlyAppAndRuntime() { + return location -> location.contains("archunit") || location.contains("/rt.jar") - || location.contains("java.base")); + || location.contains("java.base"); } } diff --git a/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/SlicesIsolationTest.java b/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/SlicesIsolationTest.java index 8b10044ae5..e2873ee47d 100644 --- a/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/SlicesIsolationTest.java +++ b/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/SlicesIsolationTest.java @@ -43,7 +43,7 @@ public void controllers_should_only_use_their_own_slice_with_custom_ignore() { .check(classes); } - private static DescribedPredicate containDescription(final String descriptionPart) { + private static DescribedPredicate containDescription(String descriptionPart) { return new DescribedPredicate("contain description '%s'", descriptionPart) { @Override public boolean test(Slice input) { diff --git a/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/extension/NewConfigurationEvent.java b/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/extension/NewConfigurationEvent.java index 25dcbe4128..7b511e2c2c 100644 --- a/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/extension/NewConfigurationEvent.java +++ b/archunit-example/example-plain/src/test/java/com/tngtech/archunit/exampletest/extension/NewConfigurationEvent.java @@ -35,7 +35,7 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } - final NewConfigurationEvent other = (NewConfigurationEvent) obj; + NewConfigurationEvent other = (NewConfigurationEvent) obj; return Objects.equals(this.properties, other.properties); } diff --git a/archunit-integration-test/build.gradle b/archunit-integration-test/build.gradle index bf89fe6eba..28032ef4a1 100644 --- a/archunit-integration-test/build.gradle +++ b/archunit-integration-test/build.gradle @@ -7,14 +7,8 @@ ext.moduleName = 'com.tngtech.archunit.integrationtest' ext.minimumJavaVersion = JavaVersion.VERSION_1_8 dependencies { - testImplementation dependency.junit5JupiterEngine testImplementation dependency.junitPlatform testImplementation dependency.assertj - testImplementation dependency.mockito - testImplementation dependency.guava - testImplementation dependency.log4j_api - testImplementation dependency.log4j_core - testImplementation dependency.log4j_slf4j testImplementation project(path: ':archunit', configuration: 'tests') testImplementation project(path: ':archunit-junit4') testImplementation project(path: ':archunit-junit5-api') @@ -26,7 +20,3 @@ dependencies { testRuntimeOnly project(path: ':archunit-junit5-engine') } - -test { - useJUnitPlatform() -} diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/ArchUnitArchitectureTest.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/ArchUnitArchitectureTest.java index 4b1234795f..4a4d5efd46 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/ArchUnitArchitectureTest.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/ArchUnitArchitectureTest.java @@ -21,7 +21,6 @@ import com.tngtech.archunit.junit.ArchTests; import com.tngtech.archunit.junit.ArchUnitRunner; import com.tngtech.archunit.lang.ArchRule; -import org.junit.runner.RunWith; import static com.tngtech.archunit.base.DescribedPredicate.not; import static com.tngtech.archunit.core.domain.JavaAccess.Predicates.origin; @@ -35,7 +34,6 @@ import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; import static com.tngtech.archunit.library.Architectures.layeredArchitecture; -@RunWith(ArchUnitRunner.class) @AnalyzeClasses( packagesOf = ArchUnitArchitectureTest.class, importOptions = ArchUnitArchitectureTest.ArchUnitProductionCode.class) @@ -83,11 +81,11 @@ private static DescribedPredicate> typeIsIllegallyResolvedViaReflect return classIsResolvedViaReflection().and(not(explicitlyAllowedUsage)); } - private static DescribedPredicate> contextIsAnnotatedWith(final Class annotationType) { + private static DescribedPredicate> contextIsAnnotatedWith(Class annotationType) { return origin(With.owner(withAnnotation(annotationType))); } - private static DescribedPredicate withAnnotation(final Class annotationType) { + private static DescribedPredicate withAnnotation(Class annotationType) { return new DescribedPredicate("annotated with @" + annotationType.getName()) { @Override public boolean test(JavaClass input) { diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/ArchUnitExampleArchitectureTest.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/ArchUnitExampleArchitectureTest.java index d3e558961e..9dc310372f 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/ArchUnitExampleArchitectureTest.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/ArchUnitExampleArchitectureTest.java @@ -5,13 +5,10 @@ import com.tngtech.archunit.core.importer.Location; import com.tngtech.archunit.junit.AnalyzeClasses; import com.tngtech.archunit.junit.ArchTest; -import com.tngtech.archunit.junit.ArchUnitRunner; import com.tngtech.archunit.lang.ArchRule; -import org.junit.runner.RunWith; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; -@RunWith(ArchUnitRunner.class) @AnalyzeClasses(packages = "com.tngtech.archunit", importOptions = ArchUnitExampleLocations.class) public class ArchUnitExampleArchitectureTest { @ArchTest diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/ImporterRules.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/ImporterRules.java index b26acd3cb0..50b9709b4b 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/ImporterRules.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/ImporterRules.java @@ -9,8 +9,10 @@ import com.tngtech.archunit.lang.ArchRule; import static com.tngtech.archunit.ArchUnitArchitectureTest.THIRDPARTY_PACKAGE_IDENTIFIER; +import static com.tngtech.archunit.base.DescribedPredicate.doNot; import static com.tngtech.archunit.base.DescribedPredicate.not; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.belongToAnyOf; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.resideOutsideOfPackage; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses; public class ImporterRules { @@ -18,7 +20,22 @@ public class ImporterRules { @ArchTest public static final ArchRule domain_does_not_access_importer = noClasses().that().resideInAPackage("..core.domain..") - .should().accessClassesThat(belong_to_the_import_context()); + .should().dependOnClassesThat(belong_to_the_import_context()); + + @ArchTest + public static final ArchRule asm_is_only_used_in_importer_or_JavaClassDescriptor = + noClasses() + .that( + resideOutsideOfPackage("..core.importer..") + .and(doNot(belongToAnyOf(JavaClassDescriptor.class + // Conceptually there are also dependencies from JavaModifier to ASM. + // However, at the moment all those are inlined by the compiler (primitives). + // Whenever we get the chance to break the public API + // we should remove the dependencies from JavaModifier to ASM. + // Those dependencies crept in by accident at some point because the design was convenient. + ))) + ) + .should().dependOnClassesThat().resideInAPackage("org.objectweb.."); @ArchTest public static final ArchRule ASM_type_is_only_accessed_within_JavaClassDescriptor_or_JavaClassDescriptorImporter = diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/PublicAPIRules.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/PublicAPIRules.java index ea5f75acbb..0e6793daca 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/PublicAPIRules.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/PublicAPIRules.java @@ -5,9 +5,11 @@ import java.lang.reflect.Type; import java.lang.reflect.WildcardType; import java.util.List; +import java.util.Set; import java.util.stream.Stream; import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.Dependency; import com.tngtech.archunit.core.domain.JavaAnnotation; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaCodeUnit; @@ -22,33 +24,38 @@ import com.tngtech.archunit.lang.CompositeArchRule; import com.tngtech.archunit.lang.ConditionEvents; import com.tngtech.archunit.lang.SimpleConditionEvent; -import com.tngtech.archunit.lang.syntax.ArchRuleDefinition; +import com.tngtech.archunit.lang.conditions.ArchPredicates; import static com.google.common.collect.Iterables.getLast; import static com.tngtech.archunit.ArchUnitArchitectureTest.THIRDPARTY_PACKAGE_IDENTIFIER; import static com.tngtech.archunit.PublicAPI.Usage.INHERITANCE; import static com.tngtech.archunit.base.DescribedPredicate.anyElementThat; import static com.tngtech.archunit.base.DescribedPredicate.doNot; +import static com.tngtech.archunit.base.DescribedPredicate.equalTo; import static com.tngtech.archunit.base.DescribedPredicate.not; import static com.tngtech.archunit.core.domain.Formatters.formatNamesOf; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.ANONYMOUS_CLASSES; import static com.tngtech.archunit.core.domain.JavaClass.Predicates.assignableTo; +import static com.tngtech.archunit.core.domain.JavaClass.Predicates.belongTo; 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.JavaModifier.SYNTHETIC; 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.core.domain.properties.HasName.Utils.namesOf; import static com.tngtech.archunit.lang.SimpleConditionEvent.violated; +import static com.tngtech.archunit.lang.conditions.ArchConditions.beAnnotatedWith; +import static com.tngtech.archunit.lang.conditions.ArchConditions.have; import static com.tngtech.archunit.lang.conditions.ArchPredicates.are; -import static com.tngtech.archunit.lang.conditions.ArchPredicates.have; import static com.tngtech.archunit.lang.conditions.ArchPredicates.is; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.codeUnits; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.members; import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toSet; import static org.assertj.core.util.Lists.newArrayList; public class PublicAPIRules { @@ -66,6 +73,14 @@ public class PublicAPIRules { .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 public_classes_of_public_API_should_be_annotated_with_PublicAPI = + classes() + .that(haveMemberThatBelongsToPublicApi()) + .should(beAnnotatedWith(PublicAPI.class).forSubtype() + .or(beAnnotatedWith(Internal.class)) + .or(have(supertype(annotatedWith(PublicAPI.class)).and(not(modifier(PUBLIC)))))); + @ArchTest public static final ArchRule only_members_that_are_public_API_or_explicitly_marked_as_internal_are_accessible = members() @@ -81,6 +96,7 @@ public class PublicAPIRules { public static final ArchRule all_public_API_members_are_accessible = members() .that().areAnnotatedWith(PublicAPI.class) + .and().doNotHaveModifier(SYNTHETIC) .should(bePubliclyAccessible()); @ArchTest @@ -104,18 +120,40 @@ public class PublicAPIRules { public static final ArchRule only_entry_point_and_syntax_interfaces_should_be_public = classes() .that().resideInAPackage("..syntax..") - .and().haveNameNotMatching(".*" + ArchRuleDefinition.class.getSimpleName() + ".*") + .and().haveNameNotMatching(".*RuleDefinition.*") .and().areNotInterfaces() .and().areNotAnnotatedWith(Internal.class) + .and(are(not(onlyUsedAsPublicApiParameter()))) .should().notBePublic() - .as(String.format( - "Only %s and interfaces within the ArchUnit syntax (..syntax..) should be public", - ArchRuleDefinition.class.getSimpleName())); + .as("Only RuleDefinitions and interfaces within the ArchUnit syntax (..syntax..) should be public"); + + private static DescribedPredicate onlyUsedAsPublicApiParameter() { + return DescribedPredicate.describe("only used as public API parameter", clazz -> { + Set relevantDependenciesFromPublicClasses = clazz.getDirectDependenciesToSelf().stream() + .filter(d -> d.getOriginClass().getModifiers().contains(PUBLIC)) + .filter(d -> + // this excludes fluent APIs where some public class returns a public nested class or similar + !belongTo(equalTo(d.getOriginClass())).test(d.getTargetClass()) + && !belongTo(equalTo(d.getTargetClass())).test(d.getOriginClass()) + && !d.getOriginClass().getEnclosingClass().equals(d.getTargetClass().getEnclosingClass()) + ) + .collect(toSet()); + long numberOfMethodParameterDependencies = relevantDependenciesFromPublicClasses.stream() + .map(Dependency::getOriginClass) + .distinct() + .flatMap(originClass -> originClass.getMethods().stream()) + .flatMap(method -> method.getRawParameterTypes().stream()) + .filter(parameterType -> parameterType.equals(clazz)) + .count(); + return relevantDependenciesFromPublicClasses.size() == numberOfMethodParameterDependencies; + }); + } @ArchTest public static final ArchRule parameters_of_public_API_are_public = codeUnits() .that().areDeclaredInClassesThat().arePublic() + .and().areDeclaredInClassesThat(not(enclosedInANonPublicClass())) .and().areDeclaredInClassesThat().areNotAnnotatedWith(Internal.class) .and().arePublic() .and().doNotHaveName("adhereToPlantUmlDiagram") @@ -127,7 +165,7 @@ public class PublicAPIRules { .that().haveRawParameterTypes(anyElementThat(is(assignableTo(DescribedPredicate.class)))) .and(are(declaredIn(modifier(PUBLIC)))) .and(are(not(declaredIn(annotatedWith(Internal.class))))) - .and(have(modifier(PUBLIC))) + .and(ArchPredicates.have(modifier(PUBLIC))) .should(haveContravariantPredicateParameterTypes()) .as(String.format( "Public API methods that take a %s should declare the type parameter contravariantly (i.e. %s)", @@ -201,21 +239,6 @@ private static DescribedPredicate declaredInClassIn(String packageId return declaredIn(resideInAPackage(packageIdentifier).as("class in '%s'", packageIdentifier)); } - private static ArchCondition notBePublic() { - return new ArchCondition("not be public") { - @Override - public void check(JavaMember member, ConditionEvents events) { - boolean satisfied = !member.getModifiers().contains(PUBLIC); - events.add(new SimpleConditionEvent(member, satisfied, - String.format("member %s.%s is %spublic in %s", - member.getOwner().getName(), - member.getName(), - satisfied ? "not " : "", - member.getSourceCodeLocation()))); - } - }; - } - private static ArchCondition bePubliclyAccessible() { return new ArchCondition("be publicly accessible") { @Override @@ -263,10 +286,17 @@ public boolean test(JavaClass input) { }; } + private static DescribedPredicate supertype(DescribedPredicate predicate) { + return DescribedPredicate.describe( + "supertype " + predicate.getDescription(), + javaClass -> Stream.concat(javaClass.getAllRawSuperclasses().stream(), javaClass.getAllRawInterfaces().stream()).anyMatch(predicate)); + } + private static DescribedPredicate withoutAPIMarking() { return not(annotatedWith(PublicAPI.class)).forSubtype() .and(not(annotatedWith(Internal.class)).forSubtype()) .and(declaredIn(modifier(PUBLIC))) + .and(not(declaredIn(enclosedInANonPublicClass()))) .as("without API marking"); } diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java index b1364a1d85..a224025da5 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExamplesIntegrationTest.java @@ -14,15 +14,17 @@ import java.util.UUID; import java.util.function.BiConsumer; import java.util.function.Consumer; +import java.util.function.Function; import java.util.stream.Stream; -import javax.annotation.Resource; import javax.persistence.EntityManager; import com.google.common.base.Joiner; import com.google.common.collect.ImmutableSet; import com.tngtech.archunit.ArchConfiguration; import com.tngtech.archunit.core.domain.JavaModifier; +import com.tngtech.archunit.example.AppModule; +import com.tngtech.archunit.example.ModuleApi; import com.tngtech.archunit.example.cycles.complexcycles.slice1.ClassBeingCalledInSliceOne; import com.tngtech.archunit.example.cycles.complexcycles.slice1.ClassOfMinimalCycleCallingSliceTwo; import com.tngtech.archunit.example.cycles.complexcycles.slice1.SliceOneCallingConstructorInSliceTwoAndMethodInSliceThree; @@ -132,12 +134,15 @@ import com.tngtech.archunit.example.onionarchitecture.domain.service.OrderQuantity; import com.tngtech.archunit.example.onionarchitecture.domain.service.ProductName; import com.tngtech.archunit.example.onionarchitecture.domain.service.ShoppingService; -import com.tngtech.archunit.example.plantuml.address.Address; -import com.tngtech.archunit.example.plantuml.catalog.ProductCatalog; -import com.tngtech.archunit.example.plantuml.customer.Customer; -import com.tngtech.archunit.example.plantuml.importer.ProductImport; -import com.tngtech.archunit.example.plantuml.order.Order; -import com.tngtech.archunit.example.plantuml.product.Product; +import com.tngtech.archunit.example.shopping.address.Address; +import com.tngtech.archunit.example.shopping.address.AddressController; +import com.tngtech.archunit.example.shopping.catalog.ProductCatalog; +import com.tngtech.archunit.example.shopping.customer.Customer; +import com.tngtech.archunit.example.shopping.importer.ProductImport; +import com.tngtech.archunit.example.shopping.order.Order; +import com.tngtech.archunit.example.shopping.product.Product; +import com.tngtech.archunit.example.shopping.xml.processor.XmlProcessor; +import com.tngtech.archunit.example.shopping.xml.types.XmlTypes; import com.tngtech.archunit.exampletest.ControllerRulesTest; import com.tngtech.archunit.exampletest.SecurityTest; import com.tngtech.archunit.testutil.TransientCopyRule; @@ -146,6 +151,7 @@ import com.tngtech.archunit.testutils.ExpectedConstructor; import com.tngtech.archunit.testutils.ExpectedField; import com.tngtech.archunit.testutils.ExpectedMethod; +import com.tngtech.archunit.testutils.ExpectedModuleDependency; import com.tngtech.archunit.testutils.ExpectedTestFailures; import com.tngtech.archunit.testutils.MessageAssertionChain; import com.tngtech.archunit.testutils.ResultStoringExtension; @@ -179,6 +185,7 @@ import static com.tngtech.archunit.testutils.ExpectedAccess.callFromMethod; import static com.tngtech.archunit.testutils.ExpectedAccess.callFromStaticInitializer; import static com.tngtech.archunit.testutils.ExpectedDependency.annotatedClass; +import static com.tngtech.archunit.testutils.ExpectedDependency.annotatedPackageInfo; import static com.tngtech.archunit.testutils.ExpectedDependency.annotatedParameter; import static com.tngtech.archunit.testutils.ExpectedDependency.constructor; import static com.tngtech.archunit.testutils.ExpectedDependency.field; @@ -191,6 +198,7 @@ import static com.tngtech.archunit.testutils.ExpectedDependency.method; import static com.tngtech.archunit.testutils.ExpectedDependency.typeParameter; import static com.tngtech.archunit.testutils.ExpectedLocation.javaClass; +import static com.tngtech.archunit.testutils.ExpectedMessage.violation; import static com.tngtech.archunit.testutils.ExpectedNaming.simpleNameOf; import static com.tngtech.archunit.testutils.ExpectedNaming.simpleNameOfAnonymousClassOf; import static com.tngtech.archunit.testutils.ExpectedViolation.clazz; @@ -247,8 +255,7 @@ Stream CodingRulesTest() { expectFailures.ofRule("no classes should use JodaTime, because modern Java projects use the [java.time] API instead") .by(callFromMethod(ClassViolatingCodingRules.class, "jodaTimeIsBad") .toMethod(org.joda.time.DateTime.class, "now") - .inLine(31) - .asDependency()) + .inLine(31)) .by(method(ClassViolatingCodingRules.class, "jodaTimeIsBad") .withReturnType(org.joda.time.DateTime.class)); @@ -258,7 +265,9 @@ Stream CodingRulesTest() { .by(ExpectedField.of(ClassViolatingInjectionRules.class, "badBecauseValueField").beingAnnotatedWith(Value.class)) .by(ExpectedField.of(ClassViolatingInjectionRules.class, "badBecauseJavaxInjectField").beingAnnotatedWith(javax.inject.Inject.class)) .by(ExpectedField.of(ClassViolatingInjectionRules.class, "badBecauseComGoogleInjectField").beingAnnotatedWith(com.google.inject.Inject.class)) - .by(ExpectedField.of(ClassViolatingInjectionRules.class, "badBecauseResourceField").beingAnnotatedWith(Resource.class)); + .by(ExpectedField.of(ClassViolatingInjectionRules.class, "badBecauseJavaxResourceField").beingAnnotatedWith(javax.annotation.Resource.class)) + .by(ExpectedField.of(ClassViolatingInjectionRules.class, "badBecauseJakartaInjectField").beingAnnotatedWith(jakarta.inject.Inject.class)) + .by(ExpectedField.of(ClassViolatingInjectionRules.class, "badBecauseJakartaResourceField").beingAnnotatedWith(jakarta.annotation.Resource.class)); expectFailures.ofRule("no classes should access standard streams and no classes should throw generic exceptions"); expectAccessToStandardStreams(expectFailures); @@ -627,13 +636,13 @@ Stream DependencyRulesTest() { .ofRule("no classes should depend on upper packages, because that might prevent packages on that level from being split into separate artifacts in a clean way") .by(inheritanceFrom(UseCaseTwoController.class).extending(AbstractController.class)) - .by(callFromConstructor(UseCaseTwoController.class).toConstructor(AbstractController.class).inLine(6).asDependency()) + .by(callFromConstructor(UseCaseTwoController.class).toConstructor(AbstractController.class).inLine(6)) .by(inheritanceFrom(InheritedControllerImpl.class).extending(AbstractController.class)) - .by(callFromConstructor(InheritedControllerImpl.class).toConstructor(AbstractController.class).inLine(5).asDependency()) + .by(callFromConstructor(InheritedControllerImpl.class).toConstructor(AbstractController.class).inLine(5)) .by(inheritanceFrom(InheritedControllerImpl.ChildControl.class).extending(AbstractController.AbstractInnerControl.class)) - .by(callFromConstructor(InheritedControllerImpl.ChildControl.class).toConstructor(AbstractController.AbstractInnerControl.class).inLine(6).asDependency()) - .by(callFromMethod(DaoCallingService.class, "violateLayerRulesTrickily").toConstructor(SomeMediator.class, ServiceViolatingLayerRules.class).inLine(18).asDependency()) - .by(callFromMethod(DaoCallingService.class, "violateLayerRulesTrickily").toMethod(SomeMediator.class, "violateLayerRulesIndirectly").inLine(18).asDependency()) + .by(callFromConstructor(InheritedControllerImpl.ChildControl.class).toConstructor(AbstractController.AbstractInnerControl.class).inLine(6)) + .by(callFromMethod(DaoCallingService.class, "violateLayerRulesTrickily").toConstructor(SomeMediator.class, ServiceViolatingLayerRules.class).inLine(18)) + .by(callFromMethod(DaoCallingService.class, "violateLayerRulesTrickily").toMethod(SomeMediator.class, "violateLayerRulesIndirectly").inLine(18)) .by(annotatedClass(WronglyAnnotated.class).annotatedWith(MyController.class)) .by(inheritanceFrom(VeryCentralCore.class).implementing(SomeOtherBusinessInterface.class)) .by(inheritanceFrom(SomeJpa.class).implementing(SomeDao.class)) @@ -659,16 +668,13 @@ Stream FrozenRulesTest() { .ofRule("no classes should depend on classes that reside in a package '..service..'") .by(callFromMethod(SomeController.class, "doSthController"). toMethod(ServiceViolatingDaoRules.class, "doSthService") - .inLine(11) - .asDependency()) + .inLine(11)) .by(callFromMethod(SomeController.class, "doSthWithSecuredService"). toMethod(ServiceViolatingLayerRules.class, "properlySecured") - .inLine(15) - .asDependency()) + .inLine(15)) .by(callFromMethod(OtherJpa.class, "testConnection") .toMethod(ProxiedConnection.class, "refresh") - .inLine(27) - .asDependency()) + .inLine(27)) .by(method(OtherJpa.class, "testConnection") .checkingInstanceOf(ProxiedConnection.class) .inLine(26)) @@ -676,12 +682,10 @@ Stream FrozenRulesTest() { .ofRule("no classes should depend on classes that are assignable to javax.persistence.EntityManager") .by(callFromMethod(ServiceViolatingDaoRules.class, "illegallyUseEntityManager"). toMethod(EntityManager.class, "persist", Object.class) - .inLine(26) - .asDependency()) + .inLine(26)) .by(callFromMethod(ServiceViolatingDaoRules.class, "illegallyUseEntityManager"). toMethod(ServiceViolatingDaoRules.MyEntityManager.class, "persist", Object.class) - .inLine(27) - .asDependency()) + .inLine(27)) .toDynamicTests(this::withTemporaryViolationStore); } @@ -786,13 +790,13 @@ Stream LayerDependencyRulesTest() { "should depend on classes that reside in a package '..controller..'") .by(callFromMethod(ServiceViolatingLayerRules.class, illegalAccessToController) .getting().field(UseCaseOneTwoController.class, someString) - .inLine(23).asDependency()) + .inLine(23)) .by(callFromMethod(ServiceViolatingLayerRules.class, illegalAccessToController) .toConstructor(UseCaseTwoController.class) - .inLine(24).asDependency()) + .inLine(24)) .by(callFromMethod(ServiceViolatingLayerRules.class, illegalAccessToController) .toMethod(UseCaseTwoController.class, doSomethingTwo) - .inLine(25).asDependency()) + .inLine(25)) .by(typeParameter(ServiceHelper.class, "TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeUtility.class)) .by(typeParameter(ServiceHelper.class, "ANOTHER_TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeEnum.class)) .by(method(ServiceHelper.class, "violatingLayerRuleByMethodTypeParameters") @@ -838,10 +842,10 @@ Stream LayerDependencyRulesTest() { "depend on classes that reside in a package '..service..'") .by(callFromMethod(DaoCallingService.class, violateLayerRules) .toMethod(ServiceViolatingLayerRules.class, ServiceViolatingLayerRules.doSomething) - .inLine(14).asDependency()) + .inLine(14)) .by(callFromMethod(OtherJpa.class, "testConnection") .toMethod(ProxiedConnection.class, "refresh") - .inLine(27).asDependency()) + .inLine(27)) .by(field(DaoCallingService.class, "service").ofType(ServiceViolatingLayerRules.class)) .by(inheritanceFrom(DaoCallingService.class).implementing(ServiceInterface.class)) .by(method(OtherJpa.class, "testConnection") @@ -852,13 +856,13 @@ Stream LayerDependencyRulesTest() { "only have dependent classes that reside in any package ['..controller..', '..service..']") .by(callFromMethod(DaoCallingService.class, violateLayerRules) .toMethod(ServiceViolatingLayerRules.class, ServiceViolatingLayerRules.doSomething) - .inLine(14).asDependency()) + .inLine(14)) .by(callFromMethod(SomeMediator.class, violateLayerRulesIndirectly) .toMethod(ServiceViolatingLayerRules.class, ServiceViolatingLayerRules.doSomething) - .inLine(15).asDependency()) + .inLine(15)) .by(callFromMethod(OtherJpa.class, "testConnection") .toMethod(ProxiedConnection.class, "refresh") - .inLine(27).asDependency()) + .inLine(27)) .by(inheritanceFrom(DaoCallingService.class).implementing(ServiceInterface.class)) .by(constructor(SomeMediator.class).withParameter(ServiceViolatingLayerRules.class)) .by(field(SomeMediator.class, "service").ofType(ServiceViolatingLayerRules.class)) @@ -871,13 +875,13 @@ Stream LayerDependencyRulesTest() { + "only depend on classes that reside in any package ['..service..', '..persistence..', 'java..', 'javax..']") .by(callFromMethod(ServiceViolatingLayerRules.class, illegalAccessToController) .getting().field(UseCaseOneTwoController.class, someString) - .inLine(23).asDependency()) + .inLine(23)) .by(callFromMethod(ServiceViolatingLayerRules.class, illegalAccessToController) .toConstructor(UseCaseTwoController.class) - .inLine(24).asDependency()) + .inLine(24)) .by(callFromMethod(ServiceViolatingLayerRules.class, illegalAccessToController) .toMethod(UseCaseTwoController.class, doSomethingTwo) - .inLine(25).asDependency()) + .inLine(25)) .by(typeParameter(ServiceHelper.class, "TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeUtility.class)) .by(typeParameter(ServiceHelper.class, "ANOTHER_TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeEnum.class)) .by(method(ServiceHelper.class, "violatingLayerRuleByMethodTypeParameters") @@ -951,27 +955,27 @@ Stream LayeredArchitectureTest() { .by(callFromMethod(DaoCallingService.class, "violateLayerRules") .toMethod(ServiceViolatingLayerRules.class, "doSomething") .inLine(14) - .asDependency()) + ) .by(callFromMethod(ServiceViolatingLayerRules.class, "illegalAccessToController") .toConstructor(UseCaseTwoController.class) .inLine(24) - .asDependency()) + ) .by(callFromMethod(ServiceViolatingLayerRules.class, "illegalAccessToController") .toMethod(UseCaseTwoController.class, "doSomethingTwo") .inLine(25) - .asDependency()) + ) .by(callFromMethod(ServiceViolatingLayerRules.class, "illegalAccessToController") .getting().field(UseCaseOneTwoController.class, "someString") .inLine(23) - .asDependency()) + ) .by(callFromMethod(OtherJpa.class, "testConnection") .toMethod(ProxiedConnection.class, "refresh") .inLine(27) - .asDependency()) + ) .by(typeParameter(ServiceHelper.class, "TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeUtility.class)) .by(typeParameter(ServiceHelper.class, "ANOTHER_TYPE_PARAMETER_VIOLATING_LAYER_RULE").dependingOn(SomeEnum.class)) @@ -1027,8 +1031,7 @@ Stream LayeredArchitectureTest() { expectedTestFailures .by(callFromMethod(SomeMediator.class, "violateLayerRulesIndirectly") .toMethod(ServiceViolatingLayerRules.class, "doSomething") - .inLine(15) - .asDependency()) + .inLine(15)) .by(constructor(SomeMediator.class).withParameter(ServiceViolatingLayerRules.class)) .by(field(SomeMediator.class, "service").ofType(ServiceViolatingLayerRules.class)); @@ -1067,24 +1070,24 @@ Stream OnionArchitectureTest() { .inLine(16)) .by(callFromMethod(AdministrationCLI.class, "handle", String[].class, AdministrationPort.class) .toMethod(ProductRepository.class, "getTotalCount") - .inLine(17).asDependency()) + .inLine(17)) .by(callFromMethod(ShoppingController.class, "addToShoppingCart", UUID.class, UUID.class, int.class) .toConstructor(ProductId.class, UUID.class) - .inLine(20).asDependency()) + .inLine(20)) .by(callFromMethod(ShoppingController.class, "addToShoppingCart", UUID.class, UUID.class, int.class) .toConstructor(ShoppingCartId.class, UUID.class) - .inLine(20).asDependency()) + .inLine(20)) .by(method(ShoppingService.class, "addToShoppingCart").withParameter(ProductId.class)) .by(method(ShoppingService.class, "addToShoppingCart").withParameter(ShoppingCartId.class)) .by(callFromMethod(ShoppingService.class, "addToShoppingCart", ShoppingCartId.class, ProductId.class, OrderQuantity.class) .toMethod(ShoppingCartRepository.class, "read", ShoppingCartId.class) - .inLine(21).asDependency()) + .inLine(21)) .by(callFromMethod(ShoppingService.class, "addToShoppingCart", ShoppingCartId.class, ProductId.class, OrderQuantity.class) .toMethod(ProductRepository.class, "read", ProductId.class) - .inLine(22).asDependency()) + .inLine(22)) .by(callFromMethod(ShoppingService.class, "addToShoppingCart", ShoppingCartId.class, ProductId.class, OrderQuantity.class) .toMethod(ShoppingCartRepository.class, "save", ShoppingCart.class) - .inLine(25).asDependency()); + .inLine(25)); ExpectedTestFailures expectedTestFailures = ExpectedTestFailures .forTests( @@ -1126,6 +1129,233 @@ Stream MethodsTest() { .toDynamicTests(); } + @TestFactory + Stream ModulesTest() { + ExpectedTestFailures expectedFailures = ExpectedTestFailures + .forTests( + com.tngtech.archunit.exampletest.ModulesTest.class, + com.tngtech.archunit.exampletest.junit4.ModulesTest.class, + com.tngtech.archunit.exampletest.junit5.ModulesTest.class); + + BiConsumer expectRespectTheirDeclaredDependenciesViolations = + (moduleNames, expected) -> expected + + .by(ExpectedModuleDependency.uncontained(callFromConstructor(AddressController.class).toConstructor(AbstractController.class).inLine(8))) + .by(ExpectedModuleDependency.uncontained(inheritanceFrom(AddressController.class).extending(AbstractController.class))) + + .by(ExpectedModuleDependency.fromModule(moduleNames.address()).toModule(moduleNames.catalog()) + .including(field(Address.class, "productCatalog").ofType(ProductCatalog.class))) + + .by(ExpectedModuleDependency.fromModule(moduleNames.product()).toModule(moduleNames.customer()) + .including(field(Product.class, "customer").ofType(Customer.class))) + + .by(ExpectedModuleDependency.fromModule(moduleNames.product()).toModule(moduleNames.order()) + .including(method(Product.class, "getOrder").withReturnType(Order.class))) + + .by(ExpectedModuleDependency.fromModule(moduleNames.customer()).toModule(moduleNames.order()) + .including(method(Customer.class, "addOrder").withParameter(Order.class))) + + .by(ExpectedModuleDependency.fromModule(moduleNames.catalog()).toModule(moduleNames.order()) + .including(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder") + .toConstructor(Order.class).inLine(12)) + .including(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder") + .toMethod(Order.class, "addProducts", Set.class).inLine(16))) + + .by(ExpectedModuleDependency.fromModule(moduleNames.importer()).toModule(moduleNames.customer()) + .including(callFromMethod(ProductImport.class, "getCustomer") + .toConstructor(Customer.class).inLine(17)) + .including(method(ProductImport.class, "getCustomer") + .withReturnType(Customer.class))) + + .by(ExpectedModuleDependency.fromModule(moduleNames.order()).toModule(moduleNames.address()) + .including(method(Order.class, "report") + .withParameter(Address.class))); + + expectedFailures = expectedFailures + .ofRule("modules defined by packages '..shopping.(*)..' should respect their allowed dependencies " + + "{ catalog -> [product], customer -> [address], importer -> [catalog, xml], order -> [customer, product] } " + + "considering only dependencies in any package ['..example..']"); + expectRespectTheirDeclaredDependenciesViolations.accept(ModuleNames.definedByPackages(), expectedFailures); + + Consumer expectDependOnEachOtherThroughViolations = + expected -> expected + .by(field(Address.class, "productCatalog").ofType(ProductCatalog.class)) + .by(field(ProductImport.class, "productCatalog").ofType(ProductCatalog.class)) + .by(field(ProductImport.class, "xmlType").ofType(XmlTypes.class)) + .by(callFromMethod(ProductImport.class, "parse", byte[].class).toConstructor(ProductCatalog.class).inLine(21)) + .by(method(ProductImport.class, "parse").withReturnType(ProductCatalog.class)); + + expectedFailures = expectedFailures + .ofRule(String.format("modules defined by annotation @%s should respect their allowed dependencies declared in 'allowedDependencies' " + + "considering only dependencies in any package ['..example..'] " + + "and should only depend on each other through packages declared in 'exposedPackages'", + AppModule.class.getSimpleName())); + expectRespectTheirDeclaredDependenciesViolations.accept(ModuleNames.definedByMetaInfo(), expectedFailures); + expectDependOnEachOtherThroughViolations.accept(expectedFailures); + + expectedFailures = expectedFailures + .ofRule(String.format("modules defined by annotation @%s should respect their allowed dependencies declared by descriptor annotation" + + " considering only dependencies in any package ['..example..']", + AppModule.class.getSimpleName())); + expectRespectTheirDeclaredDependenciesViolations.accept(ModuleNames.definedByMetaInfo(), expectedFailures); + + expectedFailures = expectedFailures + .ofRule(String.format("modules defined by root classes annotated with @%s ", AppModule.class.getSimpleName()) + + String.format("deriving module from root class by annotation @%s ", AppModule.class.getSimpleName()) + + "should respect their allowed dependencies declared by descriptor annotation considering only dependencies in any package ['..example..']"); + expectRespectTheirDeclaredDependenciesViolations.accept(ModuleNames.definedByMetaInfo(), expectedFailures); + + expectedFailures = expectedFailures + .ofRule(String.format("modules defined by root classes with annotation @%s ", AppModule.class.getSimpleName()) + + String.format("deriving module from @%s(name) ", AppModule.class.getSimpleName()) + + "should respect their allowed dependencies declared by descriptor annotation considering only dependencies in any package ['..example..']"); + expectRespectTheirDeclaredDependenciesViolations.accept(ModuleNames.definedByMetaInfo(), expectedFailures); + + expectedFailures + .ofRule("modules defined by annotation @AppModule should only depend on each other through classes that are annotated with @ModuleApi"); + expectDependOnEachOtherThroughViolations.accept(expectedFailures); + + expectedFailures.ofRule("modules defined by annotation @AppModule should be free of cycles") + .by(cycle() + .from("Address") + .by(field(Address.class, "productCatalog").ofType(ProductCatalog.class)) + .from("Catalog") + .by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder") + .toConstructor(Order.class) + .inLine(12)) + .by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder") + .toMethod(Order.class, "addProducts", Set.class) + .inLine(16)) + .from("Order") + .by(method(Order.class, "report").withParameter(Address.class))) + .by(cycle() + .from("Address") + .by(field(Address.class, "productCatalog").ofType(ProductCatalog.class)) + .from("Catalog") + .by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder") + .toConstructor(Order.class) + .inLine(12)) + .by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder") + .toMethod(Order.class, "addProducts", Set.class) + .inLine(16)) + .from("Order") + .by(field(Order.class, "customer").ofType(Customer.class)) + .by(callFromMethod(Order.class, "report") + .toMethod(Customer.class, "getAddress") + .inLine(21)) + .from("Customer") + .by(field(Customer.class, "address").ofType(Address.class)) + .by(method(Customer.class, "getAddress").withReturnType(Address.class))) + .by(cycle() + .from("Address") + .by(field(Address.class, "productCatalog").ofType(ProductCatalog.class)) + .from("Catalog") + .by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder") + .toConstructor(Order.class) + .inLine(12)) + .by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder") + .toMethod(Order.class, "addProducts", Set.class) + .inLine(16)) + .from("Order") + .by(genericFieldType(Order.class, "products").dependingOn(Product.class)) + .by(genericMethodParameterType(Order.class, "addProducts", Set.class).dependingOn(Product.class)) + .by(callFromMethod(Order.class, "report") + .toMethod(Product.class, "report") + .inLine(23)) + .from("Product") + .by(field(Product.class, "customer").ofType(Customer.class)) + .from("Customer") + .by(field(Customer.class, "address").ofType(Address.class)) + .by(method(Customer.class, "getAddress").withReturnType(Address.class))) + .by(cycle() + .from("Address") + .by(field(Address.class, "productCatalog").ofType(ProductCatalog.class)) + .from("Catalog") + .by(genericFieldType(ProductCatalog.class, "allProducts").dependingOn(Product.class)) + .by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder") + .toMethod(Product.class, "register") + .inLine(14)) + .from("Product") + .by(field(Product.class, "customer").ofType(Customer.class)) + .from("Customer") + .by(field(Customer.class, "address").ofType(Address.class)) + .by(method(Customer.class, "getAddress").withReturnType(Address.class))) + .by(cycle() + .from("Address") + .by(field(Address.class, "productCatalog").ofType(ProductCatalog.class)) + .from("Catalog") + .by(genericFieldType(ProductCatalog.class, "allProducts").dependingOn(Product.class)) + .by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder") + .toMethod(Product.class, "register") + .inLine(14)) + .from("Product") + .by(field(Product.class, "customer").ofType(Customer.class)) + .from("Customer") + .by(method(Customer.class, "addOrder").withParameter(Order.class)) + .from("Order") + .by(method(Order.class, "report").withParameter(Address.class))) + .by(cycle() + .from("Address") + .by(field(Address.class, "productCatalog").ofType(ProductCatalog.class)) + .from("Catalog") + .by(genericFieldType(ProductCatalog.class, "allProducts").dependingOn(Product.class)) + .by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder") + .toMethod(Product.class, "register") + .inLine(14)) + .from("Product") + .by(method(Product.class, "getOrder").withReturnType(Order.class)) + .from("Order") + .by(method(Order.class, "report").withParameter(Address.class))) + .by(cycle() + .from("Address") + .by(field(Address.class, "productCatalog").ofType(ProductCatalog.class)) + .from("Catalog") + .by(genericFieldType(ProductCatalog.class, "allProducts").dependingOn(Product.class)) + .by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder") + .toMethod(Product.class, "register") + .inLine(14)) + .from("Product") + .by(method(Product.class, "getOrder").withReturnType(Order.class)) + .from("Order") + .by(field(Order.class, "customer").ofType(Customer.class)) + .by(callFromMethod(Order.class, "report") + .toMethod(Customer.class, "getAddress") + .inLine(21)) + .from("Customer") + .by(field(Customer.class, "address").ofType(Address.class)) + .by(method(Customer.class, "getAddress").withReturnType(Address.class))) + .by(cycle() + .from("Customer") + .by(method(Customer.class, "addOrder").withParameter(Order.class)) + .from("Order") + .by(field(Order.class, "customer").ofType(Customer.class)) + .by(callFromMethod(Order.class, "report") + .toMethod(Customer.class, "getAddress") + .inLine(21))) + .by(cycle() + .from("Customer") + .by(method(Customer.class, "addOrder").withParameter(Order.class)) + .from("Order") + .by(genericFieldType(Order.class, "products").dependingOn(Product.class)) + .by(genericMethodParameterType(Order.class, "addProducts", Set.class).dependingOn(Product.class)) + .by(callFromMethod(Order.class, "report") + .toMethod(Product.class, "report") + .inLine(23)) + .from("Product") + .by(field(Product.class, "customer").ofType(Customer.class))) + .by(cycle() + .from("Order") + .by(genericFieldType(Order.class, "products").dependingOn(Product.class)) + .by(genericMethodParameterType(Order.class, "addProducts", Set.class).dependingOn(Product.class)) + .by(callFromMethod(Order.class, "report") + .toMethod(Product.class, "report") + .inLine(23)) + .from("Product") + .by(method(Product.class, "getOrder").withReturnType(Order.class))); + + return expectedFailures.toDynamicTests(); + } + @TestFactory Stream NamingConventionTest() { return ExpectedTestFailures @@ -1195,11 +1425,11 @@ Stream PlantUmlArchitectureTest() { .by(method(Customer.class, "addOrder") .withParameter(Order.class)) .by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder") - .toConstructor(Order.class).inLine(12).asDependency()) + .toConstructor(Order.class).inLine(12)) .by(callFromMethod(ProductCatalog.class, "gonnaDoSomethingIllegalWithOrder") - .toMethod(Order.class, "addProducts", Set.class).inLine(16).asDependency()) + .toMethod(Order.class, "addProducts", Set.class).inLine(16)) .by(callFromMethod(ProductImport.class, "getCustomer") - .toConstructor(Customer.class).inLine(14).asDependency()) + .toConstructor(Customer.class).inLine(17)) .by(method(ProductImport.class, "getCustomer") .withReturnType(Customer.class)) .by(method(Order.class, "report") @@ -1212,16 +1442,35 @@ Stream PlantUmlArchitectureTest() { ProductCatalog.class.getName(), Product.class.getName(), Order.class.getName())) .by(field(Address.class, "productCatalog") .ofType(ProductCatalog.class)) + .by(inheritanceFrom(AddressController.class) + .extending(AbstractController.class)) + .by(callFromConstructor(AddressController.class) + .toConstructor(AbstractController.class) + .inLine(8)) .by(field(Product.class, "customer") .ofType(Customer.class)) .by(method(Customer.class, "addOrder") .withParameter(Order.class)) .by(callFromMethod(ProductImport.class, "getCustomer") - .toConstructor(Customer.class).inLine(14).asDependency()) + .toConstructor(Customer.class).inLine(17)) .by(method(ProductImport.class, "getCustomer") .withReturnType(Customer.class)) .by(method(Order.class, "report") .withParameter(Address.class)) + .by(annotatedClass(Address.class).annotatedWith(ModuleApi.class)) + .by(annotatedClass(AddressController.class).annotatedWith(ModuleApi.class)) + .by(annotatedClass(Customer.class).annotatedWith(ModuleApi.class)) + .by(annotatedClass(ProductImport.class).annotatedWith(ModuleApi.class)) + .by(annotatedClass(Order.class).annotatedWith(ModuleApi.class)) + .by(annotatedClass(Product.class).annotatedWith(ModuleApi.class)) + .by(annotatedClass(XmlProcessor.class).annotatedWith(ModuleApi.class)) + .by(annotatedPackageInfo(Address.class.getPackage().getName()).annotatedWith(AppModule.class)) + .by(annotatedPackageInfo(ProductCatalog.class.getPackage().getName()).annotatedWith(AppModule.class)) + .by(annotatedPackageInfo(Customer.class.getPackage().getName()).annotatedWith(AppModule.class)) + .by(annotatedPackageInfo(ProductImport.class.getPackage().getName()).annotatedWith(AppModule.class)) + .by(annotatedPackageInfo(Order.class.getPackage().getName()).annotatedWith(AppModule.class)) + .by(annotatedPackageInfo(Product.class.getPackage().getName()).annotatedWith(AppModule.class)) + .by(violation("Class com.tngtech.archunit.example.shopping.xml.package-info is not contained in any component")) .toDynamicTests(); } @@ -1263,8 +1512,8 @@ Stream RestrictNumberOfClassesWithACertainPropertyTest() { } - private static MessageAssertionChain.Link classesContaining(final Class... classes) { - final String expectedLine = String.format("there is/are %d element(s) in %s", classes.length, formatNamesOf(classes)); + private static MessageAssertionChain.Link classesContaining(Class... classes) { + String expectedLine = String.format("there is/are %d element(s) in %s", classes.length, formatNamesOf(classes)); return new MessageAssertionChain.Link() { @Override public Result filterMatching(List lines) { @@ -1498,4 +1747,44 @@ Stream ThirdPartyRulesTest() { .toDynamicTests(); } + + private static class ModuleNames { + private final Function nameModification; + + private ModuleNames(Function nameModification) { + this.nameModification = nameModification; + } + + String address() { + return nameModification.apply("address"); + } + + String catalog() { + return nameModification.apply("catalog"); + } + + String customer() { + return nameModification.apply("customer"); + } + + String order() { + return nameModification.apply("order"); + } + + String product() { + return nameModification.apply("product"); + } + + String importer() { + return nameModification.apply("importer"); + } + + static ModuleNames definedByPackages() { + return new ModuleNames(Function.identity()); + } + + static ModuleNames definedByMetaInfo() { + return new ModuleNames(name -> name.substring(0, 1).toUpperCase() + name.substring(1)); + } + } } diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExpectedOnionArchitectureByAnnotationFailures.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExpectedOnionArchitectureByAnnotationFailures.java index 8099ce0eb9..f86a30ce0a 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExpectedOnionArchitectureByAnnotationFailures.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExpectedOnionArchitectureByAnnotationFailures.java @@ -53,24 +53,24 @@ static void addTo(ExpectedTestFailures expectedTestFailures) { .inLine(18)) .by(callFromMethod(AdministrationCLI.class, "handle", String[].class, AdministrationPort.class) .toMethod(ProductRepository.class, "getTotalCount") - .inLine(19).asDependency()) + .inLine(19)) .by(callFromMethod(ShoppingController.class, "addToShoppingCart", UUID.class, UUID.class, int.class) .toConstructor(ProductId.class, UUID.class) - .inLine(20).asDependency()) + .inLine(20)) .by(callFromMethod(ShoppingController.class, "addToShoppingCart", UUID.class, UUID.class, int.class) .toConstructor(ShoppingCartId.class, UUID.class) - .inLine(20).asDependency()) + .inLine(20)) .by(method(ShoppingService.class, "addToShoppingCart").withParameter(ProductId.class)) .by(method(ShoppingService.class, "addToShoppingCart").withParameter(ShoppingCartId.class)) .by(callFromMethod(ShoppingService.class, "addToShoppingCart", ShoppingCartId.class, ProductId.class, OrderQuantity.class) .toMethod(ShoppingCartRepository.class, "read", ShoppingCartId.class) - .inLine(21).asDependency()) + .inLine(21)) .by(callFromMethod(ShoppingService.class, "addToShoppingCart", ShoppingCartId.class, ProductId.class, OrderQuantity.class) .toMethod(ProductRepository.class, "read", ProductId.class) - .inLine(22).asDependency()) + .inLine(22)) .by(callFromMethod(ShoppingService.class, "addToShoppingCart", ShoppingCartId.class, ProductId.class, OrderQuantity.class) .toMethod(ShoppingCartRepository.class, "save", ShoppingCart.class) - .inLine(25).asDependency()) + .inLine(25)) .by(constructor(OrderItem.class).withParameter(OrderQuantity.class)) .by(field(OrderItem.class, "quantity").ofType(OrderQuantity.class)); } diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExtensionIntegrationTest.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExtensionIntegrationTest.java index 7721526c02..61b63341d1 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExtensionIntegrationTest.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/integration/ExtensionIntegrationTest.java @@ -65,7 +65,7 @@ void evaluation_results_are_only_dispatched_to_enabled_extensions() { assertThat(ExampleExtension.getEvaluatedRuleEvents()).isEmpty(); } - private static Condition containingEntry(final String propKey, final String propValue) { + private static Condition containingEntry(String propKey, String propValue) { return new Condition(String.format("containing entry {%s=%s}", propKey, propValue)) { @Override public boolean matches(Object value) { diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/CyclicErrorMatcher.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/CyclicErrorMatcher.java index c34595b3f2..5d1a167343 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/CyclicErrorMatcher.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/CyclicErrorMatcher.java @@ -40,14 +40,14 @@ private String detailText() { private List detailLines() { List result = new ArrayList<>(); for (Map.Entry> detail : details.asMap().entrySet()) { - result.add(dependenciesOfSliceHeaderPattern(detail.getKey())); + result.add(dependenciesOfComponentHeaderPattern(detail.getKey())); result.addAll(transform(detail.getValue(), r -> detailLinePattern(r.toString()))); } return result; } - public CyclicErrorMatcher from(String sliceName) { - cycleDescriptions.add(sliceName); + public CyclicErrorMatcher from(String componentName) { + cycleDescriptions.add(componentName); return this; } @@ -58,11 +58,11 @@ public CyclicErrorMatcher by(ExpectedRelation dependency) { @Override public MessageAssertionChain.Link.Result filterMatching(List lines) { - final Result.Builder builder = new Result.Builder() + Result.Builder builder = new Result.Builder() .containsText(cycleText()); - for (String sliceName : details.asMap().keySet()) { - builder.matchesLine(dependenciesOfSliceHeaderPattern(sliceName)); + for (String componentName : details.asMap().keySet()) { + builder.matchesLine(dependenciesOfComponentHeaderPattern(componentName)); } for (ExpectedRelation relation : details.values()) { @@ -82,14 +82,19 @@ public void associateIfStringIsContained(String string) { return builder.build(lines); } - private String dependenciesOfSliceHeaderPattern(String sliceName) { - return "\\s*\\d+. Dependencies of " + quote(sliceName); + private String dependenciesOfComponentHeaderPattern(String componentName) { + return "\\s*\\d+. Dependencies of " + quote(componentName); } private String detailLinePattern(String string) { return ".*" + quote(string) + ".*"; } + @Override + public void addTo(HandlingAssertion handlingAssertion) { + details.values().forEach(relation -> relation.addTo(handlingAssertion)); + } + @Override public String getDescription() { return String.format("Message contains cycle description '%s' and details '%s'", diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedAccess.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedAccess.java index ba7e57cd22..4be5e1719d 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedAccess.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedAccess.java @@ -82,7 +82,7 @@ public boolean correspondsTo(Object object) { public abstract ExpectedDependency asDependency(); public static class ExpectedAccessViolationCreationProcess { - private ExpectedOrigin origin; + private final ExpectedOrigin origin; private ExpectedAccessViolationCreationProcess(String memberDescription, Class clazz, String method, Class[] paramTypes) { origin = new ExpectedOrigin(memberDescription, clazz, method, paramTypes); @@ -162,6 +162,14 @@ public ExpectedDependency asDependency() { .toFieldDeclaredIn(getTarget().getDeclaringClass()) .inLineNumber(getLineNumber()); } + + @Override + public void addTo(HandlingAssertion assertion) { + assertion.byFieldAccess(this); + if (!getOrigin().getDeclaringClass().equals(getTarget().getDeclaringClass())) { + assertion.byDependency(asDependency()); + } + } } public static class ExpectedCall extends ExpectedAccess { @@ -179,5 +187,17 @@ public ExpectedDependency asDependency() { .toCodeUnitDeclaredIn(getTarget().getDeclaringClass()) .inLineNumber(getLineNumber()); } + + @Override + public void addTo(HandlingAssertion assertion) { + if (isToConstructor()) { + assertion.byConstructorCall(this); + } else { + assertion.byMethodCall(this); + } + if (!getOrigin().getDeclaringClass().equals(getTarget().getDeclaringClass())) { + assertion.byDependency(asDependency()); + } + } } } diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedDependency.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedDependency.java index 13172db16c..b7b36f505f 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedDependency.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedDependency.java @@ -19,7 +19,7 @@ public class ExpectedDependency implements ExpectedRelation { private final Class origin; private final Class target; - private String dependencyPattern; + private final String dependencyPattern; private ExpectedDependency(Class origin, Class target, String dependencyPattern) { this.origin = origin; @@ -80,6 +80,14 @@ public static GenericMemberTypeArgumentCreator genericMethodParameterType(Class< genericParameterTypes[0]); } + public static AnnotationDependencyCreator annotatedPackageInfo(String packageName) { + try { + return new AnnotationDependencyCreator(Class.forName(packageName + ".package-info")); + } catch (ClassNotFoundException e) { + throw new RuntimeException(e); + } + } + public static AnnotationDependencyCreator annotatedClass(Class clazz) { return new AnnotationDependencyCreator(clazz); } @@ -104,6 +112,11 @@ public static MemberDependencyCreator constructor(Class owner) { return new MemberDependencyCreator(owner, CONSTRUCTOR_NAME); } + @Override + public void addTo(HandlingAssertion assertion) { + assertion.byDependency(this); + } + @Override public void associateLines(LineAssociation association) { association.associateIfPatternMatches(dependencyPattern); diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedMessage.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedMessage.java index 1f9a6a2418..560b65a2fe 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedMessage.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedMessage.java @@ -4,7 +4,7 @@ import static java.util.stream.Collectors.toList; -class ExpectedMessage implements MessageAssertionChain.Link { +public class ExpectedMessage implements MessageAssertionChain.Link { private final String expectedMessage; ExpectedMessage(String expectedMessage) { @@ -22,4 +22,8 @@ public Result filterMatching(List lines) { public String getDescription() { return "Message: " + expectedMessage; } + + public static ExpectedMessage violation(String message) { + return new ExpectedMessage(message); + } } diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedModuleDependency.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedModuleDependency.java new file mode 100644 index 0000000000..c188b3ffc1 --- /dev/null +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedModuleDependency.java @@ -0,0 +1,99 @@ +package com.tngtech.archunit.testutils; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import com.google.common.base.Joiner; +import com.tngtech.archunit.testutils.ExpectedRelation.LineAssociation; + +import static java.util.regex.Pattern.quote; + +public class ExpectedModuleDependency implements MessageAssertionChain.Link { + private final String dependencyPattern; + private final Set details = new HashSet<>(); + + private ExpectedModuleDependency(String dependencyPattern) { + this.dependencyPattern = dependencyPattern; + } + + public static ModuleDependencyCreator fromModule(String moduleName) { + return new ModuleDependencyCreator(moduleName); + } + + public static MessageAssertionChain.Link uncontained(ExpectedRelation call) { + return new ExpectedUncontainedModuleDependency(call); + } + + @Override + public void addTo(HandlingAssertion handlingAssertion) { + details.forEach(it -> it.addTo(handlingAssertion)); + } + + @Override + public Result filterMatching(List lines) { + return new Result.Builder() + .matchesLine(dependencyPattern) + .contains(details) + .build(lines); + } + + @Override + public String getDescription() { + return String.format("Module Dependency :: matches %s :: matches each of [%s]", + dependencyPattern, Joiner.on(", ").join(details)); + } + + public ExpectedModuleDependency including(ExpectedRelation relation) { + details.add(relation); + return this; + } + + public static class ModuleDependencyCreator { + private final String originModuleName; + + private ModuleDependencyCreator(String originModuleName) { + this.originModuleName = originModuleName; + } + + public ExpectedModuleDependency toModule(String moduleName) { + String description = quote(String.format("[%s -> %s]", originModuleName, moduleName)); + return new ExpectedModuleDependency(String.format("Module Dependency %s.*", description)); + } + } + + private static class ExpectedUncontainedModuleDependency implements MessageAssertionChain.Link { + private final ExpectedRelation delegate; + + private ExpectedUncontainedModuleDependency(ExpectedRelation delegate) { + this.delegate = delegate; + } + + @Override + public void addTo(HandlingAssertion assertion) { + delegate.addTo(assertion); + } + + @Override + public Result filterMatching(List lines) { + Result.Builder builder = new Result.Builder(); + delegate.associateLines(new LineAssociation() { + @Override + public void associateIfPatternMatches(String pattern) { + builder.matchesLine("Dependency not contained in any module:" + pattern); + } + + @Override + public void associateIfStringIsContained(String string) { + associateIfPatternMatches(".*" + quote(string) + ".*"); + } + }); + return builder.build(lines); + } + + @Override + public String getDescription() { + return "Uncontained module dependency: " + delegate; + } + } +} diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedRelation.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedRelation.java index 614680c93c..a7902f2c33 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedRelation.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedRelation.java @@ -4,11 +4,14 @@ import com.tngtech.archunit.lang.ConditionEvent; public interface ExpectedRelation { + + void addTo(HandlingAssertion assertion); + void associateLines(LineAssociation association); /** * @return True, if this expected dependency refers to the supplied object - * (i.e. the object that was passed to the {@link ConditionEvent}, e.g. a {@link JavaAccess}) + * (i.e. the object that was passed to the {@link ConditionEvent}, e.g. a {@link JavaAccess}) */ boolean correspondsTo(Object object); diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedTestFailures.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedTestFailures.java index 6f337dc86b..b923e681ab 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedTestFailures.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/ExpectedTestFailures.java @@ -16,10 +16,14 @@ import com.google.common.collect.ImmutableList; import com.tngtech.archunit.lang.EvaluationResult; import org.junit.jupiter.api.DynamicTest; -import org.junit.platform.runner.JUnitPlatform; +import org.junit.platform.engine.TestExecutionResult; +import org.junit.platform.launcher.Launcher; +import org.junit.platform.launcher.LauncherDiscoveryRequest; +import org.junit.platform.launcher.TestExecutionListener; +import org.junit.platform.launcher.TestIdentifier; +import org.junit.platform.launcher.core.LauncherDiscoveryRequestBuilder; +import org.junit.platform.launcher.core.LauncherFactory; import org.junit.runner.JUnitCore; -import org.junit.runner.notification.Failure; -import org.junit.runner.notification.RunNotifier; import static com.google.common.base.Preconditions.checkArgument; import static java.lang.System.lineSeparator; @@ -29,6 +33,7 @@ import static java.util.stream.Collectors.toList; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.DynamicTest.dynamicTest; +import static org.junit.platform.engine.discovery.DiscoverySelectors.selectClass; public class ExpectedTestFailures { private final SortedSet> testClasses; @@ -149,7 +154,7 @@ private RunnableJUnit4Test(Class testClass) { @Override TestFailures run() { List result = new JUnitCore().run(testClass).getFailures().stream() - .map(failure -> new TestFailure(failure, failure.getException())) + .map(failure -> new TestFailure(failure.getDescription().getMethodName(), failure.getException())) .collect(toList()); return new TestFailures(result); } @@ -163,12 +168,23 @@ private RunnableJUnit5Test(Class testClass) { @Override TestFailures run() { List result = new ArrayList<>(); - new JUnitPlatform(testClass).run(new RunNotifier() { + + LauncherDiscoveryRequest request = LauncherDiscoveryRequestBuilder.request() + .selectors(selectClass(testClass)) + .build(); + Launcher launcher = LauncherFactory.create(); + launcher.registerTestExecutionListeners(new TestExecutionListener() { @Override - public void fireTestFailure(Failure failure) { - result.add(new TestFailure(failure, failure.getException())); + public void executionFinished(TestIdentifier testIdentifier, TestExecutionResult testExecutionResult) { + if (!testIdentifier.isContainer() && testExecutionResult.getStatus() == TestExecutionResult.Status.FAILED) { + testExecutionResult.getThrowable().ifPresent(throwable -> { + result.add(new TestFailure(testIdentifier.getDisplayName(), throwable)); + }); + } } }); + launcher.execute(request); + return new TestFailures(result); } } @@ -258,8 +274,8 @@ private static class TestFailure { final String memberName; final Throwable error; - private TestFailure(Failure junitFailure, Throwable error) { - this.memberName = junitFailure.getDescription().getMethodName(); + private TestFailure(String memberName, Throwable error) { + this.memberName = memberName; this.error = error; } @@ -341,21 +357,22 @@ boolean isAssignedTo(TestFailure failure) { void by(ExpectedAccess.ExpectedFieldAccess access) { expectedViolation.by(access); - handlingAssertion.by(access); + access.addTo(handlingAssertion); } void by(ExpectedAccess.ExpectedCall call) { expectedViolation.by(call); - handlingAssertion.by(call); + call.addTo(handlingAssertion); } void by(ExpectedDependency inheritance) { expectedViolation.by(inheritance); - handlingAssertion.by(inheritance); + inheritance.addTo(handlingAssertion); } void by(MessageAssertionChain.Link assertion) { expectedViolation.by(assertion); + assertion.addTo(handlingAssertion); } ExpectedViolationToAssign copy() { diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/HandlingAssertion.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/HandlingAssertion.java index 2341ce840b..a98fdd7cfe 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/HandlingAssertion.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/HandlingAssertion.java @@ -5,6 +5,7 @@ import java.util.Iterator; import java.util.Set; import java.util.TreeSet; +import java.util.function.BiPredicate; import com.google.common.base.Joiner; import com.google.common.collect.Sets; @@ -15,17 +16,14 @@ import com.tngtech.archunit.core.domain.JavaFieldAccess; import com.tngtech.archunit.core.domain.JavaMethodCall; import com.tngtech.archunit.lang.EvaluationResult; -import com.tngtech.archunit.testutils.ExpectedAccess.ExpectedCall; -import com.tngtech.archunit.testutils.ExpectedAccess.ExpectedFieldAccess; -import static com.google.common.collect.Iterables.getOnlyElement; import static com.google.common.collect.Sets.union; import static java.lang.System.lineSeparator; import static java.util.Collections.emptySet; import static java.util.Collections.singleton; import static java.util.stream.Collectors.toSet; -class HandlingAssertion { +public class HandlingAssertion { private final Set expectedFieldAccesses; private final Set expectedMethodCalls; private final Set expectedConstructorCalls; @@ -47,20 +45,20 @@ private HandlingAssertion( this.expectedDependencies = expectedDependencies; } - void by(ExpectedFieldAccess access) { + void byFieldAccess(ExpectedRelation access) { expectedFieldAccesses.add(access); } - void by(ExpectedCall call) { - if (call.isToConstructor()) { - expectedConstructorCalls.add(call); - } else { - expectedMethodCalls.add(call); - } + void byConstructorCall(ExpectedRelation call) { + expectedConstructorCalls.add(call); + } + + void byMethodCall(ExpectedRelation call) { + expectedMethodCalls.add(call); } - void by(ExpectedDependency inheritance) { - expectedDependencies.add(inheritance); + void byDependency(ExpectedRelation dependency) { + expectedDependencies.add(dependency); } static HandlingAssertion ofRule() { @@ -80,7 +78,7 @@ Result evaluate(EvaluationResult evaluationResult) { private Set evaluateFieldAccesses(EvaluationResult result) { Set errorMessages = new HashSet<>(); - final Set left = new HashSet<>(this.expectedFieldAccesses); + Set left = new HashSet<>(this.expectedFieldAccesses); result.handleViolations((Collection violatingObjects, String message) -> errorMessages.addAll(removeExpectedAccesses(violatingObjects, left))); return union(errorMessages, errorMessagesFrom(left)); @@ -88,7 +86,7 @@ private Set evaluateFieldAccesses(EvaluationResult result) { private Set evaluateMethodCalls(EvaluationResult result) { Set errorMessages = new HashSet<>(); - final Set left = new HashSet<>(expectedMethodCalls); + Set left = new HashSet<>(expectedMethodCalls); result.handleViolations((Collection violatingObjects, String message) -> errorMessages.addAll(removeExpectedAccesses(violatingObjects, left))); return union(errorMessages, errorMessagesFrom(left)); @@ -96,7 +94,7 @@ private Set evaluateMethodCalls(EvaluationResult result) { private Set evaluateConstructorCalls(EvaluationResult result) { Set errorMessages = new HashSet<>(); - final Set left = new HashSet<>(expectedConstructorCalls); + Set left = new HashSet<>(expectedConstructorCalls); result.handleViolations((Collection violatingObjects, String message) -> errorMessages.addAll(removeExpectedAccesses(violatingObjects, left))); return union(errorMessages, errorMessagesFrom(left)); @@ -104,7 +102,7 @@ private Set evaluateConstructorCalls(EvaluationResult result) { private Set evaluateCalls(EvaluationResult result) { Set errorMessages = new HashSet<>(); - final Set left = new HashSet<>(Sets.union(expectedConstructorCalls, expectedMethodCalls)); + Set left = new HashSet<>(Sets.union(expectedConstructorCalls, expectedMethodCalls)); result.handleViolations((Collection> violatingObjects, String message) -> errorMessages.addAll(removeExpectedAccesses(violatingObjects, left))); return union(errorMessages, errorMessagesFrom(left)); @@ -112,7 +110,7 @@ private Set evaluateCalls(EvaluationResult result) { private Set evaluateAccesses(EvaluationResult result) { Set errorMessages = new HashSet<>(); - final Set left = new HashSet() { + Set left = new HashSet() { { addAll(expectedConstructorCalls); addAll(expectedMethodCalls); @@ -126,22 +124,20 @@ private Set evaluateAccesses(EvaluationResult result) { private Set evaluateDependencies(EvaluationResult result) { Set errorMessages = new HashSet<>(); - final Set left = new HashSet<>(expectedDependencies); + Set left = new HashSet<>(expectedDependencies); result.handleViolations((Collection violatingObjects, String message) -> errorMessages.addAll(removeExpectedAccesses(violatingObjects, left))); return union(errorMessages, errorMessagesFrom(left)); } private Set removeExpectedAccesses(Collection violatingObjects, Set left) { - Object violatingObject = getOnlyElement(violatingObjects); - for (Iterator actualMethodCalls = left.iterator(); actualMethodCalls.hasNext(); ) { - ExpectedRelation next = actualMethodCalls.next(); - if (next.correspondsTo(violatingObject)) { - actualMethodCalls.remove(); - return emptySet(); - } + removeMatchingElements(violatingObjects, left, (object, relation) -> relation.correspondsTo(object)); + + if (!violatingObjects.isEmpty()) { + return singleton("Unhandled violations: " + violatingObjects); } - return singleton("Unexpected violation handling: " + violatingObject); + + return emptySet(); } private Set errorMessagesFrom(Set set) { @@ -152,6 +148,19 @@ HandlingAssertion copy() { return new HandlingAssertion(expectedFieldAccesses, expectedMethodCalls, expectedConstructorCalls, expectedDependencies); } + private void removeMatchingElements(Collection actual, Collection expected, BiPredicate matches) { + for (Iterator actualIterator = actual.iterator(); actualIterator.hasNext(); ) { + T actualElement = actualIterator.next(); + for (Iterator expectedIterator = expected.iterator(); expectedIterator.hasNext(); ) { + if (matches.test(actualElement, expectedIterator.next())) { + actualIterator.remove(); + expectedIterator.remove(); + break; + } + } + } + } + static class Result { private final Set errorMessages = new TreeSet<>(); diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/MessageAssertionChain.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/MessageAssertionChain.java index cdde0a4db2..ad1a7167a6 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/MessageAssertionChain.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/MessageAssertionChain.java @@ -11,10 +11,10 @@ import com.google.common.collect.ImmutableList; import com.google.common.collect.Iterables; import com.tngtech.archunit.Internal; +import com.tngtech.archunit.testutils.ExpectedRelation.LineAssociation; import static com.google.common.base.Preconditions.checkArgument; import static java.lang.System.lineSeparator; -import static java.util.Collections.singletonList; import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; @@ -38,8 +38,8 @@ public String toString() { return links.stream().map(Link::getDescription).collect(joining(lineSeparator())); } - static Link matchesLine(final String pattern) { - final Pattern p = Pattern.compile(pattern); + static Link matchesLine(String pattern) { + Pattern p = Pattern.compile(pattern); return new Link() { @Override public Result filterMatching(List lines) { @@ -61,7 +61,7 @@ public String getDescription() { } static Link containsLine(String text, Object... args) { - final String expectedLine = String.format(text, args); + String expectedLine = String.format(text, args); return new Link() { @Override public Result filterMatching(List lines) { @@ -79,7 +79,7 @@ public String getDescription() { static Link containsText(String text, Object... args) { String expectedText = String.format(text, args); - final List expectedLines = Splitter.on(lineSeparator()).splitToList(expectedText); + List expectedLines = Splitter.on(lineSeparator()).splitToList(expectedText); return new Link() { @Override public Result filterMatching(List lines) { @@ -115,10 +115,10 @@ private static String describeLines(List lines) { return "Lines were >>>>>>>>" + lineSeparator() + Joiner.on(lineSeparator()).join(lines) + lineSeparator() + "<<<<<<<<"; } - static Link containsConsecutiveLines(final List expectedLines) { + static Link containsConsecutiveLines(List expectedLines) { checkArgument(!expectedLines.isEmpty(), "Asserting zero consecutive lines makes no sense"); - final String linesDescription = Joiner.on(lineSeparator()).join(expectedLines); - final String description = "Message contains consecutive lines " + lineSeparator() + linesDescription; + String linesDescription = Joiner.on(lineSeparator()).join(expectedLines); + String description = "Message contains consecutive lines " + lineSeparator() + linesDescription; return new Link() { @Override @@ -189,6 +189,9 @@ public interface Link { String getDescription(); + default void addTo(HandlingAssertion handlingAssertion) { + } + @Internal class Result { private final boolean matches; @@ -217,16 +220,6 @@ public Result(boolean matches, List remainingLines, String mismatchDescr this.mismatchDescription = mismatchDescription; } - static List difference(List list, String toSubtract) { - return difference(list, singletonList(toSubtract)); - } - - static List difference(List list, List toSubtract) { - List result = new ArrayList<>(list); - result.removeAll(toSubtract); - return result; - } - Optional getMismatchDescription() { return Optional.ofNullable(mismatchDescription); } @@ -250,6 +243,23 @@ Builder matchesLine(String pattern) { return this; } + Builder contains(Iterable relations) { + for (ExpectedRelation relation : relations) { + relation.associateLines(new LineAssociation() { + @Override + public void associateIfPatternMatches(String pattern) { + matchesLine(pattern); + } + + @Override + public void associateIfStringIsContained(String string) { + containsLine(string); + } + }); + } + return this; + } + Result build(List lines) { boolean matches = true; List remainingLines = new ArrayList<>(lines); diff --git a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/SliceDependencyErrorMatcher.java b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/SliceDependencyErrorMatcher.java index 4e7966f20d..6617a4797c 100644 --- a/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/SliceDependencyErrorMatcher.java +++ b/archunit-integration-test/src/test/java/com/tngtech/archunit/testutils/SliceDependencyErrorMatcher.java @@ -49,6 +49,11 @@ public Result filterMatching(List lines) { return new Result(true, remainingLines); } + @Override + public void addTo(HandlingAssertion handlingAssertion) { + expectedAccesses.forEach(it -> it.addTo(handlingAssertion)); + } + @Override public String getDescription() { return Joiner.on(System.lineSeparator()).join(ImmutableList.builder() diff --git a/archunit-java-modules-test/build.gradle b/archunit-java-modules-test/build.gradle index e17403da88..eea3c8ec2d 100644 --- a/archunit-java-modules-test/build.gradle +++ b/archunit-java-modules-test/build.gradle @@ -1,6 +1,6 @@ plugins { id 'archunit.java-conventions' - id 'org.javamodularity.moduleplugin' version '1.8.11' + id 'org.javamodularity.moduleplugin' version '1.8.15' } ext.moduleName = 'com.tngtech.archunit.javamodulestest' @@ -9,16 +9,6 @@ ext.minimumJavaVersion = JavaVersion.VERSION_1_9 dependencies { testImplementation project(path: ':archunit', configuration: 'shadow') - testImplementation dependency.log4j_slf4j - - testImplementation dependency.junit5JupiterApi - - testRuntimeOnly dependency.junitPlatform - testRuntimeOnly dependency.junit5JupiterEngine -} - -test { - useJUnitPlatform() } def addArchUnitModuleOptions = { diff --git a/archunit-junit/build.gradle b/archunit-junit/build.gradle index 663fad3ff1..a449537e66 100644 --- a/archunit-junit/build.gradle +++ b/archunit-junit/build.gradle @@ -38,9 +38,6 @@ dependencies { dependency.addGuava { dependencyNotation, config -> implementation(dependencyNotation, config) } implementation dependency.slf4j - testImplementation dependency.log4j_api - testImplementation dependency.log4j_core - testImplementation dependency.log4j_slf4j testImplementation dependency.junit4 testImplementation dependency.junit_dataprovider testImplementation dependency.mockito diff --git a/archunit-junit/junit4/build.gradle b/archunit-junit/junit4/build.gradle index 894575fac4..b84799d800 100644 --- a/archunit-junit/junit4/build.gradle +++ b/archunit-junit/junit4/build.gradle @@ -12,9 +12,6 @@ dependencies { dependency.addGuava { dependencyNotation, config -> implementation(dependencyNotation, config) } implementation dependency.slf4j - testImplementation dependency.log4j_api - testImplementation dependency.log4j_core - testImplementation dependency.log4j_slf4j testImplementation dependency.junit4 testImplementation dependency.junit_dataprovider testImplementation dependency.mockito @@ -40,10 +37,6 @@ sourcesJar { } } -archUnitTest { - hasSlowTests = true -} - shadowJar { exclude 'META-INF/**' diff --git a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java index c90d7d4e82..a93b15be9f 100644 --- a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java +++ b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -23,7 +23,6 @@ import com.tngtech.archunit.core.importer.ImportOption; import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludeJars; import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludeTests; -import com.tngtech.archunit.core.importer.ImportOptions; import static java.lang.annotation.ElementType.TYPE; import static java.lang.annotation.RetentionPolicy.RUNTIME; @@ -70,7 +69,7 @@ /** * Allows to filter the class import. The supplied types will be instantiated and used to create the - * {@link ImportOptions} passed to the {@link ClassFileImporter}. Considering caching, compare the notes on + * {@link ImportOption ImportOptions} passed to the {@link ClassFileImporter}. Considering caching, compare the notes on * {@link ImportOption}. * * @return The types of {@link ImportOption} to use for the import diff --git a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/ArchTest.java b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/ArchTest.java index fe98e0091b..70f5890370 100644 --- a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/ArchTest.java +++ b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/ArchTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/ArchUnitRunner.java b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/ArchUnitRunner.java index e0fadb980e..f572e40089 100644 --- a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/ArchUnitRunner.java +++ b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/ArchUnitRunner.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchRuleDeclaration.java b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchRuleDeclaration.java index 995e10cd08..a0c06a38d2 100644 --- a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchRuleDeclaration.java +++ b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchRuleDeclaration.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -18,10 +18,11 @@ import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; -import java.lang.reflect.Member; import java.lang.reflect.Method; +import java.util.List; import java.util.Set; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; import com.tngtech.archunit.junit.ArchIgnore; import com.tngtech.archunit.junit.ArchTests; @@ -34,13 +35,13 @@ import static java.util.Collections.singleton; abstract class ArchRuleDeclaration { - private final Class testClass; + final List> testClassPath; final T declaration; final Class owner; private final boolean forceIgnore; - ArchRuleDeclaration(Class testClass, T declaration, Class owner, boolean forceIgnore) { - this.testClass = testClass; + ArchRuleDeclaration(List> testClassPath, T declaration, Class owner, boolean forceIgnore) { + this.testClassPath = testClassPath; this.declaration = declaration; this.owner = owner; this.forceIgnore = forceIgnore; @@ -48,79 +49,76 @@ abstract class ArchRuleDeclaration { abstract void handleWith(Handler handler); - private static ArchRuleDeclaration from(Class testClass, Method method, Class methodOwner, boolean forceIgnore) { - return new AsMethod(testClass, method, methodOwner, forceIgnore); + private static ArchRuleDeclaration from(List> testClassPath, Method method, Class methodOwner, boolean forceIgnore) { + return new AsMethod(testClassPath, method, methodOwner, forceIgnore); } - private static ArchRuleDeclaration from(Class testClass, Field field, Class fieldOwner, boolean forceIgnore) { - return new AsField(testClass, field, fieldOwner, forceIgnore); + private static ArchRuleDeclaration from(List> testClassPath, Field field, Class fieldOwner, boolean forceIgnore) { + return new AsField(testClassPath, field, fieldOwner, forceIgnore); } - static boolean elementShouldBeIgnored(T member) { - return elementShouldBeIgnored(member.getDeclaringClass(), member); - } - - static boolean elementShouldBeIgnored(Class testClass, AnnotatedElement ruleDeclaration) { - return testClass.getAnnotation(ArchIgnore.class) != null || + static boolean elementShouldBeIgnored(Class owner, AnnotatedElement ruleDeclaration) { + return owner.getAnnotation(ArchIgnore.class) != null || ruleDeclaration.getAnnotation(ArchIgnore.class) != null; } boolean shouldBeIgnored() { - return forceIgnore || elementShouldBeIgnored(testClass, declaration); + return forceIgnore || elementShouldBeIgnored(owner, declaration); } static Set> toDeclarations( - ArchTests archTests, Class testClass, Class archTestAnnotationType, boolean forceIgnore) { + ArchTests archTests, List> testClassPath, Class archTestAnnotationType, boolean forceIgnore) { ImmutableSet.Builder> result = ImmutableSet.builder(); Class definitionLocation = archTests.getDefinitionLocation(); + List> childTestClassPath = ImmutableList.>builder().addAll(testClassPath).add(definitionLocation).build(); for (Field field : getAllFields(definitionLocation, withAnnotation(archTestAnnotationType))) { - result.addAll(archRuleDeclarationsFrom(testClass, field, definitionLocation, archTestAnnotationType, forceIgnore)); + result.addAll(archRuleDeclarationsFrom(childTestClassPath, field, definitionLocation, archTestAnnotationType, forceIgnore)); } for (Method method : getAllMethods(definitionLocation, withAnnotation(archTestAnnotationType))) { - result.add(ArchRuleDeclaration.from(testClass, method, definitionLocation, forceIgnore)); + result.add(ArchRuleDeclaration.from(childTestClassPath, method, definitionLocation, forceIgnore)); } return result.build(); } - private static Set> archRuleDeclarationsFrom(Class testClass, Field field, Class fieldOwner, + private static Set> archRuleDeclarationsFrom(List> testClassPath, Field field, Class fieldOwner, Class archTestAnnotationType, boolean forceIgnore) { return ArchTests.class.isAssignableFrom(field.getType()) ? - toDeclarations(getArchRulesIn(field, fieldOwner), testClass, archTestAnnotationType, forceIgnore || elementShouldBeIgnored(field)) : - singleton(ArchRuleDeclaration.from(testClass, field, fieldOwner, forceIgnore)); + toDeclarations(getArchTestsIn(field, fieldOwner), testClassPath, archTestAnnotationType, forceIgnore || elementShouldBeIgnored(fieldOwner, field)) : + singleton(ArchRuleDeclaration.from(testClassPath, field, fieldOwner, forceIgnore)); } - private static ArchTests getArchRulesIn(Field field, Class fieldOwner) { + private static ArchTests getArchTestsIn(Field field, Class fieldOwner) { ArchTests value = getValue(field, fieldOwner); return checkNotNull(value, "Field %s.%s is not initialized", fieldOwner.getName(), field.getName()); } private static class AsMethod extends ArchRuleDeclaration { - AsMethod(Class testClass, Method method, Class methodOwner, boolean forceIgnore) { - super(testClass, method, methodOwner, forceIgnore); + AsMethod(List> testClassPath, Method method, Class methodOwner, boolean forceIgnore) { + super(testClassPath, method, methodOwner, forceIgnore); } @Override void handleWith(Handler handler) { - handler.handleMethodDeclaration(declaration, owner, shouldBeIgnored()); + handler.handleMethodDeclaration(testClassPath, declaration, owner, shouldBeIgnored()); } } private static class AsField extends ArchRuleDeclaration { - AsField(Class testClass, Field field, Class fieldOwner, boolean forceIgnore) { - super(testClass, field, fieldOwner, forceIgnore); + AsField(List> testClassPath, Field field, Class fieldOwner, boolean forceIgnore) { + super(testClassPath, field, fieldOwner, forceIgnore); } @Override void handleWith(Handler handler) { - handler.handleFieldDeclaration(declaration, owner, shouldBeIgnored()); + handler.handleFieldDeclaration(testClassPath, declaration, owner, shouldBeIgnored()); } } interface Handler { - void handleFieldDeclaration(Field field, Class fieldOwner, boolean ignore); + void handleFieldDeclaration(List> testClassPath, Field field, Class fieldOwner, boolean ignore); - void handleMethodDeclaration(Method method, Class methodOwner, boolean ignore); + void handleMethodDeclaration(List> testClassPath, Method method, Class methodOwner, boolean ignore); } } diff --git a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchRuleExecution.java b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchRuleExecution.java index f1bb8dbfcd..50b4364058 100644 --- a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchRuleExecution.java +++ b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchRuleExecution.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -16,29 +16,28 @@ package com.tngtech.archunit.junit.internal; import java.lang.reflect.Field; +import java.util.List; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.lang.ArchRule; import org.junit.runner.Description; -import static com.tngtech.archunit.junit.internal.DisplayNameResolver.determineDisplayName; - class ArchRuleExecution extends ArchTestExecution { private final Field ruleField; - ArchRuleExecution(Class testClass, Field ruleField, boolean ignore) { - super(testClass, ignore); + ArchRuleExecution(List> testClassPath, Class ruleDeclaringClass, Field ruleField, boolean ignore) { + super(testClassPath, ruleDeclaringClass, ignore); ArchTestInitializationException.check(ArchRule.class.isAssignableFrom(ruleField.getType()), "Rule field %s.%s to check must be of type %s", - testClass.getSimpleName(), ruleField.getName(), ArchRule.class.getSimpleName()); + ruleDeclaringClass.getSimpleName(), ruleField.getName(), ArchRule.class.getSimpleName()); this.ruleField = ruleField; } @Override Result evaluateOn(JavaClasses classes) { - ArchRule rule = getValue(ruleField, testClass); + ArchRule rule = getValue(ruleField, ruleDeclaringClass); try { rule.check(classes); } catch (Exception | AssertionError e) { @@ -49,7 +48,7 @@ Result evaluateOn(JavaClasses classes) { @Override Description describeSelf() { - return Description.createTestDescription(testClass, determineDisplayName(ruleField.getName()), ruleField.getAnnotations()); + return createDescription(ruleField); } @Override diff --git a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestExecution.java b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestExecution.java index 51372bc049..0670d0a6ce 100644 --- a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestExecution.java +++ b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestExecution.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -15,21 +15,31 @@ */ package com.tngtech.archunit.junit.internal; +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; import com.tngtech.archunit.core.domain.JavaClasses; import org.junit.runner.Description; import org.junit.runner.notification.Failure; import org.junit.runner.notification.RunNotifier; +import static com.tngtech.archunit.junit.internal.DisplayNameResolver.determineDisplayName; import static com.tngtech.archunit.junit.internal.ReflectionUtils.getValueOrThrowException; +import static java.util.stream.Collectors.joining; abstract class ArchTestExecution { - final Class testClass; + final List> testClassPath; + final Class ruleDeclaringClass; private final boolean ignore; - ArchTestExecution(Class testClass, boolean ignore) { - this.testClass = testClass; + ArchTestExecution(List> testClassPath, Class ruleDeclaringClass, boolean ignore) { + this.testClassPath = testClassPath; + this.ruleDeclaringClass = ruleDeclaringClass; this.ignore = ignore; } @@ -42,6 +52,26 @@ public String toString() { return describeSelf().toString(); } + Description createDescription(T member) { + Annotation[] annotations = Stream.concat( + Arrays.stream(member.getAnnotations()), + Stream.of(new ArchTestMetaInfo.Instance(member.getName())) + ).toArray(Annotation[]::new); + String testName = formatWithPath(member.getName()); + return Description.createTestDescription(testClassPath.get(0), determineDisplayName(testName), annotations); + } + + private String formatWithPath(String testName) { + if (testClassPath.size() <= 1) { + return testName; + } + + return Stream.concat( + testClassPath.subList(1, testClassPath.size()).stream().map(Class::getSimpleName), + Stream.of(testName) + ).collect(joining(" > ")); + } + abstract String getName(); boolean ignore() { diff --git a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestMetaInfo.java b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestMetaInfo.java new file mode 100644 index 0000000000..3b56d4f2f5 --- /dev/null +++ b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestMetaInfo.java @@ -0,0 +1,72 @@ +/* + * Copyright 2014-2025 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.junit.internal; + +import java.lang.annotation.Annotation; +import java.util.Objects; + +import org.junit.runner.Description; +import org.junit.runner.Runner; +import org.junit.runner.manipulation.Filter; + +/** + * Hack to transport meta-information from {@link ArchTestExecution} to {@link ArchUnitSystemPropertyTestFilterJunit4}. + * Unfortunately, the {@link Filter} interface doesn't allow access to the original child of the {@link Runner}, + * but only the {@link Description}, which is not suitable to obtain the original member name reliably. + */ +@interface ArchTestMetaInfo { + String memberName(); + + class Instance implements ArchTestMetaInfo, Annotation { + private final String memberName; + + Instance(String memberName) { + this.memberName = memberName; + } + + @Override + public String memberName() { + return memberName; + } + + @Override + public Class annotationType() { + return ArchTestMetaInfo.class; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + Instance instance = (Instance) o; + return Objects.equals(memberName, instance.memberName); + } + + @Override + public int hashCode() { + return Objects.hash(memberName); + } + + @Override + public String toString() { + return "@" + ArchTestMetaInfo.class.getSimpleName() + "(" + memberName + ")"; + } + } +} diff --git a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestMethodExecution.java b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestMethodExecution.java index 2fdda33f16..174d029eb3 100644 --- a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestMethodExecution.java +++ b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchTestMethodExecution.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -17,19 +17,19 @@ import java.lang.reflect.Method; import java.util.Arrays; +import java.util.List; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.junit.ArchTest; import org.junit.runner.Description; -import static com.tngtech.archunit.junit.internal.DisplayNameResolver.determineDisplayName; import static com.tngtech.archunit.junit.internal.ReflectionUtils.invokeMethod; class ArchTestMethodExecution extends ArchTestExecution { private final Method testMethod; - ArchTestMethodExecution(Class testClass, Method testMethod, boolean ignore) { - super(testClass, ignore); + ArchTestMethodExecution(List> testClassPath, Class ruleDeclaringClass, Method testMethod, boolean ignore) { + super(testClassPath, ruleDeclaringClass, ignore); this.testMethod = testMethod; } @@ -49,12 +49,12 @@ private void executeTestMethod(JavaClasses classes) { "Methods annotated with @%s must have exactly one parameter of type %s", ArchTest.class.getSimpleName(), JavaClasses.class.getSimpleName()); - invokeMethod(testMethod, testClass, classes); + invokeMethod(testMethod, ruleDeclaringClass, classes); } @Override Description describeSelf() { - return Description.createTestDescription(testClass, determineDisplayName(testMethod.getName()), testMethod.getAnnotations()); + return createDescription(testMethod); } @Override diff --git a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerInternal.java b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerInternal.java index 42b44f17f8..22198ce123 100644 --- a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerInternal.java +++ b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerInternal.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -32,6 +32,7 @@ import com.tngtech.archunit.junit.CacheMode; import com.tngtech.archunit.junit.LocationProvider; import org.junit.runner.Description; +import org.junit.runner.manipulation.NoTestsRemainException; import org.junit.runner.notification.RunNotifier; import org.junit.runners.ParentRunner; import org.junit.runners.model.FrameworkField; @@ -42,7 +43,9 @@ import static com.tngtech.archunit.junit.internal.ArchRuleDeclaration.elementShouldBeIgnored; import static com.tngtech.archunit.junit.internal.ArchRuleDeclaration.toDeclarations; import static com.tngtech.archunit.junit.internal.ArchTestExecution.getValue; +import static com.tngtech.archunit.junit.internal.ReflectionUtils.findAnnotation; import static java.util.Collections.singleton; +import static java.util.Collections.singletonList; import static java.util.stream.Collectors.toList; final class ArchUnitRunnerInternal extends ParentRunner implements ArchUnitRunner.InternalRunner { @@ -51,20 +54,22 @@ final class ArchUnitRunnerInternal extends ParentRunner imple ArchUnitRunnerInternal(Class testClass) throws InitializationError { super(testClass); - checkAnnotation(testClass); + checkTestRunnable(testClass); + + try { + ArchUnitSystemPropertyTestFilterJunit4.filter(this); + } catch (NoTestsRemainException e) { + throw new InitializationError(e); + } } - private static AnalyzeClasses checkAnnotation(Class testClass) { - AnalyzeClasses analyzeClasses = testClass.getAnnotation(AnalyzeClasses.class); - ArchTestInitializationException.check(analyzeClasses != null, - "Class %s must be annotated with @%s", - testClass.getSimpleName(), AnalyzeClasses.class.getSimpleName()); - return analyzeClasses; + private static void checkTestRunnable(Class testClass) { + findAnnotation(testClass, AnalyzeClasses.class); } @Override public Statement classBlock(RunNotifier notifier) { - final Statement statement = super.classBlock(notifier); + Statement statement = super.classBlock(notifier); return new Statement() { @Override public void evaluate() throws Throwable { @@ -92,30 +97,30 @@ private Collection findArchRuleFields() { } private Set findArchRulesIn(FrameworkField ruleField) { - boolean ignore = elementShouldBeIgnored(ruleField.getField()); + boolean ignore = elementShouldBeIgnored(getTestClass().getJavaClass(), ruleField.getField()); if (ruleField.getType() == ArchTests.class) { - return asTestExecutions(getArchRules(ruleField.getField()), ignore); + return asTestExecutions(getArchTests(ruleField.getField()), ignore); } - return singleton(new ArchRuleExecution(getTestClass().getJavaClass(), ruleField.getField(), ignore)); + return singleton(new ArchRuleExecution(singletonList(getTestClass().getJavaClass()), getTestClass().getJavaClass(), ruleField.getField(), ignore)); } private Set asTestExecutions(ArchTests archTests, boolean forceIgnore) { ExecutionTransformer executionTransformer = new ExecutionTransformer(); - for (ArchRuleDeclaration declaration : toDeclarations(archTests, getTestClass().getJavaClass(), ArchTest.class, forceIgnore)) { + for (ArchRuleDeclaration declaration : toDeclarations(archTests, singletonList(getTestClass().getJavaClass()), ArchTest.class, forceIgnore)) { declaration.handleWith(executionTransformer); } return executionTransformer.getExecutions(); } - private ArchTests getArchRules(Field field) { + private ArchTests getArchTests(Field field) { return getValue(field, field.getDeclaringClass()); } private Collection findArchRuleMethods() { List result = new ArrayList<>(); for (FrameworkMethod testMethod : getTestClass().getAnnotatedMethods(ArchTest.class)) { - boolean ignore = elementShouldBeIgnored(testMethod.getMethod()); - result.add(new ArchTestMethodExecution(getTestClass().getJavaClass(), testMethod.getMethod(), ignore)); + boolean ignore = elementShouldBeIgnored(getTestClass().getJavaClass(), testMethod.getMethod()); + result.add(new ArchTestMethodExecution(singletonList(getTestClass().getJavaClass()), getTestClass().getJavaClass(), testMethod.getMethod(), ignore)); } return result; } @@ -154,13 +159,13 @@ private static class ExecutionTransformer implements ArchRuleDeclaration.Handler private final ImmutableSet.Builder executions = ImmutableSet.builder(); @Override - public void handleFieldDeclaration(Field field, Class fieldOwner, boolean ignore) { - executions.add(new ArchRuleExecution(fieldOwner, field, ignore)); + public void handleFieldDeclaration(List> testClassPath, Field field, Class fieldOwner, boolean ignore) { + executions.add(new ArchRuleExecution(testClassPath, fieldOwner, field, ignore)); } @Override - public void handleMethodDeclaration(Method method, Class methodOwner, boolean ignore) { - executions.add(new ArchTestMethodExecution(methodOwner, method, ignore)); + public void handleMethodDeclaration(List> testClassPath, Method method, Class methodOwner, boolean ignore) { + executions.add(new ArchTestMethodExecution(testClassPath, methodOwner, method, ignore)); } Set getExecutions() { @@ -172,7 +177,7 @@ private static class JUnit4ClassAnalysisRequest implements ClassAnalysisRequest private final AnalyzeClasses analyzeClasses; JUnit4ClassAnalysisRequest(Class testClass) { - analyzeClasses = checkAnnotation(testClass); + analyzeClasses = findAnnotation(testClass, AnalyzeClasses.class); } @Override diff --git a/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitSystemPropertyTestFilterJunit4.java b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitSystemPropertyTestFilterJunit4.java new file mode 100644 index 0000000000..eb4bfce4df --- /dev/null +++ b/archunit-junit/junit4/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitSystemPropertyTestFilterJunit4.java @@ -0,0 +1,57 @@ +/* + * Copyright 2014-2025 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.junit.internal; + +import java.util.List; + +import com.google.common.base.Splitter; +import com.tngtech.archunit.ArchConfiguration; +import org.junit.runner.Description; +import org.junit.runner.manipulation.Filter; +import org.junit.runner.manipulation.NoTestsRemainException; +import org.junit.runners.ParentRunner; + +import static java.util.Objects.requireNonNull; + +class ArchUnitSystemPropertyTestFilterJunit4 extends Filter { + private static final String JUNIT_TEST_FILTER_PROPERTY_NAME = "junit.testFilter"; + private final List memberNames; + + private ArchUnitSystemPropertyTestFilterJunit4(List memberNames) { + this.memberNames = memberNames; + } + + @Override + public boolean shouldRun(Description description) { + ArchTestMetaInfo metaInfo = requireNonNull(description.getAnnotation(ArchTestMetaInfo.class)); + return memberNames.contains(metaInfo.memberName()); + } + + @Override + public String describe() { + return JUNIT_TEST_FILTER_PROPERTY_NAME + " = " + memberNames; + } + + static void filter(ParentRunner runner) throws NoTestsRemainException { + ArchConfiguration configuration = ArchConfiguration.get(); + if (!configuration.containsProperty(JUNIT_TEST_FILTER_PROPERTY_NAME)) { + return; + } + + String testFilterProperty = configuration.getProperty(JUNIT_TEST_FILTER_PROPERTY_NAME); + runner.filter(new ArchUnitSystemPropertyTestFilterJunit4(Splitter.on(",").splitToList(testFilterProperty))); + } +} diff --git a/archunit-junit/junit4/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerRunsMethodsTest.java b/archunit-junit/junit4/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerRunsMethodsTest.java index bffe20dd52..cac9ca5360 100644 --- a/archunit-junit/junit4/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerRunsMethodsTest.java +++ b/archunit-junit/junit4/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerRunsMethodsTest.java @@ -31,6 +31,8 @@ import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsMethodsTest.ArchTestWithTestMethod.testSomething; import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsMethodsTest.IgnoredArchTest.toBeIgnoredOne; import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsMethodsTest.IgnoredArchTest.toBeIgnoredTwo; +import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsMethodsTest.IgnoredArchTestWithBaseClass.toBeIgnoredOneInBaseClass; +import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsMethodsTest.IgnoredArchTestWithBaseClass.toBeIgnoredTwoInBaseClass; import static com.tngtech.archunit.junit.internal.ArchUnitRunnerTestUtils.BE_SATISFIED; import static com.tngtech.archunit.junit.internal.ArchUnitRunnerTestUtils.getRule; import static com.tngtech.archunit.junit.internal.ArchUnitRunnerTestUtils.newRunnerFor; @@ -119,6 +121,18 @@ public void ignores_all_methods_in_classes_annotated_with_ArchIgnore() throws In .contains(toBeIgnoredTwo); } + @Test + public void ignores_all_methods_in_base_classes_of_classes_annotated_with_ArchIgnore() { + ArchUnitRunnerInternal runner = newRunnerFor(IgnoredArchTestWithBaseClass.class); + + runner.runChild(getRule(toBeIgnoredOneInBaseClass, runner), runNotifier); + runner.runChild(getRule(toBeIgnoredTwoInBaseClass, runner), runNotifier); + verify(runNotifier, times(2)).fireTestIgnored(descriptionCaptor.capture()); + assertThat(descriptionCaptor.getAllValues()).extractingResultOf("getMethodName") + .contains(toBeIgnoredOneInBaseClass) + .contains(toBeIgnoredTwoInBaseClass); + } + @Test public void ignores_methods_annotated_with_ArchIgnore() throws InitializationError { ArchUnitRunnerInternal runner = new ArchUnitRunnerInternal(ArchTestWithIgnoredMethod.class); @@ -239,6 +253,23 @@ public static void toBeIgnoredTwo(JavaClasses classes) { } } + @ArchIgnore + @AnalyzeClasses(packages = "some.pkg") + public static class IgnoredArchTestWithBaseClass extends BaseClass { + static final String toBeIgnoredOneInBaseClass = "toBeIgnoredOne"; + static final String toBeIgnoredTwoInBaseClass = "toBeIgnoredTwo"; + } + + public static class BaseClass { + @ArchTest + public static void toBeIgnoredOne(JavaClasses classes) { + } + + @ArchTest + public static void toBeIgnoredTwo(JavaClasses classes) { + } + } + @AnalyzeClasses(packages = "some.pkg") public static class ArchTestWithIgnoredMethod { static final String toBeIgnored = "toBeIgnored"; diff --git a/archunit-junit/junit4/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerRunsRuleFieldsTest.java b/archunit-junit/junit4/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerRunsRuleFieldsTest.java index f6f414a418..e6e3cc597d 100644 --- a/archunit-junit/junit4/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerRunsRuleFieldsTest.java +++ b/archunit-junit/junit4/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerRunsRuleFieldsTest.java @@ -13,7 +13,6 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.Description; import org.junit.runner.notification.Failure; import org.junit.runner.notification.RunNotifier; @@ -28,6 +27,8 @@ import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsRuleFieldsTest.ArchTestWithPrivateInstanceField.PRIVATE_RULE_FIELD_NAME; import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsRuleFieldsTest.IgnoredArchTest.RULE_ONE_IN_IGNORED_TEST; import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsRuleFieldsTest.IgnoredArchTest.RULE_TWO_IN_IGNORED_TEST; +import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsRuleFieldsTest.IgnoredArchTestWithBaseClass.RULE_ONE_IN_IGNORED_BASE_CLASS; +import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsRuleFieldsTest.IgnoredArchTestWithBaseClass.RULE_TWO_IN_IGNORED_BASE_CLASS; import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsRuleFieldsTest.SomeArchTest.FAILING_FIELD_NAME; import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsRuleFieldsTest.SomeArchTest.IGNORED_FIELD_NAME; import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsRuleFieldsTest.SomeArchTest.SATISFIED_FIELD_NAME; @@ -38,6 +39,7 @@ import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; import static java.lang.annotation.RetentionPolicy.RUNTIME; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; @@ -45,8 +47,6 @@ import static org.mockito.Mockito.when; public class ArchUnitRunnerRunsRuleFieldsTest { - @Rule - public final ExpectedException thrown = ExpectedException.none(); @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); @Rule @@ -69,7 +69,7 @@ public class ArchUnitRunnerRunsRuleFieldsTest { @InjectMocks private ArchUnitRunnerInternal runner = ArchUnitRunnerTestUtils.newRunnerFor(SomeArchTest.class); - private JavaClasses cachedClasses = importClassesWithContext(Object.class); + private final JavaClasses cachedClasses = importClassesWithContext(Object.class); @Before public void setUp() { @@ -123,11 +123,11 @@ public void should_allow_instance_field_in_abstract_base_class() { public void should_fail_on_wrong_field_type() { ArchUnitRunnerInternal runner = newRunnerFor(WrongArchTestWrongFieldType.class, cache); - thrown.expectMessage("Rule field " + - WrongArchTestWrongFieldType.class.getSimpleName() + "." + NO_RULE_AT_ALL_FIELD_NAME + - " to check must be of type " + ArchRule.class.getSimpleName()); - - runner.runChild(ArchUnitRunnerTestUtils.getRule(NO_RULE_AT_ALL_FIELD_NAME, runner), runNotifier); + assertThatThrownBy( + () -> runner.runChild(ArchUnitRunnerTestUtils.getRule(NO_RULE_AT_ALL_FIELD_NAME, runner), runNotifier) + ) + .hasMessage("Rule field %s.%s to check must be of type %s", + WrongArchTestWrongFieldType.class.getSimpleName(), NO_RULE_AT_ALL_FIELD_NAME, ArchRule.class.getSimpleName()); } @Test @@ -165,6 +165,19 @@ public void should_skip_ignored_test() { .contains(RULE_ONE_IN_IGNORED_TEST, RULE_TWO_IN_IGNORED_TEST); } + @Test + public void should_skip_ignored_test_with_rule_in_base_class() { + ArchUnitRunnerInternal runner = newRunnerFor(IgnoredArchTestWithBaseClass.class, cache); + + runner.runChild(ArchUnitRunnerTestUtils.getRule(RULE_ONE_IN_IGNORED_BASE_CLASS, runner), runNotifier); + runner.runChild(ArchUnitRunnerTestUtils.getRule(RULE_TWO_IN_IGNORED_BASE_CLASS, runner), runNotifier); + + verify(runNotifier, times(2)).fireTestIgnored(descriptionCaptor.capture()); + + assertThat(descriptionCaptor.getAllValues()).extractingResultOf("getMethodName") + .contains(RULE_ONE_IN_IGNORED_BASE_CLASS, RULE_TWO_IN_IGNORED_BASE_CLASS); + } + @Test public void should_pass_annotations_of_rule_field() { ArchUnitRunnerInternal runner = newRunnerFor(ArchTestWithFieldWithAdditionalAnnotation.class, cache); @@ -210,12 +223,15 @@ private void verifyTestFinishedSuccessfully(String expectedDescriptionMethodName verifyTestFinishedSuccessfully(runNotifier, descriptionCaptor, expectedDescriptionMethodName); } - static void verifyTestFinishedSuccessfully(RunNotifier runNotifier, ArgumentCaptor descriptionCaptor, - String expectedDescriptionMethodName) { + static void verifyTestFinishedSuccessfully( + RunNotifier runNotifier, + ArgumentCaptor descriptionCaptor, + String expectedDescriptionMethodName + ) { verify(runNotifier, never()).fireTestFailure(any(Failure.class)); verify(runNotifier).fireTestFinished(descriptionCaptor.capture()); Description description = descriptionCaptor.getValue(); - assertThat(description.getMethodName()).isEqualTo(expectedDescriptionMethodName); + assertThat(description.getMethodName()).endsWith(expectedDescriptionMethodName); } @AnalyzeClasses(packages = "some.pkg") @@ -277,6 +293,21 @@ public static class IgnoredArchTest { public static final ArchRule someRuleTwo = classes().should(NEVER_BE_SATISFIED); } + @ArchIgnore + @AnalyzeClasses(packages = "some.pkg") + public static class IgnoredArchTestWithBaseClass extends BaseClass { + static final String RULE_ONE_IN_IGNORED_BASE_CLASS = "someRuleOne"; + static final String RULE_TWO_IN_IGNORED_BASE_CLASS = "someRuleTwo"; + } + + public static abstract class BaseClass { + @ArchTest + public static final ArchRule someRuleOne = classes().should(NEVER_BE_SATISFIED); + + @ArchTest + public static final ArchRule someRuleTwo = classes().should(NEVER_BE_SATISFIED); + } + @AnalyzeClasses(packages = "some.pkg") public static class ArchTestWithFieldWithAdditionalAnnotation { static final String TEST_FIELD_NAME = "annotatedTestField"; diff --git a/archunit-junit/junit4/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerRunsRuleSetsTest.java b/archunit-junit/junit4/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerRunsRuleSetsTest.java index 5f577bcadd..4d18dce3d4 100644 --- a/archunit-junit/junit4/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerRunsRuleSetsTest.java +++ b/archunit-junit/junit4/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerRunsRuleSetsTest.java @@ -3,6 +3,7 @@ import java.lang.reflect.Method; import java.util.Collection; +import com.tngtech.archunit.ArchConfiguration; import com.tngtech.archunit.core.domain.JavaClass; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.junit.AnalyzeClasses; @@ -13,6 +14,7 @@ import com.tngtech.archunit.lang.ArchCondition; import com.tngtech.archunit.lang.ArchRule; import com.tngtech.archunit.lang.ConditionEvents; +import com.tngtech.archunit.testutil.ArchConfigurationRule; import org.assertj.core.api.iterable.Extractor; import org.junit.Before; import org.junit.Rule; @@ -29,12 +31,17 @@ import static com.google.common.base.Preconditions.checkState; import static com.tngtech.archunit.core.domain.TestUtils.importClassesWithContext; import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsRuleSetsTest.ArchTestWithRuleLibrary.someOtherMethodRuleName; -import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsRuleSetsTest.IgnoredRules.someIgnoredFieldRuleName; -import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsRuleSetsTest.IgnoredRules.someIgnoredMethodRuleName; +import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsRuleSetsTest.FullyIgnoredRules.someFieldRuleInBaseClassOfIgnoredClassName; +import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsRuleSetsTest.FullyIgnoredRules.someFieldRuleInIgnoredClassName; +import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsRuleSetsTest.FullyIgnoredRules.someMethodRuleInBaseClassOfIgnoredClassName; +import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsRuleSetsTest.FullyIgnoredRules.someMethodRuleInIgnoredClassName; import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsRuleSetsTest.IgnoredSubRules.someIgnoredSubFieldRuleName; import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsRuleSetsTest.IgnoredSubRules.someIgnoredSubMethodRuleName; import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsRuleSetsTest.IgnoredSubRules.someNonIgnoredSubFieldRuleName; import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsRuleSetsTest.IgnoredSubRules.someNonIgnoredSubMethodRuleName; +import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsRuleSetsTest.OtherRules.someOtherFieldRuleName; +import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsRuleSetsTest.PartiallyIgnoredRules.someIgnoredFieldRuleName; +import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsRuleSetsTest.PartiallyIgnoredRules.someIgnoredMethodRuleName; import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsRuleSetsTest.Rules.someFieldRuleName; import static com.tngtech.archunit.junit.internal.ArchUnitRunnerRunsRuleSetsTest.Rules.someMethodRuleName; import static com.tngtech.archunit.junit.internal.ArchUnitRunnerTestUtils.getRule; @@ -44,12 +51,17 @@ import static com.tngtech.archunit.testutil.TestUtils.invoke; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.Mockito.atLeast; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; public class ArchUnitRunnerRunsRuleSetsTest { @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); + @Rule + public final ArchConfigurationRule archConfigurationRule = new ArchConfigurationRule(); @Mock private SharedCache cache; @@ -74,6 +86,9 @@ public class ArchUnitRunnerRunsRuleSetsTest { @InjectMocks private ArchUnitRunnerInternal runnerForIgnoredRuleLibrary = newRunnerFor(ArchTestWithIgnoredRuleLibrary.class); + @InjectMocks + private ArchUnitRunnerInternal runnerForFullyIgnoredRuleLibrary = newRunnerFor(ArchTestWithFullyIgnoredRuleLibrary.class); + private final JavaClasses cachedClasses = importClassesWithContext(ArchUnitRunnerRunsRuleSetsTest.class); @Before @@ -84,20 +99,40 @@ public void setUp() { @Test public void should_find_children_in_rule_set() { - assertThat(runnerForRuleSet.getChildren()).as("Rules defined in Test Class").hasSize(2); - assertThat(runnerForRuleSet.getChildren()) - .extracting(resultOf("describeSelf")) - .extractingResultOf("getMethodName") - .as("Descriptions").containsOnly(someFieldRuleName, someMethodRuleName); + assertThat(runnerForRuleSet.getChildren()).as("Rules defined in Test Class") + .hasSize(2) + .map(ArchTestExecution::describeSelf) + .containsExactlyInAnyOrder( + Description.createTestDescription( + ArchTestWithRuleSet.class, + Rules.class.getSimpleName() + " > " + someFieldRuleName + ), + Description.createTestDescription( + ArchTestWithRuleSet.class, + Rules.class.getSimpleName() + " > " + someMethodRuleName + ) + ); } @Test public void should_find_children_in_rule_library() { - assertThat(runnerForRuleLibrary.getChildren()).as("Rules defined in Library").hasSize(3); - assertThat(runnerForRuleLibrary.getChildren()) - .extracting(resultOf("describeSelf")) - .extractingResultOf("getMethodName") - .as("Descriptions").containsOnly(someFieldRuleName, someMethodRuleName, someOtherMethodRuleName); + assertThat(runnerForRuleLibrary.getChildren()).as("Rules defined in Library") + .hasSize(3) + .map(ArchTestExecution::describeSelf) + .containsExactlyInAnyOrder( + Description.createTestDescription( + ArchTestWithRuleLibrary.class, + ArchTestWithRuleSet.class.getSimpleName() + " > " + Rules.class.getSimpleName() + " > " + someFieldRuleName + ), + Description.createTestDescription( + ArchTestWithRuleLibrary.class, + ArchTestWithRuleSet.class.getSimpleName() + " > " + Rules.class.getSimpleName() + " > " + someMethodRuleName + ), + Description.createTestDescription( + ArchTestWithRuleLibrary.class.getName(), + someOtherMethodRuleName + ) + ); } @Test @@ -111,10 +146,22 @@ public void can_run_rule_method() { } @Test - public void describes_nested_rules_within_their_declaring_class() { - for (ArchTestExecution execution : runnerForRuleSet.getChildren()) { - assertThat(execution.describeSelf().getTestClass()).isEqualTo(Rules.class); - } + public void allows_to_run_single_rule_via_configuration() { + ArchConfiguration.get().setProperty("junit.testFilter", someFieldRuleName); + + newRunnerFor(ArchTestWithRuleSet.class).run(runNotifier); + + verifyOnlyTestsRan("Rules > " + someFieldRuleName); + } + + @Test + public void allows_to_run_selected_rules_via_configuration() { + ArchConfiguration.get().setProperty("junit.testFilter", + someFieldRuleName + "," + someOtherMethodRuleName + ",some_non_existing_rule"); + + newRunnerFor(ArchTestWithRuleLibrary.class).run(runNotifier); + + verifyOnlyTestsRan("ArchTestWithRuleSet > Rules > " + someFieldRuleName, someOtherMethodRuleName); } @Test @@ -137,6 +184,31 @@ public void ignores_nested_method_rule() { run(someIgnoredMethodRuleName, runnerForIgnoredRuleLibrary, verifyTestIgnored()); } + @Test + public void ignores_nested_field_rule_of_ignored_class() { + run(someFieldRuleInIgnoredClassName, runnerForFullyIgnoredRuleLibrary, verifyTestIgnored()); + } + + @Test + public void ignores_nested_method_rule_of_ignored_class() { + run(someMethodRuleInIgnoredClassName, runnerForFullyIgnoredRuleLibrary, verifyTestIgnored()); + } + + @Test + public void ignores_nested_field_rule_in_base_class_of_ignored_class() { + run(someFieldRuleInBaseClassOfIgnoredClassName, runnerForFullyIgnoredRuleLibrary, verifyTestIgnored()); + } + + @Test + public void ignores_nested_method_rule_in_base_class_of_ignored_class() { + run(someMethodRuleInBaseClassOfIgnoredClassName, runnerForFullyIgnoredRuleLibrary, verifyTestIgnored()); + } + + @Test + public void ignores_nested_rule_set_in_base_class_of_ignored_class() { + run(someOtherFieldRuleName, runnerForFullyIgnoredRuleLibrary, verifyTestIgnored()); + } + @Test public void ignores_double_nested_field_rule() { run(someIgnoredSubFieldRuleName, runnerForIgnoredRuleLibrary, verifyTestIgnored()); @@ -200,8 +272,26 @@ private Runnable verifyTestRan() { }; } + private void verifyOnlyTestsRan(String... memberNames) { + // Ignore these invocations as they are irrelevant + verify(runNotifier, atLeast(0)).fireTestSuiteStarted(any()); + verify(runNotifier, atLeast(0)).fireTestFailure(any()); + verify(runNotifier, atLeast(0)).fireTestSuiteFinished(any()); + + for (String memberName : memberNames) { + verify(runNotifier).fireTestStarted(descriptionMemberName(memberName)); + verify(runNotifier).fireTestFinished(descriptionMemberName(memberName)); + } + + verifyNoMoreInteractions(runNotifier); + } + + private static Description descriptionMemberName(String memberName) { + return argThat(description -> description.getMethodName().equals(memberName)); + } + // extractingResultOf(..) only looks for public methods - private Extractor resultOf(final String methodName) { + private Extractor resultOf(String methodName) { return input -> { Collection candidates = getAllMethods(input.getClass(), method -> method.getName().equals(methodName)); checkState(!candidates.isEmpty(), @@ -248,7 +338,13 @@ public static class ArchTestWithIgnoredRuleSet { @AnalyzeClasses(packages = "some.pkg") public static class ArchTestWithIgnoredRuleLibrary { @ArchTest - public static final ArchTests rules = ArchTests.in(IgnoredRules.class); + public static final ArchTests rules = ArchTests.in(PartiallyIgnoredRules.class); + } + + @AnalyzeClasses(packages = "some.pkg") + public static class ArchTestWithFullyIgnoredRuleLibrary { + @ArchTest + public static final ArchTests rules = ArchTests.in(FullyIgnoredRules.class); } public static class Rules { @@ -263,7 +359,7 @@ public static void someMethodRule(JavaClasses classes) { } } - public static class IgnoredRules { + public static class PartiallyIgnoredRules { static final String someIgnoredFieldRuleName = "someIgnoredFieldRule"; static final String someIgnoredMethodRuleName = "someIgnoredMethodRule"; @@ -284,6 +380,40 @@ public static void someIgnoredMethodRule(JavaClasses classes) { public static final ArchTests ignoredSubRules = ArchTests.in(Rules.class); } + @ArchIgnore + public static class FullyIgnoredRules extends BaseClass { + static final String someFieldRuleInIgnoredClassName = "someFieldRuleInIgnoredClass"; + static final String someMethodRuleInIgnoredClassName = "someMethodRuleInIgnoredClass"; + static final String someFieldRuleInBaseClassOfIgnoredClassName = "someFieldRuleInBaseClass"; + static final String someMethodRuleInBaseClassOfIgnoredClassName = "someMethodRuleInBaseClass"; + + @ArchTest + public static final ArchRule someFieldRuleInIgnoredClass = classes().should(satisfySomething()); + + @ArchTest + public static void someMethodRuleInIgnoredClass(JavaClasses classes) { + } + } + + public static class BaseClass { + @ArchTest + public static final ArchRule someFieldRuleInBaseClass = classes().should(satisfySomething()); + + @ArchTest + public static void someMethodRuleInBaseClass(JavaClasses classes) { + } + + @ArchTest + public static final ArchTests someSubRulesInBaseClass = ArchTests.in(OtherRules.class); + } + + public static class OtherRules { + static final String someOtherFieldRuleName = "someOtherFieldRule"; + + @ArchTest + public static final ArchRule someOtherFieldRule = classes().should(satisfySomething()); + } + public static class IgnoredSubRules { static final String someIgnoredSubFieldRuleName = "someIgnoredSubFieldRule"; static final String someNonIgnoredSubFieldRuleName = "someNonIgnoredSubFieldRule"; diff --git a/archunit-junit/junit4/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerTest.java b/archunit-junit/junit4/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerTest.java index 771fc9656e..4027c7869b 100644 --- a/archunit-junit/junit4/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerTest.java +++ b/archunit-junit/junit4/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitRunnerTest.java @@ -1,5 +1,6 @@ package com.tngtech.archunit.junit.internal; +import java.lang.annotation.Retention; import java.util.Set; import com.tngtech.archunit.core.domain.JavaClass; @@ -17,7 +18,6 @@ import org.junit.Before; import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.notification.RunNotifier; import org.junit.runners.model.InitializationError; import org.mockito.ArgumentCaptor; @@ -29,7 +29,9 @@ import static com.tngtech.archunit.core.domain.TestUtils.importClasses; import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; +import static java.lang.annotation.RetentionPolicy.RUNTIME; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.verify; @@ -38,8 +40,6 @@ public class ArchUnitRunnerTest { @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); - @Rule - public final ExpectedException thrown = ExpectedException.none(); @Mock private ClassCache cache; @@ -51,7 +51,9 @@ public class ArchUnitRunnerTest { @InjectMocks private ArchUnitRunnerInternal runner = newRunner(SomeArchTest.class); @InjectMocks - private ArchUnitRunnerInternal runnerOfMaxTest = newRunner(MaxAnnotatedTest.class); + private ArchUnitRunnerInternal runnerOfMaxAnnotatedTest = newRunner(MaxAnnotatedTest.class); + @InjectMocks + private ArchUnitRunnerInternal runnerOfMetaAnnotatedTest = newRunner(MetaAnnotatedTest.class); @Before public void setUp() { @@ -60,7 +62,7 @@ public void setUp() { @Test public void runner_creates_correct_analysis_request() { - runnerOfMaxTest.run(new RunNotifier()); + runnerOfMaxAnnotatedTest.run(new RunNotifier()); verify(cache).getClassesToAnalyzeFor(eq(MaxAnnotatedTest.class), analysisRequestCaptor.capture()); @@ -88,13 +90,30 @@ public void runner_clears_cache_after_exception_during_test_run() { } @Test - public void rejects_missing_analyze_annotation() throws InitializationError { - thrown.expect(ArchTestInitializationException.class); - thrown.expectMessage(Object.class.getSimpleName()); - thrown.expectMessage("must be annotated"); - thrown.expectMessage(AnalyzeClasses.class.getSimpleName()); + public void rejects_missing_analyze_annotation() { + assertThatThrownBy( + () -> new ArchUnitRunnerInternal(Object.class) + ) + .isInstanceOf(ArchTestInitializationException.class) + .hasMessageContaining(Object.class.getSimpleName()) + .hasMessageContaining("is not (meta-)annotated") + .hasMessageContaining(AnalyzeClasses.class.getSimpleName()); + } + + @Test + public void runner_creates_correct_analysis_request_for_meta_annotated_class() { + runnerOfMetaAnnotatedTest.run(new RunNotifier()); - new ArchUnitRunnerInternal(Object.class); + verify(cache).getClassesToAnalyzeFor(eq(MetaAnnotatedTest.class), analysisRequestCaptor.capture()); + + AnalyzeClasses analyzeClasses = MetaAnnotatedTest.class.getAnnotation(MetaAnnotatedTest.MetaAnalyzeClasses.class) + .annotationType().getAnnotation(AnalyzeClasses.class); + ClassAnalysisRequest analysisRequest = analysisRequestCaptor.getValue(); + assertThat(analysisRequest.getPackageNames()).isEqualTo(analyzeClasses.packages()); + assertThat(analysisRequest.getPackageRoots()).isEqualTo(analyzeClasses.packagesOf()); + assertThat(analysisRequest.getLocationProviders()).isEqualTo(analyzeClasses.locations()); + assertThat(analysisRequest.scanWholeClasspath()).as("scan whole classpath").isTrue(); + assertThat(analysisRequest.getImportOptions()).isEqualTo(analyzeClasses.importOptions()); } private ArchUnitRunnerInternal newRunner(Class testClass) { @@ -161,4 +180,19 @@ public static class MaxAnnotatedTest { public static void someTest(JavaClasses classes) { } } + + @MetaAnnotatedTest.MetaAnalyzeClasses + public static class MetaAnnotatedTest { + @ArchTest + public static void someTest(JavaClasses classes) { + } + + @Retention(RUNTIME) + @AnalyzeClasses( + packages = {"com.foo", "com.bar"}, + wholeClasspath = true + ) + public @interface MetaAnalyzeClasses { + } + } } diff --git a/archunit-junit/junit5/api/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java b/archunit-junit/junit5/api/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java index b0e65ef6e6..f96fae9e07 100644 --- a/archunit-junit/junit5/api/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java +++ b/archunit-junit/junit5/api/src/main/java/com/tngtech/archunit/junit/AnalyzeClasses.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -24,7 +24,6 @@ import com.tngtech.archunit.core.importer.ImportOption; import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludeJars; import com.tngtech.archunit.core.importer.ImportOption.DoNotIncludeTests; -import com.tngtech.archunit.core.importer.ImportOptions; import org.junit.platform.commons.annotation.Testable; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; @@ -74,7 +73,7 @@ /** * Allows to filter the class import. The supplied types will be instantiated and used to create the - * {@link ImportOptions} passed to the {@link ClassFileImporter}. Considering caching, compare the notes on + * {@link ImportOption ImportOptions} passed to the {@link ClassFileImporter}. Considering caching, compare the notes on * {@link ImportOption}. * * @return The types of {@link ImportOption} to use for the import diff --git a/archunit-junit/junit5/api/src/main/java/com/tngtech/archunit/junit/ArchTag.java b/archunit-junit/junit5/api/src/main/java/com/tngtech/archunit/junit/ArchTag.java index 419afd3a65..b1a5bc5050 100644 --- a/archunit-junit/junit5/api/src/main/java/com/tngtech/archunit/junit/ArchTag.java +++ b/archunit-junit/junit5/api/src/main/java/com/tngtech/archunit/junit/ArchTag.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit-junit/junit5/api/src/main/java/com/tngtech/archunit/junit/ArchTags.java b/archunit-junit/junit5/api/src/main/java/com/tngtech/archunit/junit/ArchTags.java index 9dc3304867..730f9ad9a7 100644 --- a/archunit-junit/junit5/api/src/main/java/com/tngtech/archunit/junit/ArchTags.java +++ b/archunit-junit/junit5/api/src/main/java/com/tngtech/archunit/junit/ArchTags.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit-junit/junit5/api/src/main/java/com/tngtech/archunit/junit/ArchTest.java b/archunit-junit/junit5/api/src/main/java/com/tngtech/archunit/junit/ArchTest.java index a42f7e00e3..08131b63bb 100644 --- a/archunit-junit/junit5/api/src/main/java/com/tngtech/archunit/junit/ArchTest.java +++ b/archunit-junit/junit5/api/src/main/java/com/tngtech/archunit/junit/ArchTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit-junit/junit5/engine-api/build.gradle b/archunit-junit/junit5/engine-api/build.gradle index efc8b2837d..dffc0e57e2 100644 --- a/archunit-junit/junit5/engine-api/build.gradle +++ b/archunit-junit/junit5/engine-api/build.gradle @@ -16,17 +16,10 @@ dependencies { testImplementation project(path: ':archunit', configuration: 'tests') testImplementation dependency.assertj testImplementation dependency.mockito - testImplementation dependency.junit5JupiterApi - - testRuntimeOnly dependency.junit5JupiterEngine } compileJava.dependsOn project(':archunit-junit5-api').finishArchive -archUnitTest { - hasSlowTests = true -} - test { useJUnitPlatform() { excludeEngines 'archunit' diff --git a/archunit-junit/junit5/engine-api/src/main/java/com/tngtech/archunit/junit/engine_api/FieldSelector.java b/archunit-junit/junit5/engine-api/src/main/java/com/tngtech/archunit/junit/engine_api/FieldSelector.java index fcdc8e94f8..2db97f2efe 100644 --- a/archunit-junit/junit5/engine-api/src/main/java/com/tngtech/archunit/junit/engine_api/FieldSelector.java +++ b/archunit-junit/junit5/engine-api/src/main/java/com/tngtech/archunit/junit/engine_api/FieldSelector.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -59,7 +59,7 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } - final FieldSelector other = (FieldSelector) obj; + FieldSelector other = (FieldSelector) obj; return Objects.equals(this.clazz, other.clazz) && Objects.equals(this.field, other.field); } diff --git a/archunit-junit/junit5/engine-api/src/main/java/com/tngtech/archunit/junit/engine_api/FieldSource.java b/archunit-junit/junit5/engine-api/src/main/java/com/tngtech/archunit/junit/engine_api/FieldSource.java index 85abe8ee22..a98fa80bfb 100644 --- a/archunit-junit/junit5/engine-api/src/main/java/com/tngtech/archunit/junit/engine_api/FieldSource.java +++ b/archunit-junit/junit5/engine-api/src/main/java/com/tngtech/archunit/junit/engine_api/FieldSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -62,7 +62,7 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } - final FieldSource other = (FieldSource) obj; + FieldSource other = (FieldSource) obj; return Objects.equals(this.javaClass, other.javaClass) && Objects.equals(this.fieldName, other.fieldName); } diff --git a/archunit-junit/junit5/engine/build.gradle b/archunit-junit/junit5/engine/build.gradle index ef87d98cba..8100031f2d 100644 --- a/archunit-junit/junit5/engine/build.gradle +++ b/archunit-junit/junit5/engine/build.gradle @@ -17,11 +17,8 @@ dependencies { testImplementation project(path: ':archunit', configuration: 'tests') testImplementation dependency.assertj testImplementation dependency.mockito - testImplementation dependency.junit5JupiterApi + testImplementation dependency.mockito_junit5 testImplementation dependency.log4j_core - testImplementation dependency.log4j_slf4j - - testRuntimeOnly dependency.junit5JupiterEngine } gradle.projectsEvaluated { @@ -36,12 +33,8 @@ sourcesJar { from project(':archunit-junit').sourceSets.main.allSource } -archUnitTest { - hasSlowTests = true -} - test { - useJUnitPlatform() { + useJUnitPlatform { excludeEngines 'archunit' } } diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/AbstractArchUnitTestDescriptor.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/AbstractArchUnitTestDescriptor.java index 9505b6d6bc..a5be37f5d4 100644 --- a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/AbstractArchUnitTestDescriptor.java +++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/AbstractArchUnitTestDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -21,7 +21,9 @@ import java.util.HashSet; import java.util.Optional; import java.util.Set; +import java.util.stream.Stream; +import com.tngtech.archunit.core.domain.Formatters; import com.tngtech.archunit.junit.ArchIgnore; import com.tngtech.archunit.junit.ArchTag; import org.junit.platform.commons.support.AnnotationSupport; @@ -33,6 +35,7 @@ import org.junit.platform.engine.support.hierarchical.Node; import static java.util.Collections.emptySet; +import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toSet; abstract class AbstractArchUnitTestDescriptor extends AbstractTestDescriptor implements Node { @@ -69,4 +72,15 @@ public Set getTags() { result.addAll(getParent().map(TestDescriptor::getTags).orElse(emptySet())); return result; } + + static String formatWithPath(UniqueId uniqueId, String name) { + return Stream.concat( + uniqueId.getSegments().stream() + .filter(it -> it.getType().equals(ArchUnitTestDescriptor.CLASS_SEGMENT_TYPE)) + .skip(1) + .map(UniqueId.Segment::getValue) + .map(Formatters::ensureSimpleName), + Stream.of(name) + ).collect(joining(" > ")); + } } diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitEngineDescriptor.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitEngineDescriptor.java index 8981ccee06..015c80adc9 100644 --- a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitEngineDescriptor.java +++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitEngineDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitEngineExecutionContext.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitEngineExecutionContext.java index a6ca8b63f1..a4a8ddb131 100644 --- a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitEngineExecutionContext.java +++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitEngineExecutionContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitSystemPropertyTestFilterJUnit5.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitSystemPropertyTestFilterJUnit5.java new file mode 100644 index 0000000000..f05ca55134 --- /dev/null +++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitSystemPropertyTestFilterJUnit5.java @@ -0,0 +1,61 @@ +/* + * Copyright 2014-2025 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.junit.internal; + +import java.util.List; +import java.util.Set; +import java.util.function.Predicate; + +import com.google.common.base.Splitter; +import com.google.common.collect.ImmutableSet; +import com.tngtech.archunit.ArchConfiguration; +import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.UniqueId; + +import static com.tngtech.archunit.junit.internal.ArchUnitTestDescriptor.FIELD_SEGMENT_TYPE; +import static com.tngtech.archunit.junit.internal.ArchUnitTestDescriptor.METHOD_SEGMENT_TYPE; + +class ArchUnitSystemPropertyTestFilterJUnit5 { + private static final String JUNIT_TEST_FILTER_PROPERTY_NAME = "junit.testFilter"; + private static final Set MEMBER_SEGMENT_TYPES = ImmutableSet.of(FIELD_SEGMENT_TYPE, METHOD_SEGMENT_TYPE); + + void filter(TestDescriptor descriptor) { + ArchConfiguration configuration = ArchConfiguration.get(); + if (!configuration.containsProperty(JUNIT_TEST_FILTER_PROPERTY_NAME)) { + return; + } + + String testFilterProperty = configuration.getProperty(JUNIT_TEST_FILTER_PROPERTY_NAME); + List memberNames = Splitter.on(",").splitToList(testFilterProperty); + Predicate shouldRunPredicate = testDescriptor -> memberNameMatches(testDescriptor, memberNames); + removeNonMatching(descriptor, shouldRunPredicate); + } + + private void removeNonMatching(TestDescriptor descriptor, Predicate shouldRunPredicate) { + ImmutableSet.copyOf(descriptor.getChildren()) + .forEach(child -> removeNonMatching(child, shouldRunPredicate)); + + if (!descriptor.isRoot() && descriptor.getChildren().isEmpty() && !shouldRunPredicate.test(descriptor)) { + descriptor.removeFromHierarchy(); + } + } + + private static boolean memberNameMatches(TestDescriptor testDescriptor, List memberNames) { + UniqueId.Segment lastSegment = testDescriptor.getUniqueId().getLastSegment(); + return MEMBER_SEGMENT_TYPES.contains(lastSegment.getType()) + && memberNames.stream().anyMatch(it -> lastSegment.getValue().equals(it)); + } +} diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestDescriptor.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestDescriptor.java index 2b818a0e1f..b8f83d3174 100644 --- a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestDescriptor.java +++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -32,18 +32,20 @@ import com.tngtech.archunit.junit.engine_api.FieldSource; import com.tngtech.archunit.lang.ArchRule; import org.junit.platform.engine.TestDescriptor; +import org.junit.platform.engine.TestSource; import org.junit.platform.engine.UniqueId; import org.junit.platform.engine.support.descriptor.ClassSource; import org.junit.platform.engine.support.descriptor.MethodSource; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static com.google.common.base.Preconditions.checkArgument; import static com.tngtech.archunit.junit.internal.DisplayNameResolver.determineDisplayName; +import static com.tngtech.archunit.junit.internal.ReflectionUtils.findAnnotation; import static com.tngtech.archunit.junit.internal.ReflectionUtils.getAllFields; import static com.tngtech.archunit.junit.internal.ReflectionUtils.getAllMethods; import static com.tngtech.archunit.junit.internal.ReflectionUtils.getValueOrThrowException; import static com.tngtech.archunit.junit.internal.ReflectionUtils.invokeMethod; +import static com.tngtech.archunit.junit.internal.ReflectionUtils.tryFindAnnotation; import static com.tngtech.archunit.junit.internal.ReflectionUtils.withAnnotation; class ArchUnitTestDescriptor extends AbstractArchUnitTestDescriptor implements CreatesChildren { @@ -70,8 +72,8 @@ static void resolve(TestDescriptor parent, ElementResolver resolver, ClassCache } private static void createTestDescriptor(TestDescriptor parent, ClassCache classCache, Class clazz, ElementResolver childResolver) { - if (clazz.getAnnotation(AnalyzeClasses.class) == null) { - LOG.warn("Class {} is not annotated with @{} and thus cannot run as a top level test. " + if (!tryFindAnnotation(clazz, AnalyzeClasses.class).isPresent()) { + LOG.warn("Class {} is not (meta-)annotated with @{} and thus cannot run as a top level test. " + "This warning can be ignored if {} is only used as part of a rules library included via {}.in({}.class).", clazz.getName(), AnalyzeClasses.class.getSimpleName(), clazz.getSimpleName(), ArchTests.class.getSimpleName(), clazz.getSimpleName()); @@ -151,7 +153,7 @@ private static class ArchUnitRuleDescriptor extends AbstractArchUnitTestDescript private final Supplier classes; ArchUnitRuleDescriptor(UniqueId uniqueId, ArchRule rule, Supplier classes, TestMember field) { - super(uniqueId, determineDisplayName(field.getName()), FieldSource.from(field.member), field.member); + super(uniqueId, determineDisplayName(formatWithPath(uniqueId, field.getName())), FieldSource.from(field.member), field.member); this.rule = rule; this.classes = classes; } @@ -174,7 +176,7 @@ private static class ArchUnitMethodDescriptor extends AbstractArchUnitTestDescri ArchUnitMethodDescriptor(UniqueId uniqueId, TestMember method, Supplier classes) { super(uniqueId.append("method", method.member.getName()), - determineDisplayName(method.member.getName()), + determineDisplayName(formatWithPath(uniqueId, method.member.getName())), MethodSource.from(method.member), method.member); @@ -211,13 +213,21 @@ private static class ArchUnitArchTestsDescriptor extends AbstractArchUnitTestDes super(resolver.getUniqueId(), archTests.getDisplayName(), - ClassSource.from(archTests.getDefinitionLocation()), + noSource(), field.member, archTests.getDefinitionLocation()); this.archTests = archTests; this.classes = classes; } + /** + * We don't pass a ClassSource for intermediary descriptors or it will be used as test location by test executors. + * We want the root class declaring @AnalyzeClasses to be used for this. + */ + private static TestSource noSource() { + return null; + } + @Override public void createChildren(ElementResolver resolver) { archTests.handleFields(field -> @@ -282,15 +292,7 @@ private static class JUnit5ClassAnalysisRequest implements ClassAnalysisRequest private final AnalyzeClasses analyzeClasses; JUnit5ClassAnalysisRequest(Class testClass) { - analyzeClasses = checkAnnotation(testClass); - } - - private static AnalyzeClasses checkAnnotation(Class testClass) { - AnalyzeClasses analyzeClasses = testClass.getAnnotation(AnalyzeClasses.class); - checkArgument(analyzeClasses != null, - "Class %s must be annotated with @%s", - testClass.getSimpleName(), AnalyzeClasses.class.getSimpleName()); - return analyzeClasses; + analyzeClasses = findAnnotation(testClass, AnalyzeClasses.class); } @Override diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngine.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngine.java index 0625fcfc37..084ca07fd3 100644 --- a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngine.java +++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngine.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -71,6 +71,9 @@ public final class ArchUnitTestEngine extends HierarchicalTestEngine { static final String UNIQUE_ID = "archunit"; + private final ArchUnitSystemPropertyTestFilterJUnit5 systemPropertyTestFilter = new ArchUnitSystemPropertyTestFilterJUnit5(); + + @SuppressWarnings("FieldMayBeFinal") private SharedCache cache = new SharedCache(); // NOTE: We want to change this in tests -> no static/final reference @Override @@ -89,6 +92,8 @@ public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId resolveRequestedFields(discoveryRequest, uniqueId, result); resolveRequestedUniqueIds(discoveryRequest, uniqueId, result); + systemPropertyTestFilter.filter(result); + return result; } diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/CreatesChildren.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/CreatesChildren.java index 39a97ab7de..3a42901711 100644 --- a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/CreatesChildren.java +++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/CreatesChildren.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ElementResolver.java b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ElementResolver.java index 7d2fbf559f..b47cd16fa4 100644 --- a/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ElementResolver.java +++ b/archunit-junit/junit5/engine/src/main/java/com/tngtech/archunit/junit/internal/ElementResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -159,7 +159,7 @@ private static Deque getRemainingSegments(UniqueId rootId, Uni return remainingSegments; } - abstract class PossiblyResolvedClass { + abstract static class PossiblyResolvedClass { void ifRequestedButUnresolved(BiConsumer, ElementResolver> doIfResolved) { } @@ -168,7 +168,7 @@ PossiblyResolvedClass ifRequestedAndResolved(BiConsumer, ElementResolver> doWithChildR } } - private class ClassNotRequested extends PossiblyResolvedClass { + private static class ClassNotRequested extends PossiblyResolvedClass { } - abstract class PossiblyResolvedMember { + abstract static class PossiblyResolvedMember { abstract void ifUnresolved(Consumer childResolver); } - private class SuccessfullyResolvedMember extends PossiblyResolvedMember { + private static class SuccessfullyResolvedMember extends PossiblyResolvedMember { @Override void ifUnresolved(Consumer childResolver) { } @@ -218,7 +218,7 @@ void ifUnresolved(Consumer childResolver) { private class UnresolvedMember extends PossiblyResolvedMember { private final Member member; - private String segmentType; + private final String segmentType; UnresolvedMember(Member member, String segmentType) { this.member = member; diff --git a/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngineTest.java b/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngineTest.java index 14153aad69..449c2f3269 100644 --- a/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngineTest.java +++ b/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/ArchUnitTestEngineTest.java @@ -27,6 +27,7 @@ import com.tngtech.archunit.junit.internal.testexamples.FullAnalyzeClassesSpec; import com.tngtech.archunit.junit.internal.testexamples.LibraryWithPrivateTests; import com.tngtech.archunit.junit.internal.testexamples.SimpleRuleLibrary; +import com.tngtech.archunit.junit.internal.testexamples.TestClassWithMetaAnnotationForAnalyzeClasses; import com.tngtech.archunit.junit.internal.testexamples.TestClassWithMetaTag; import com.tngtech.archunit.junit.internal.testexamples.TestClassWithMetaTags; import com.tngtech.archunit.junit.internal.testexamples.TestClassWithTags; @@ -54,7 +55,7 @@ import com.tngtech.archunit.junit.internal.testexamples.wrong.WrongRuleMethodNotStatic; import com.tngtech.archunit.junit.internal.testexamples.wrong.WrongRuleMethodWrongParameters; import com.tngtech.archunit.junit.internal.testutil.LogCaptor; -import com.tngtech.archunit.junit.internal.testutil.MockitoExtension; +import com.tngtech.archunit.junit.internal.testutil.SystemPropertiesExtension; import com.tngtech.archunit.junit.internal.testutil.TestLogExtension; import com.tngtech.archunit.testutil.TestLogRecorder.RecordedLogEvent; import org.junit.jupiter.api.AfterEach; @@ -75,6 +76,9 @@ import org.mockito.Captor; import org.mockito.InjectMocks; import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; import static com.google.common.collect.Iterables.getLast; import static com.google.common.collect.Iterables.getOnlyElement; @@ -115,7 +119,8 @@ import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; -@ExtendWith(MockitoExtension.class) +@ExtendWith({MockitoExtension.class, SystemPropertiesExtension.class}) +@MockitoSettings(strictness = Strictness.WARN) class ArchUnitTestEngineTest { @Mock private ClassCache classCache; @@ -165,6 +170,20 @@ void a_single_test_class() { assertThat(child.getParent().get()).isEqualTo(descriptor); } + @Test + void a_test_class_with_meta_annotated_analyze_classes() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(TestClassWithMetaAnnotationForAnalyzeClasses.class); + + TestDescriptor descriptor = testEngine.discover(discoveryRequest, engineId); + + TestDescriptor child = getOnlyElement(descriptor.getChildren()); + assertThat(child).isInstanceOf(ArchUnitTestDescriptor.class); + assertThat(child.getUniqueId()).isEqualTo(engineId.append(CLASS_SEGMENT_TYPE, TestClassWithMetaAnnotationForAnalyzeClasses.class.getName())); + assertThat(child.getDisplayName()).isEqualTo(TestClassWithMetaAnnotationForAnalyzeClasses.class.getSimpleName()); + assertThat(child.getType()).isEqualTo(CONTAINER); + assertThat(child.getParent()).get().isEqualTo(descriptor); + } + @Test void source_of_a_single_test_class() { EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(SimpleRuleField.class); @@ -254,16 +273,35 @@ void a_class_with_simple_hierarchy__class_source() { List archRulesDescriptors = getArchRulesDescriptorsOfOnlyChild(descriptor).collect(toList()); TestDescriptor testDescriptor = findRulesDescriptor(archRulesDescriptors, SimpleRules.class); - assertClassSource(testDescriptor, SimpleRules.class); + assertNoIntermediaryTestSource(testDescriptor); testDescriptor.getChildren().forEach(d -> assertThat(d.getSource().isPresent()).as("source is present").isTrue()); testDescriptor = findRulesDescriptor(archRulesDescriptors, SimpleRuleField.class); - assertClassSource(testDescriptor, SimpleRuleField.class); + assertNoIntermediaryTestSource(testDescriptor); testDescriptor.getChildren().forEach(d -> assertThat(d.getSource().isPresent()).as("source is present").isTrue()); } + @Test + void a_class_with_simple_hierarchy__display_name() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(SimpleRuleLibrary.class); + + TestDescriptor descriptor = testEngine.discover(discoveryRequest, engineId); + + List archRulesDescriptors = getArchRulesDescriptorsOfOnlyChild(descriptor).collect(toList()); + + TestDescriptor testDescriptor = findRulesDescriptor(archRulesDescriptors, SimpleRules.class); + assertThat(testDescriptor.getChildren().stream().map(TestDescriptor::getDisplayName)).contains( + SimpleRules.class.getSimpleName() + " > " + SimpleRules.SIMPLE_RULE_FIELD_ONE_NAME + ); + + testDescriptor = findRulesDescriptor(archRulesDescriptors, SimpleRuleField.class); + assertThat(testDescriptor.getChildren().stream().map(TestDescriptor::getDisplayName)).contains( + SimpleRuleField.class.getSimpleName() + " > " + SIMPLE_RULE_FIELD_NAME + ); + } + @Test void a_class_with_complex_hierarchy() { EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(ComplexRuleLibrary.class); @@ -275,6 +313,22 @@ void a_class_with_complex_hierarchy() { .containsOnlyElementsOf(getExpectedIdsForComplexRuleLibrary(engineId)); } + @Test + void a_class_with_complex_hierarchy__display_names() { + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(ComplexRuleLibrary.class); + + TestDescriptor rootDescriptor = testEngine.discover(discoveryRequest, engineId); + + TestDescriptor complexRuleLibraryDescriptor = findRulesDescriptor(rootDescriptor.getChildren(), ComplexRuleLibrary.class); + TestDescriptor simpleRuleLibraryDescriptor = findRulesDescriptor(complexRuleLibraryDescriptor.getChildren(), SimpleRuleLibrary.class); + TestDescriptor simpleRulesDescriptor = findRulesDescriptor(simpleRuleLibraryDescriptor.getChildren(), SimpleRules.class); + assertThat(simpleRulesDescriptor.getChildren().stream().map(TestDescriptor::getDisplayName)).contains( + SimpleRuleLibrary.class.getSimpleName() + + " > " + SimpleRules.class.getSimpleName() + + " > " + SimpleRules.SIMPLE_RULE_FIELD_ONE_NAME + ); + } + @Test void private_instance_members() { EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest().withClass(ClassWithPrivateTests.class); @@ -466,10 +520,10 @@ void mixed_class_methods_and_fields() { expectedLeafIds.add(simpleRuleFieldTestId(engineId)); expectedLeafIds.add(simpleRuleMethodTestId(engineId)); Stream.concat( - SimpleRules.RULE_FIELD_NAMES.stream().map(fieldName -> - simpleRulesId(engineId).append(FIELD_SEGMENT_TYPE, fieldName)), - SimpleRules.RULE_METHOD_NAMES.stream().map(methodName -> - simpleRulesId(engineId).append(METHOD_SEGMENT_TYPE, methodName))) + SimpleRules.RULE_FIELD_NAMES.stream().map(fieldName -> + simpleRulesId(engineId).append(FIELD_SEGMENT_TYPE, fieldName)), + SimpleRules.RULE_METHOD_NAMES.stream().map(methodName -> + simpleRulesId(engineId).append(METHOD_SEGMENT_TYPE, methodName))) .forEach(expectedLeafIds::add); assertThat(getAllLeafUniqueIds(rootDescriptor)) @@ -722,6 +776,42 @@ void filtering_included_packages() { simpleRulesId(engineId)); } + @Test + void filtering_specific_rule_by_system_property() { + System.setProperty("archunit.junit.testFilter", SimpleRules.SIMPLE_RULE_FIELD_TWO_NAME); + + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest() + .withPackage(SimpleRuleLibrary.class.getPackage().getName()) + .withPackageNameFilter(excludePackageNames(WrongRuleMethodNotStatic.class.getPackage().getName())); + + TestDescriptor rootDescriptor = testEngine.discover(discoveryRequest, engineId); + + Set leafIds = getAllLeafUniqueIds(rootDescriptor); + assertThat(leafIds).isNotEmpty(); + leafIds.forEach(leafId -> { + assertThat(leafId.getLastSegment().getType()).isEqualTo(FIELD_SEGMENT_TYPE); + assertThat(leafId.getLastSegment().getValue()).isEqualTo(SimpleRules.SIMPLE_RULE_FIELD_TWO_NAME); + }); + } + + @Test + void filtering_specific_rules_by_system_property() { + System.setProperty("archunit.junit.testFilter", "simple_rule_method_two,simple_rule,some_non_existing_rule"); + + EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest() + .withClass(SimpleRuleLibrary.class); + + TestDescriptor rootDescriptor = testEngine.discover(discoveryRequest, engineId); + + Set leafIds = getAllLeafUniqueIds(rootDescriptor); + assertThat(leafIds).containsOnly( + simpleRulesInLibraryId(engineId) + .append(METHOD_SEGMENT_TYPE, SimpleRules.SIMPLE_RULE_METHOD_TWO_NAME), + simpleRuleFieldTestId(engineId + .append(CLASS_SEGMENT_TYPE, SimpleRuleLibrary.class.getName()) + .append(FIELD_SEGMENT_TYPE, SimpleRuleLibrary.RULES_TWO_FIELD))); + } + @Test void all_without_filters() { EngineDiscoveryTestRequest discoveryRequest = new EngineDiscoveryTestRequest() @@ -811,7 +901,14 @@ private void assertClassSource(TestDescriptor child, Class aClass) { assertThat(classSource.getPosition().isPresent()).as("position is present").isFalse(); } - private TestDescriptor findRulesDescriptor(Collection archRulesDescriptors, Class clazz) { + private void assertNoIntermediaryTestSource(TestDescriptor testDescriptor) { + // Test executors like Gradle test support traverse up the descriptor hierarchy until they find + // a test descriptor with a class source that they then pick for the test report. + // We want to show the original class used to trigger the tests for this to avoid confusion. + assertThat(testDescriptor.getSource()).as("source of intermediary test descriptor").isEmpty(); + } + + private TestDescriptor findRulesDescriptor(Collection archRulesDescriptors, Class clazz) { return archRulesDescriptors.stream().filter(d -> d.getUniqueId().toString().contains(clazz.getSimpleName())).findFirst().get(); } } @@ -916,13 +1013,13 @@ public void library_with_rules_in_abstract_base_class() { .append(FIELD_SEGMENT_TYPE, ArchTestWithLibraryWithAbstractBaseClass.FIELD_RULE_LIBRARY_NAME) .append(CLASS_SEGMENT_TYPE, ArchTestWithAbstractBaseClassWithFieldRule.class.getName()) .append(FIELD_SEGMENT_TYPE, ArchTestWithAbstractBaseClassWithFieldRule.INSTANCE_FIELD_NAME) - ); + ); testListener.verifySuccessful(engineId .append(CLASS_SEGMENT_TYPE, ArchTestWithLibraryWithAbstractBaseClass.class.getName()) .append(FIELD_SEGMENT_TYPE, ArchTestWithLibraryWithAbstractBaseClass.METHOD_RULE_LIBRARY_NAME) .append(CLASS_SEGMENT_TYPE, ArchTestWithAbstractBaseClassWithMethodRule.class.getName()) .append(METHOD_SEGMENT_TYPE, ArchTestWithAbstractBaseClassWithMethodRule.INSTANCE_METHOD_NAME) - ); + ); } @Test @@ -992,6 +1089,21 @@ void cache_is_cleared_afterwards() { verify(classCache, atLeastOnce()).getClassesToAnalyzeFor(any(Class.class), any(ClassAnalysisRequest.class)); verifyNoMoreInteractions(classCache); } + + @Test + void a_class_with_analyze_classes_as_meta_annotation() { + execute(createEngineId(), TestClassWithMetaAnnotationForAnalyzeClasses.class); + + verify(classCache).getClassesToAnalyzeFor(eq(TestClassWithMetaAnnotationForAnalyzeClasses.class), classAnalysisRequestCaptor.capture()); + ClassAnalysisRequest request = classAnalysisRequestCaptor.getValue(); + AnalyzeClasses expected = TestClassWithMetaAnnotationForAnalyzeClasses.class.getAnnotation(TestClassWithMetaAnnotationForAnalyzeClasses.MetaAnalyzeClasses.class) + .annotationType().getAnnotation(AnalyzeClasses.class); + assertThat(request.getPackageNames()).isEqualTo(expected.packages()); + assertThat(request.getPackageRoots()).isEqualTo(expected.packagesOf()); + assertThat(request.getLocationProviders()).isEqualTo(expected.locations()); + assertThat(request.scanWholeClasspath()).as("scan whole classpath").isTrue(); + assertThat(request.getImportOptions()).isEqualTo(expected.importOptions()); + } } @Nested diff --git a/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/EngineDiscoveryTestRequest.java b/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/EngineDiscoveryTestRequest.java index 7b3d5f0b33..9e26ff0eba 100644 --- a/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/EngineDiscoveryTestRequest.java +++ b/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/EngineDiscoveryTestRequest.java @@ -7,6 +7,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Optional; +import java.util.Set; import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.junit.engine_api.FieldSelector; @@ -26,6 +27,7 @@ import static com.tngtech.archunit.junit.engine_api.FieldSelector.selectField; import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; import static org.junit.platform.engine.discovery.DiscoverySelectors.selectMethod; @@ -169,5 +171,10 @@ public Optional getBoolean(String key) { public int size() { return 0; } + + @Override + public Set keySet() { + return emptySet(); + } } } diff --git a/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestClassWithMetaAnnotationForAnalyzeClasses.java b/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestClassWithMetaAnnotationForAnalyzeClasses.java new file mode 100644 index 0000000000..3141f35de0 --- /dev/null +++ b/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/TestClassWithMetaAnnotationForAnalyzeClasses.java @@ -0,0 +1,21 @@ +package com.tngtech.archunit.junit.internal.testexamples; + +import java.lang.annotation.Retention; + +import com.tngtech.archunit.junit.AnalyzeClasses; +import com.tngtech.archunit.junit.ArchTest; +import com.tngtech.archunit.lang.ArchRule; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +@TestClassWithMetaAnnotationForAnalyzeClasses.MetaAnalyzeClasses +public class TestClassWithMetaAnnotationForAnalyzeClasses { + + @ArchTest + public static final ArchRule rule_in_class_with_meta_analyze_class_annotation = RuleThatFails.on(UnwantedClass.class); + + @Retention(RUNTIME) + @AnalyzeClasses(wholeClasspath = true) + public @interface MetaAnalyzeClasses { + } +} diff --git a/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/subtwo/SimpleRules.java b/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/subtwo/SimpleRules.java index 582bf8f8a2..727f3d60a8 100644 --- a/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/subtwo/SimpleRules.java +++ b/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testexamples/subtwo/SimpleRules.java @@ -32,5 +32,6 @@ public static void simple_rule_method_two(JavaClasses classes) { public static final String SIMPLE_RULE_FIELD_TWO_NAME = "simple_rule_field_two"; public static final Set RULE_FIELD_NAMES = ImmutableSet.of(SIMPLE_RULE_FIELD_ONE_NAME, SIMPLE_RULE_FIELD_TWO_NAME); public static final String SIMPLE_RULE_METHOD_ONE_NAME = "simple_rule_method_one"; - public static final Set RULE_METHOD_NAMES = ImmutableSet.of(SIMPLE_RULE_METHOD_ONE_NAME, "simple_rule_method_two"); + public static final String SIMPLE_RULE_METHOD_TWO_NAME = "simple_rule_method_two"; + public static final Set RULE_METHOD_NAMES = ImmutableSet.of(SIMPLE_RULE_METHOD_ONE_NAME, SIMPLE_RULE_METHOD_TWO_NAME); } diff --git a/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testutil/MockitoExtension.java b/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testutil/MockitoExtension.java deleted file mode 100644 index 113cd23b50..0000000000 --- a/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testutil/MockitoExtension.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.tngtech.archunit.junit.internal.testutil; - -import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.extension.TestInstancePostProcessor; -import org.mockito.MockitoAnnotations; - -// We can probably replace this, once Mockito 3.0 is out -public class MockitoExtension implements TestInstancePostProcessor { - @Override - public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception { - MockitoAnnotations.initMocks(testInstance); - } -} diff --git a/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testutil/SystemPropertiesExtension.java b/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testutil/SystemPropertiesExtension.java new file mode 100644 index 0000000000..5a865758a9 --- /dev/null +++ b/archunit-junit/junit5/engine/src/test/java/com/tngtech/archunit/junit/internal/testutil/SystemPropertiesExtension.java @@ -0,0 +1,21 @@ +package com.tngtech.archunit.junit.internal.testutil; + +import java.util.Properties; + +import org.junit.jupiter.api.extension.AfterEachCallback; +import org.junit.jupiter.api.extension.BeforeEachCallback; +import org.junit.jupiter.api.extension.ExtensionContext; + +public class SystemPropertiesExtension implements BeforeEachCallback, AfterEachCallback { + private Properties properties; + + @Override + public void beforeEach(ExtensionContext extensionContext) { + properties = (Properties) System.getProperties().clone(); + } + + @Override + public void afterEach(ExtensionContext extensionContext) { + System.setProperties(properties); + } +} diff --git a/archunit-junit/src/api/java/com/tngtech/archunit/junit/ArchTests.java b/archunit-junit/src/api/java/com/tngtech/archunit/junit/ArchTests.java index 500f2473f9..4f15bb3da8 100644 --- a/archunit-junit/src/api/java/com/tngtech/archunit/junit/ArchTests.java +++ b/archunit-junit/src/api/java/com/tngtech/archunit/junit/ArchTests.java @@ -50,6 +50,7 @@ * } * */ +@PublicAPI(usage = ACCESS) public final class ArchTests { private final Class definitionLocation; diff --git a/archunit-junit/src/api/java/com/tngtech/archunit/junit/CacheMode.java b/archunit-junit/src/api/java/com/tngtech/archunit/junit/CacheMode.java index b0192d0d98..bafbfefe5d 100644 --- a/archunit-junit/src/api/java/com/tngtech/archunit/junit/CacheMode.java +++ b/archunit-junit/src/api/java/com/tngtech/archunit/junit/CacheMode.java @@ -29,6 +29,7 @@ * will be reused for BTest. If this is not desired, the {@link CacheMode}.{@link #PER_CLASS} * can be used to completely deactivate caching between different test classes. */ +@PublicAPI(usage = ACCESS) public enum CacheMode { /** * Signals that imported Java classes should be cached for the current test class only, and discarded afterwards. diff --git a/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ArchTestInitializationException.java b/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ArchTestInitializationException.java index 947dbe56cb..b6765b7bd4 100644 --- a/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ArchTestInitializationException.java +++ b/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ArchTestInitializationException.java @@ -1,7 +1,7 @@ package com.tngtech.archunit.junit.internal; class ArchTestInitializationException extends RuntimeException { - private ArchTestInitializationException(String message, Object... args) { + ArchTestInitializationException(String message, Object... args) { super(String.format(message, args)); } diff --git a/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ClassCache.java b/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ClassCache.java index bf3f75dbd7..44e9535f83 100644 --- a/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ClassCache.java +++ b/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ClassCache.java @@ -16,6 +16,7 @@ package com.tngtech.archunit.junit.internal; import java.util.Collection; +import java.util.HashSet; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -29,7 +30,6 @@ import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.core.importer.ImportOption; -import com.tngtech.archunit.core.importer.ImportOptions; import com.tngtech.archunit.core.importer.Location; import com.tngtech.archunit.core.importer.Locations; import com.tngtech.archunit.junit.CacheMode; @@ -67,6 +67,7 @@ public LazyJavaClasses load(LocationsKey key) { } }); + @SuppressWarnings("FieldMayBeFinal") // We want to change this in tests private CacheClassFileImporter cacheClassFileImporter = new CacheClassFileImporter(); JavaClasses getClassesToAnalyzeFor(Class testClass, ClassAnalysisRequest classAnalysisRequest) { @@ -110,9 +111,9 @@ public JavaClasses get() { private synchronized void initialize() { if (javaClasses == null) { - ImportOptions importOptions = new ImportOptions(); + Set importOptions = new HashSet<>(); for (Class optionClass : importOptionTypes) { - importOptions = importOptions.with(newInstanceOf(optionClass)); + importOptions.add(newInstanceOf(optionClass)); } javaClasses = cacheClassFileImporter.importClasses(importOptions, locations); } @@ -121,7 +122,7 @@ private synchronized void initialize() { // Used for testing -> that's also the reason it's declared top level static class CacheClassFileImporter { - JavaClasses importClasses(ImportOptions importOptions, Collection locations) { + JavaClasses importClasses(Set importOptions, Collection locations) { return new ClassFileImporter(importOptions).importLocations(locations); } } @@ -148,7 +149,7 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } - final LocationsKey other = (LocationsKey) obj; + LocationsKey other = (LocationsKey) obj; return Objects.equals(this.importOptionTypes, other.importOptionTypes) && Objects.equals(this.locations, other.locations); } diff --git a/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ReflectionUtils.java b/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ReflectionUtils.java index e05bf41dad..bb0117a080 100644 --- a/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ReflectionUtils.java +++ b/archunit-junit/src/main/java/com/tngtech/archunit/junit/internal/ReflectionUtils.java @@ -23,6 +23,8 @@ import java.lang.reflect.Modifier; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; +import java.util.Optional; import java.util.Set; import java.util.function.Function; import java.util.function.Predicate; @@ -38,20 +40,6 @@ class ReflectionUtils { private ReflectionUtils() { } - static Set> getAllSupertypes(Class type) { - if (type == null) { - return Collections.emptySet(); - } - - ImmutableSet.Builder> result = ImmutableSet.>builder() - .add(type) - .addAll(getAllSupertypes(type.getSuperclass())); - for (Class c : type.getInterfaces()) { - result.addAll(getAllSupertypes(c)); - } - return result.build(); - } - static Collection getAllFields(Class owner, Predicate predicate) { return getAll(owner, Class::getDeclaredFields).filter(predicate).collect(toList()); } @@ -68,18 +56,22 @@ private static Stream getAll(Class type, Function, T[]> colle return result.build(); } - static T newInstanceOf(Class type) { - return com.tngtech.archunit.base.ReflectionUtils.newInstanceOf(type); - } + private static Set> getAllSupertypes(Class type) { + if (type == null) { + return Collections.emptySet(); + } - @SuppressWarnings("unchecked") // callers must know, what they do here, we can't make this compile safe anyway - private static T getValue(Field field, Object owner) { - try { - field.setAccessible(true); - return (T) field.get(owner); - } catch (IllegalAccessException e) { - throw new ReflectionException(e); + ImmutableSet.Builder> result = ImmutableSet.>builder() + .add(type) + .addAll(getAllSupertypes(type.getSuperclass())); + for (Class c : type.getInterfaces()) { + result.addAll(getAllSupertypes(c)); } + return result.build(); + } + + static T newInstanceOf(Class type) { + return com.tngtech.archunit.base.ReflectionUtils.newInstanceOf(type); } static T getValueOrThrowException(Field field, Class fieldOwner, Function exceptionConverter) { @@ -94,6 +86,16 @@ static T getValueOrThrowException(Field field, Class fieldOwner, Function } } + @SuppressWarnings("unchecked") // callers must know what they do here, we can't make this compile safe anyway + private static T getValue(Field field, Object owner) { + try { + field.setAccessible(true); + return (T) field.get(owner); + } catch (IllegalAccessException e) { + throw new ReflectionException(e); + } + } + static T invokeMethod(Method method, Class methodOwner, Object... args) { if (Modifier.isStatic(method.getModifiers())) { return invoke(null, method, args); @@ -102,7 +104,7 @@ static T invokeMethod(Method method, Class methodOwner, Object... args) { } } - @SuppressWarnings("unchecked") // callers must know, what they do here, we can't make this compile safe anyway + @SuppressWarnings("unchecked") // callers must know what they do here, we can't make this compile safe anyway private static T invoke(Object owner, Method method, Object... args) { method.setAccessible(true); try { @@ -121,7 +123,44 @@ private static void rethrowUnchecked(Throwable throwable) throw (T) throwable; } - static Predicate withAnnotation(final Class annotationType) { + static Predicate withAnnotation(Class annotationType) { return input -> input.isAnnotationPresent(annotationType); } + + /** + * Same as {@link #tryFindAnnotation(Class, Class)}, but throws an exception if no annotation can be found. + */ + static T findAnnotation(final Class clazz, Class annotationType) { + return tryFindAnnotation(clazz, annotationType).orElseThrow(() -> + new ArchTestInitializationException("Class %s is not (meta-)annotated with @%s", clazz.getName(), annotationType.getSimpleName())); + } + + /** + * Recursively searches for an annotation of type {@link T} on the given {@code clazz}. + * Returns the first matching annotation that is found. + * Any further matching annotation possibly present within the meta-annotation hierarchy will be ignored. + * If no matching annotation can be found {@link Optional#empty()} will be returned. + * + * @param clazz The {@link Class} from which to retrieve the annotation + * @return The first found annotation instance reachable in the meta-annotation hierarchy or {@link Optional#empty()} if none can be found + */ + static Optional tryFindAnnotation(final Class clazz, Class annotationType) { + return tryFindAnnotation(clazz.getAnnotations(), annotationType, new HashSet<>()); + } + + private static Optional tryFindAnnotation(final Annotation[] annotations, Class annotationType, Set visited) { + for (Annotation annotation : annotations) { + if (!visited.add(annotation)) { + continue; + } + + Optional result = annotationType.isInstance(annotation) + ? Optional.of(annotationType.cast(annotation)) + : tryFindAnnotation(annotation.annotationType().getAnnotations(), annotationType, visited); + if (result.isPresent()) { + return result; + } + } + return Optional.empty(); + } } diff --git a/archunit-junit/src/test/java/com/tngtech/archunit/junit/internal/ClassCacheConcurrencyTest.java b/archunit-junit/src/test/java/com/tngtech/archunit/junit/internal/ClassCacheConcurrencyTest.java index 43887216e7..0bb5b3bf51 100644 --- a/archunit-junit/src/test/java/com/tngtech/archunit/junit/internal/ClassCacheConcurrencyTest.java +++ b/archunit-junit/src/test/java/com/tngtech/archunit/junit/internal/ClassCacheConcurrencyTest.java @@ -8,7 +8,6 @@ import java.util.stream.IntStream; import com.tngtech.archunit.Slow; -import com.tngtech.archunit.core.importer.ImportOptions; import com.tngtech.archunit.junit.internal.ClassCache.CacheClassFileImporter; import org.junit.After; import org.junit.Before; @@ -23,7 +22,7 @@ import static java.util.concurrent.TimeUnit.MINUTES; import static java.util.stream.Collectors.toList; import static org.mockito.ArgumentMatchers.anyCollection; -import static org.mockito.Mockito.any; +import static org.mockito.ArgumentMatchers.anySet; import static org.mockito.Mockito.atMost; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verifyNoMoreInteractions; @@ -64,11 +63,11 @@ public void concurrent_access() throws Exception { for (Future future : futures) { future.get(1, MINUTES); } - verify(classFileImporter, atMost(TEST_CLASSES.size())).importClasses(any(ImportOptions.class), anyCollection()); + verify(classFileImporter, atMost(TEST_CLASSES.size())).importClasses(anySet(), anyCollection()); verifyNoMoreInteractions(classFileImporter); } - private Runnable repeatGetClassesToAnalyze(final int times) { + private Runnable repeatGetClassesToAnalyze(int times) { return () -> { for (int j = 0; j < times; j++) { cache.getClassesToAnalyzeFor(TEST_CLASSES.get(j % TEST_CLASSES.size()), diff --git a/archunit-junit/src/test/java/com/tngtech/archunit/junit/internal/ClassCacheTest.java b/archunit-junit/src/test/java/com/tngtech/archunit/junit/internal/ClassCacheTest.java index 87f20ecaf9..eb1cb5b6e5 100644 --- a/archunit-junit/src/test/java/com/tngtech/archunit/junit/internal/ClassCacheTest.java +++ b/archunit-junit/src/test/java/com/tngtech/archunit/junit/internal/ClassCacheTest.java @@ -8,7 +8,6 @@ import com.tngtech.archunit.core.domain.JavaClasses; import com.tngtech.archunit.core.importer.ClassFileImporter; import com.tngtech.archunit.core.importer.ImportOption; -import com.tngtech.archunit.core.importer.ImportOptions; import com.tngtech.archunit.core.importer.Location; import com.tngtech.archunit.core.importer.Locations; import com.tngtech.archunit.junit.LocationProvider; @@ -20,7 +19,6 @@ import org.assertj.core.api.Condition; import org.junit.Rule; import org.junit.Test; -import org.junit.rules.ExpectedException; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; import org.mockito.Captor; @@ -33,8 +31,9 @@ import static com.tngtech.archunit.testutil.Assertions.assertThatTypes; import static com.tngtech.java.junit.dataprovider.DataProviders.testForEach; import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.mockito.ArgumentMatchers.anyCollection; +import static org.mockito.ArgumentMatchers.anySet; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -46,9 +45,6 @@ public class ClassCacheTest { @Rule public final MockitoRule mockitoRule = MockitoJUnit.rule(); - @Rule - public final ExpectedException thrown = ExpectedException.none(); - @Rule public final ArchConfigurationRule archConfigurationRule = new ArchConfigurationRule() .resolveAdditionalDependenciesFromClassPath(false); @@ -129,12 +125,13 @@ public void get_all_classes_by_LocationProvider() { @Test public void rejects_LocationProviders_without_default_constructor() { - thrown.expect(ArchTestExecutionException.class); - thrown.expectMessage("public default constructor"); - thrown.expectMessage(LocationProvider.class.getSimpleName()); - - cache.getClassesToAnalyzeFor(WrongLocationProviderWithConstructorParam.class, - analyzeLocation(WrongLocationProviderWithConstructorParam.class)); + assertThatThrownBy( + () -> cache.getClassesToAnalyzeFor(WrongLocationProviderWithConstructorParam.class, + analyzeLocation(WrongLocationProviderWithConstructorParam.class)) + ) + .isInstanceOf(ArchTestExecutionException.class) + .hasMessageContaining("public default constructor") + .hasMessageContaining(LocationProvider.class.getSimpleName()); } @Test @@ -152,12 +149,12 @@ public void if_whole_classpath_is_set_true_then_the_whole_classpath_is_imported( TestAnalysisRequest defaultOptions = new TestAnalysisRequest().withWholeClasspath(true); Class[] expectedImportResult = new Class[]{getClass()}; doReturn(new ClassFileImporter().importClasses(expectedImportResult)) - .when(cacheClassFileImporter).importClasses(any(ImportOptions.class), anyCollection()); + .when(cacheClassFileImporter).importClasses(anySet(), anyCollection()); JavaClasses classes = cache.getClassesToAnalyzeFor(TestClass.class, defaultOptions); assertThatTypes(classes).matchExactly(expectedImportResult); - verify(cacheClassFileImporter).importClasses(any(ImportOptions.class), locationCaptor.capture()); + verify(cacheClassFileImporter).importClasses(anySet(), locationCaptor.capture()); assertThat(locationCaptor.getValue()) .has(locationContaining("archunit")) .has(locationContaining("asm")) @@ -203,7 +200,7 @@ public void when_there_are_only_nonexisting_sources_nothing_is_imported(TestAnal assertThat(classes).isEmpty(); - verify(cacheClassFileImporter).importClasses(any(ImportOptions.class), locationCaptor.capture()); + verify(cacheClassFileImporter).importClasses(anySet(), locationCaptor.capture()); assertThat(locationCaptor.getValue()).isEmpty(); } @@ -245,11 +242,11 @@ private ClassAnalysisRequest analyzeLocation(Class p } private void verifyNumberOfImports(int number) { - verify(cacheClassFileImporter, times(number)).importClasses(any(ImportOptions.class), anyCollection()); + verify(cacheClassFileImporter, times(number)).importClasses(anySet(), anyCollection()); verifyNoMoreInteractions(cacheClassFileImporter); } - private static Condition> locationContaining(final String part) { + private static Condition> locationContaining(String part) { return new Condition>() { @Override public boolean matches(Iterable locations) { @@ -281,7 +278,7 @@ public boolean includes(Location location) { } public static class AnotherTestFilterForJUnitJars implements ImportOption { - private TestFilterForJUnitJars filter = new TestFilterForJUnitJars(); + private final TestFilterForJUnitJars filter = new TestFilterForJUnitJars(); @Override public boolean includes(Location location) { diff --git a/archunit-junit/src/test/java/com/tngtech/archunit/junit/internal/ReflectionUtilsTest.java b/archunit-junit/src/test/java/com/tngtech/archunit/junit/internal/ReflectionUtilsTest.java index f0c12325da..1872f54ddd 100644 --- a/archunit-junit/src/test/java/com/tngtech/archunit/junit/internal/ReflectionUtilsTest.java +++ b/archunit-junit/src/test/java/com/tngtech/archunit/junit/internal/ReflectionUtilsTest.java @@ -1,19 +1,23 @@ package com.tngtech.archunit.junit.internal; +import java.lang.annotation.Retention; import java.lang.reflect.Field; import java.lang.reflect.Member; import java.lang.reflect.Method; import java.util.Collection; import java.util.function.Predicate; -import org.junit.Test; +import org.junit.jupiter.api.Test; import static com.tngtech.archunit.base.Predicates.alwaysTrue; import static com.tngtech.archunit.testutil.ReflectionTestUtils.field; import static com.tngtech.archunit.testutil.ReflectionTestUtils.method; +import static java.lang.annotation.RetentionPolicy.RUNTIME; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; public class ReflectionUtilsTest { + @Test public void getAllFields() { Collection fields = ReflectionUtils.getAllFields(Child.class, named("field")); @@ -38,14 +42,6 @@ public void getAllMethods() { ); } - @Test - public void getAllSupertypes() { - assertThat(ReflectionUtils.getAllSupertypes(Child.class)).containsOnly( - Child.class, ChildInterface.class, UpperMiddle.class, LowerMiddle.class, Parent.class, - SomeInterface.class, OtherInterface.class, Object.class - ); - } - @Test public void getAllMethods_of_interface() { assertThat(ReflectionUtils.getAllMethods(Subinterface.class, alwaysTrue())) @@ -62,7 +58,42 @@ public void getAllFields_of_interface() { field(OtherInterface.class, "OTHER_CONSTANT")); } - private Predicate named(final String name) { + @Test + public void findAnnotation_should_find_direct_annotation() { + SomeAnnotation actual = ReflectionUtils.findAnnotation(DirectlyAnnotated.class, SomeAnnotation.class); + + assertThat(actual).isInstanceOf(SomeAnnotation.class); + assertThat(actual.value()).as(SomeAnnotation.class.getSimpleName() + ".value()").isEqualTo("default"); + } + + @Test + public void findAnnotation_should_find_meta_annotation() { + SomeAnnotation actual = ReflectionUtils.findAnnotation(MetaAnnotated.class, SomeAnnotation.class); + + assertThat(actual).isInstanceOf(SomeAnnotation.class); + assertThat(actual.value()).as(SomeAnnotation.class.getSimpleName() + ".value()").isEqualTo("Meta-Annotation"); + } + + @Test + void findAnnotation_should_reject_annotation_missing_from_hierarchy() { + assertThatThrownBy(() -> ReflectionUtils.findAnnotation(Object.class, SomeAnnotation.class)) + .isInstanceOf(ArchTestInitializationException.class) + .hasMessage("Class %s is not (meta-)annotated with @%s", Object.class.getName(), SomeAnnotation.class.getSimpleName()); + } + + @Test + void tryFindAnnotation_should_return_empty_when_annotation_missing_from_hierarchy() { + assertThat(ReflectionUtils.tryFindAnnotation(Object.class, SomeAnnotation.class)).as("Optional.of(SomeAnnotation)").isEmpty(); + } + + @Test + void findAnnotation_should_return_depth_first_result_of_multiple_annotations_in_hierarchy() { + SomeAnnotation actual = ReflectionUtils.findAnnotation(AnnotationMultipleTimesInHierarchy.class, SomeAnnotation.class); + + assertThat(actual.value()).isEqualTo("default"); + } + + private Predicate named(String name) { return input -> input.getName().equals(name); } @@ -157,4 +188,27 @@ private interface OtherInterface { private interface Subinterface extends SomeInterface, OtherInterface { } + + @Retention(RUNTIME) + private @interface SomeAnnotation { + String value() default "default"; + } + + @SomeAnnotation + private static class DirectlyAnnotated { + } + + @Retention(RUNTIME) + @SomeAnnotation("Meta-Annotation") + private @interface MetaAnnotatedWithSomeAnnotation { + } + + @MetaAnnotatedWithSomeAnnotation + private static class MetaAnnotated { + } + + @SomeAnnotation + @MetaAnnotatedWithSomeAnnotation + private static class AnnotationMultipleTimesInHierarchy { + } } diff --git a/archunit-maven-test/build.gradle b/archunit-maven-test/build.gradle index f4ce491dbf..ca74c13ce0 100644 --- a/archunit-maven-test/build.gradle +++ b/archunit-maven-test/build.gradle @@ -95,9 +95,10 @@ task initializeMavenTest { } def mavenCommand = { String... params -> + def allParams = ['--batch-mode', '--no-transfer-progress'] + params.toList() OperatingSystem.current().isWindows() ? - ['cmd', '/c', 'mvnw.cmd'] + params.toList() : - ['./mvnw'] + params.toList() + ['cmd', '/c', 'mvnw.cmd'] + allParams : + ['./mvnw'] + allParams } def addMavenTest = { IntegrationTestConfig config -> diff --git a/archunit-maven-test/verification/TestResultTest.java b/archunit-maven-test/verification/TestResultTest.java index e3b3a24e48..faddade848 100644 --- a/archunit-maven-test/verification/TestResultTest.java +++ b/archunit-maven-test/verification/TestResultTest.java @@ -17,9 +17,10 @@ import java.util.List; import java.util.Objects; import java.util.Set; +import java.util.stream.Stream; import com.tngtech.archunit.lang.ArchRule; -import com.tngtech.archunit.thirdparty.com.google.common.collect.ImmutableSet; +import com.tngtech.archunit.thirdparty.com.google.common.collect.ImmutableList; import org.joox.Match; import org.junit.Test; import org.xml.sax.SAXException; @@ -27,6 +28,8 @@ import static com.tngtech.archunit.thirdparty.com.google.common.base.Preconditions.checkState; import static java.lang.reflect.Modifier.isStatic; import static java.util.Arrays.asList; +import static java.util.Collections.singletonList; +import static java.util.stream.Collectors.joining; import static org.assertj.core.api.Assertions.assertThat; import static org.joox.JOOX.$; @@ -71,8 +74,8 @@ private static class GivenTestClasses { private Set getTestsIn(Class clazz) { Set result = new HashSet<>(); - result.addAll(singleTestProvider.getTestMethods(ImmutableSet.copyOf(clazz.getDeclaredMethods()))); - result.addAll(singleTestProvider.getTestFields(ImmutableSet.copyOf(clazz.getDeclaredFields()))); + result.addAll(singleTestProvider.getTestMethods(clazz)); + result.addAll(singleTestProvider.getTestFields(clazz)); return result; } @@ -236,16 +239,16 @@ private static SingleTestProvider createSingleTestProvider() { } private interface SingleTestProvider { - Set getTestMethods(Collection methods); + Set getTestMethods(Class clazz); - Set getTestFields(Collection fields); + Set getTestFields(Class clazz); } private static class PlainSingleTestProvider implements SingleTestProvider { @Override - public Set getTestMethods(Collection methods) { + public Set getTestMethods(Class clazz) { Set result = new HashSet<>(); - for (Method method : methods) { + for (Method method : clazz.getDeclaredMethods()) { if (method.isAnnotationPresent(Test.class)) { result.add(new SingleTest(method.getDeclaringClass().getName(), method.getName())); } @@ -254,7 +257,7 @@ public Set getTestMethods(Collection methods) { } @Override - public Set getTestFields(Collection fields) { + public Set getTestFields(Class clazz) { return Collections.emptySet(); } } @@ -274,46 +277,62 @@ private JUnitSingleTestProvider() { } @Override - public Set getTestMethods(Collection methods) { + public Set getTestMethods(Class clazz) { + return getTestMethods(singletonList(clazz), ImmutableList.copyOf(clazz.getDeclaredMethods())); + } + + private Set getTestMethods(List> testClassPath, Collection methods) { Set result = new HashSet<>(); for (Method method : methods) { if (method.getAnnotation(archTestAnnotation) != null || method.getAnnotation(Test.class) != null) { - result.add(new SingleTest(method.getDeclaringClass().getName(), method.getName())); + result.add(createSingleTest(testClassPath, method.getName())); } } return result; } + private static SingleTest createSingleTest(List> testClassPath, String memberName) { + String testName = Stream.concat( + testClassPath.stream().skip(1).map(Class::getSimpleName), + Stream.of(memberName) + ).collect(joining(" > ")); + return new SingleTest(testClassPath.get(0).getName(), testName); + } + @Override - public Set getTestFields(Collection fields) { + public Set getTestFields(Class clazz) { + return getTestFields(singletonList(clazz), ImmutableList.copyOf(clazz.getDeclaredFields())); + } + + private Set getTestFields(List> testClassPath, Collection fields) { Set result = new HashSet<>(); for (Field field : fields) { if (field.getAnnotation(archTestAnnotation) != null) { - result.addAll(getTestFields(field)); + result.addAll(getTestFields(testClassPath, field)); } } return result; } - private Set getTestFields(Field field) { + private Set getTestFields(List> testClassPath, Field field) { if (ArchRule.class.isAssignableFrom(field.getType())) { - return Collections.singleton(new SingleTest(field.getDeclaringClass().getName(), field.getName())); + return Collections.singleton(createSingleTest(testClassPath, field.getName())); } if (archTestsClass.isAssignableFrom(field.getType())) { - return getTestsFrom(getValue(field, null)); + return getTestsFrom(testClassPath, getValue(field, null)); } throw new IllegalStateException("Unknown @ArchTest: " + field); } - private Set getTestsFrom(Object archTests) { + private Set getTestsFrom(List> testClassPath, Object archTests) { Set result = new HashSet<>(); Class definitionLocation = getValue("definitionLocation", archTests); - result.addAll(getTestFields(asList(definitionLocation.getDeclaredFields()))); - result.addAll(getTestMethods(asList(definitionLocation.getDeclaredMethods()))); + List> childTestClassPath = ImmutableList.>builder().addAll(testClassPath).add(definitionLocation).build(); + result.addAll(getTestFields(childTestClassPath, asList(definitionLocation.getDeclaredFields()))); + result.addAll(getTestMethods(childTestClassPath, asList(definitionLocation.getDeclaredMethods()))); return result; } - @SuppressWarnings("unchecked") private T getValue(String fieldName, Object owner) { try { return getValue(owner.getClass().getDeclaredField(fieldName), owner); diff --git a/archunit/build.gradle b/archunit/build.gradle index 4754ef02b0..16a18ff1d0 100644 --- a/archunit/build.gradle +++ b/archunit/build.gradle @@ -11,7 +11,6 @@ dependencies { testImplementation dependency.log4j_api testImplementation dependency.log4j_core - testImplementation dependency.log4j_slf4j testImplementation dependency.junit4 testImplementation dependency.junit_dataprovider testImplementation dependency.mockito @@ -50,7 +49,6 @@ publishing { archUnitTest { providesTestJar = true - hasSlowTests = true } def jdk9MainDirs = ['src/jdk9main/java'] @@ -61,19 +59,21 @@ sourceSets { java { srcDirs = jdk9MainDirs } - compileClasspath += sourceSets.main.compileClasspath + compileClasspath += sourceSets.main.compileClasspath + sourceSets.main.output.classesDirs } jdk9test { java { srcDirs = jdk9TestDirs } compileClasspath += sourceSets.test.compileClasspath + sourceSets.jdk9main.output.classesDirs + runtimeClasspath += sourceSets.test.runtimeClasspath } jdk16test { java { srcDirs = jdk16TestDirs } compileClasspath += sourceSets.test.compileClasspath + sourceSets.jdk9main.output.classesDirs + runtimeClasspath += sourceSets.test.runtimeClasspath } } diff --git a/archunit/src/jdk16test/java/com/tngtech/archunit/core/importer/ClassFileImporterRecordsTest.java b/archunit/src/jdk16test/java/com/tngtech/archunit/core/importer/ClassFileImporterRecordsTest.java index 304e8779b6..9d181fe94b 100644 --- a/archunit/src/jdk16test/java/com/tngtech/archunit/core/importer/ClassFileImporterRecordsTest.java +++ b/archunit/src/jdk16test/java/com/tngtech/archunit/core/importer/ClassFileImporterRecordsTest.java @@ -4,21 +4,33 @@ import com.tngtech.archunit.core.domain.JavaConstructor; import com.tngtech.archunit.core.domain.JavaField; import com.tngtech.archunit.core.domain.JavaMethod; +import com.tngtech.java.junit.dataprovider.DataProvider; +import com.tngtech.java.junit.dataprovider.DataProviderRunner; +import com.tngtech.java.junit.dataprovider.UseDataProvider; import org.junit.Test; +import org.junit.runner.RunWith; import static com.tngtech.archunit.testutil.Assertions.assertThat; +import static com.tngtech.java.junit.dataprovider.DataProviders.testForEach; +@RunWith(DataProviderRunner.class) public class ClassFileImporterRecordsTest { - - @Test - public void imports_simple_record() { - record RecordToImport(String component1, int component2) { + @DataProvider + public static Object[][] imports_record() { + record SimpleRecord(String component1, int component2) { } + record EmptyRecord() { + } + return testForEach(SimpleRecord.class, EmptyRecord.class); + } - JavaClass javaClass = new ClassFileImporter().importClasses(RecordToImport.class, Record.class).get(RecordToImport.class); + @Test + @UseDataProvider + public void imports_record(Class recordToImport) { + JavaClass javaClass = new ClassFileImporter().importClasses(recordToImport, Record.class).get(recordToImport); assertThat(javaClass) - .matches(RecordToImport.class) + .matches(recordToImport) .hasRawSuperclassMatching(Record.class) .hasNoInterfaces() .isInterface(false) diff --git a/archunit/src/jdk9main/java/com/tngtech/archunit/core/domain/Java9DomainPlugin.java b/archunit/src/jdk9main/java/com/tngtech/archunit/core/domain/Java9DomainPlugin.java index 9f0b9b07c0..61bb71f848 100644 --- a/archunit/src/jdk9main/java/com/tngtech/archunit/core/domain/Java9DomainPlugin.java +++ b/archunit/src/jdk9main/java/com/tngtech/archunit/core/domain/Java9DomainPlugin.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -15,7 +15,6 @@ */ package com.tngtech.archunit.core.domain; -import com.tngtech.archunit.Internal; import com.tngtech.archunit.core.InitialConfiguration; import com.tngtech.archunit.core.PluginLoader; @@ -23,14 +22,15 @@ * Resolved via {@link PluginLoader} */ @SuppressWarnings("unused") -@Internal -public class Java9DomainPlugin implements DomainPlugin { +class Java9DomainPlugin implements DomainPlugin { @Override - public void plugInAnnotationPropertiesFormatter(InitialConfiguration propertiesFormatter) { - propertiesFormatter.set(AnnotationPropertiesFormatter.configure() - .formattingArraysWithCurlyBrackets() - .formattingTypesAsClassNames() - .quotingStrings() - .build()); + public void plugInAnnotationFormatter(InitialConfiguration propertiesFormatter) { + propertiesFormatter.set( + AnnotationFormatter.formatAnnotationType(JavaClass::getName) + .formatProperties(config -> config + .formattingArraysWithCurlyBrackets() + .formattingTypesAsClassNames() + .quotingStrings() + )); } } diff --git a/archunit/src/jdk9main/java/com/tngtech/archunit/core/importer/ModuleImportPlugin.java b/archunit/src/jdk9main/java/com/tngtech/archunit/core/importer/ModuleImportPlugin.java index dd75cab6ce..c4cea03c5a 100644 --- a/archunit/src/jdk9main/java/com/tngtech/archunit/core/importer/ModuleImportPlugin.java +++ b/archunit/src/jdk9main/java/com/tngtech/archunit/core/importer/ModuleImportPlugin.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/jdk9main/java/com/tngtech/archunit/core/importer/ModuleLocationFactory.java b/archunit/src/jdk9main/java/com/tngtech/archunit/core/importer/ModuleLocationFactory.java index adfd1a5a8c..e7ee844766 100644 --- a/archunit/src/jdk9main/java/com/tngtech/archunit/core/importer/ModuleLocationFactory.java +++ b/archunit/src/jdk9main/java/com/tngtech/archunit/core/importer/ModuleLocationFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -26,10 +26,10 @@ import java.util.Iterator; import java.util.Optional; import java.util.Set; -import java.util.function.Function; import java.util.stream.Stream; import static com.google.common.base.Preconditions.checkState; +import static java.util.function.Function.identity; import static java.util.stream.Collectors.toList; import static java.util.stream.Collectors.toSet; @@ -129,7 +129,7 @@ private static class ModuleClassFileSource implements ClassFileSource { locations = entries.stream() .map(entry -> new ModuleClassFileLocation(moduleReference, entry)) .filter(classFileLocation -> classFileLocation.isIncludedBy(importOptions)) - .map(Function.identity()); // thanks Java type system :-( + .map(identity()); } private Set loadEntries(ModuleReference moduleReference, NormalizedResourceName resourceName) { diff --git a/archunit/src/jdk9main/java/com/tngtech/archunit/core/importer/ModuleLocationResolver.java b/archunit/src/jdk9main/java/com/tngtech/archunit/core/importer/ModuleLocationResolver.java index c245b56e7f..b5fec8a8db 100644 --- a/archunit/src/jdk9main/java/com/tngtech/archunit/core/importer/ModuleLocationResolver.java +++ b/archunit/src/jdk9main/java/com/tngtech/archunit/core/importer/ModuleLocationResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -34,9 +34,11 @@ import static java.util.stream.Collectors.toList; class ModuleLocationResolver implements LocationResolver { + private final FromClasspathAndUrlClassLoaders standardResolver = new FromClasspathAndUrlClassLoaders(); + @Override public UrlSource resolveClassPath() { - Iterable classpath = UrlSource.From.classPathSystemProperties(); + Iterable classpath = standardResolver.resolveClassPath(); Set systemModuleReferences = ModuleFinder.ofSystem().findAll(); Set configuredModuleReferences = ModuleFinder.of(modulepath()).findAll(); Iterable modulepath = Stream.concat(systemModuleReferences.stream(), configuredModuleReferences.stream()) diff --git a/archunit/src/main/java/com/tngtech/archunit/ArchConfiguration.java b/archunit/src/main/java/com/tngtech/archunit/ArchConfiguration.java index 39bbacfa77..8d36dec924 100644 --- a/archunit/src/main/java/com/tngtech/archunit/ArchConfiguration.java +++ b/archunit/src/main/java/com/tngtech/archunit/ArchConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -43,6 +43,7 @@ /** * Allows access to configured properties in {@value ARCHUNIT_PROPERTIES_RESOURCE_NAME}. */ +@PublicAPI(usage = ACCESS) public final class ArchConfiguration { @Internal // {@value ...} does not work on non public constants outside of the package public static final String ARCHUNIT_PROPERTIES_RESOURCE_NAME = "archunit.properties"; @@ -405,6 +406,7 @@ private Properties copy(Properties properties) { } } + @PublicAPI(usage = ACCESS) public final class ExtensionProperties { private final String extensionIdentifier; diff --git a/archunit/src/main/java/com/tngtech/archunit/Internal.java b/archunit/src/main/java/com/tngtech/archunit/Internal.java index 73589c6052..be2f719948 100644 --- a/archunit/src/main/java/com/tngtech/archunit/Internal.java +++ b/archunit/src/main/java/com/tngtech/archunit/Internal.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/PublicAPI.java b/archunit/src/main/java/com/tngtech/archunit/PublicAPI.java index 7f34747e0b..41c67d30ad 100644 --- a/archunit/src/main/java/com/tngtech/archunit/PublicAPI.java +++ b/archunit/src/main/java/com/tngtech/archunit/PublicAPI.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/base/ArchUnitException.java b/archunit/src/main/java/com/tngtech/archunit/base/ArchUnitException.java index d2fdea11fb..eb796ab5bf 100644 --- a/archunit/src/main/java/com/tngtech/archunit/base/ArchUnitException.java +++ b/archunit/src/main/java/com/tngtech/archunit/base/ArchUnitException.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/base/ChainableFunction.java b/archunit/src/main/java/com/tngtech/archunit/base/ChainableFunction.java index 702d169fd6..bfa7c6f939 100644 --- a/archunit/src/main/java/com/tngtech/archunit/base/ChainableFunction.java +++ b/archunit/src/main/java/com/tngtech/archunit/base/ChainableFunction.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -23,7 +23,7 @@ @PublicAPI(usage = INHERITANCE) public abstract class ChainableFunction implements Function { - public ChainableFunction after(final Function function) { + public ChainableFunction after(Function function) { return new ChainableFunction() { @Override public T apply(E input) { @@ -32,7 +32,7 @@ public T apply(E input) { }; } - public ChainableFunction then(final Function function) { + public ChainableFunction then(Function function) { return new ChainableFunction() { @Override public U apply(F input) { diff --git a/archunit/src/main/java/com/tngtech/archunit/base/ClassLoaders.java b/archunit/src/main/java/com/tngtech/archunit/base/ClassLoaders.java index 73f02db16f..d5247c918e 100644 --- a/archunit/src/main/java/com/tngtech/archunit/base/ClassLoaders.java +++ b/archunit/src/main/java/com/tngtech/archunit/base/ClassLoaders.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/base/DescribedFunction.java b/archunit/src/main/java/com/tngtech/archunit/base/DescribedFunction.java new file mode 100644 index 0000000000..69ea3ff260 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/base/DescribedFunction.java @@ -0,0 +1,55 @@ +/* + * Copyright 2014-2025 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.base; + +import java.util.function.Function; + +import com.tngtech.archunit.PublicAPI; + +import static com.google.common.base.MoreObjects.toStringHelper; +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +import static com.tngtech.archunit.PublicAPI.Usage.INHERITANCE; + +@PublicAPI(usage = INHERITANCE) +public abstract class DescribedFunction implements Function, HasDescription { + private final String description; + + protected DescribedFunction(String description, Object... args) { + this.description = String.format(description, args); + } + + @Override + public String getDescription() { + return description; + } + + @Override + public String toString() { + return toStringHelper(this) + .add("description", description) + .toString(); + } + + @PublicAPI(usage = ACCESS) + public static DescribedFunction describe(String description, final Function function) { + return new DescribedFunction(description) { + @Override + public T apply(F input) { + return function.apply(input); + } + }; + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/base/DescribedIterable.java b/archunit/src/main/java/com/tngtech/archunit/base/DescribedIterable.java index e43c115708..12aa125df3 100644 --- a/archunit/src/main/java/com/tngtech/archunit/base/DescribedIterable.java +++ b/archunit/src/main/java/com/tngtech/archunit/base/DescribedIterable.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -24,12 +24,13 @@ @PublicAPI(usage = INHERITANCE) public interface DescribedIterable extends Iterable, HasDescription { + @PublicAPI(usage = ACCESS) final class From { private From() { } @PublicAPI(usage = ACCESS) - public static DescribedIterable iterable(final Iterable iterable, final String description) { + public static DescribedIterable iterable(Iterable iterable, String description) { return new DescribedIterable() { @Override public String getDescription() { diff --git a/archunit/src/main/java/com/tngtech/archunit/base/DescribedPredicate.java b/archunit/src/main/java/com/tngtech/archunit/base/DescribedPredicate.java index 2e56c7cea2..3f1c609577 100644 --- a/archunit/src/main/java/com/tngtech/archunit/base/DescribedPredicate.java +++ b/archunit/src/main/java/com/tngtech/archunit/base/DescribedPredicate.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -68,15 +68,15 @@ public DescribedPredicate as(String description, Object... params) { return new AsPredicate<>(this, description, params); } - public DescribedPredicate and(final DescribedPredicate other) { + public DescribedPredicate and(DescribedPredicate other) { return new AndPredicate<>(this, other); } - public DescribedPredicate or(final DescribedPredicate other) { + public DescribedPredicate or(DescribedPredicate other) { return new OrPredicate<>(this, other); } - public DescribedPredicate onResultOf(final Function function) { + public DescribedPredicate onResultOf(Function function) { return new OnResultOfPredicate<>(this, function); } @@ -129,27 +129,27 @@ public boolean test(Object input) { }; @PublicAPI(usage = ACCESS) - public static DescribedPredicate equalTo(final T object) { + public static DescribedPredicate equalTo(T object) { return new EqualToPredicate<>(object); } @PublicAPI(usage = ACCESS) - public static > DescribedPredicate lessThan(final T value) { + public static > DescribedPredicate lessThan(T value) { return new LessThanPredicate<>(value); } @PublicAPI(usage = ACCESS) - public static > DescribedPredicate greaterThan(final T value) { + public static > DescribedPredicate greaterThan(T value) { return new GreaterThanPredicate<>(value); } @PublicAPI(usage = ACCESS) - public static > DescribedPredicate lessThanOrEqualTo(final T value) { + public static > DescribedPredicate lessThanOrEqualTo(T value) { return new LessThanOrEqualToPredicate<>(value); } @PublicAPI(usage = ACCESS) - public static > DescribedPredicate greaterThanOrEqualTo(final T value) { + public static > DescribedPredicate greaterThanOrEqualTo(T value) { return new GreaterThanOrEqualToPredicate<>(value); } @@ -162,7 +162,7 @@ public static DescribedPredicate describe(String description, Predicate DescribedPredicate doesNot(final DescribedPredicate predicate) { + public static DescribedPredicate doesNot(DescribedPredicate predicate) { return not(predicate).as("does not %s", predicate.getDescription()).forSubtype(); } @@ -170,7 +170,7 @@ public static DescribedPredicate doesNot(final DescribedPredicate DescribedPredicate doNot(final DescribedPredicate predicate) { + public static DescribedPredicate doNot(DescribedPredicate predicate) { return not(predicate).as("do not %s", predicate.getDescription()).forSubtype(); } @@ -180,7 +180,7 @@ public static DescribedPredicate doNot(final DescribedPredicate The type of object the {@link DescribedPredicate predicate} applies to */ @PublicAPI(usage = ACCESS) - public static DescribedPredicate not(final DescribedPredicate predicate) { + public static DescribedPredicate not(DescribedPredicate predicate) { return new NotPredicate<>(predicate); } @@ -253,7 +253,7 @@ public static DescribedPredicate> empty() { } @PublicAPI(usage = ACCESS) - public static DescribedPredicate> optionalContains(final DescribedPredicate predicate) { + public static DescribedPredicate> optionalContains(DescribedPredicate predicate) { return new OptionalContainsPredicate<>(predicate); } @@ -271,7 +271,7 @@ public static DescribedPredicate> optionalEmpty() { * @param The type of object the {@link DescribedPredicate predicate} applies to */ @PublicAPI(usage = ACCESS) - public static DescribedPredicate> anyElementThat(final DescribedPredicate predicate) { + public static DescribedPredicate> anyElementThat(DescribedPredicate predicate) { return new AnyElementPredicate<>(predicate); } @@ -282,7 +282,7 @@ public static DescribedPredicate> anyElementThat(final * @param The type of object the {@link DescribedPredicate predicate} applies to */ @PublicAPI(usage = ACCESS) - public static DescribedPredicate> allElements(final DescribedPredicate predicate) { + public static DescribedPredicate> allElements(DescribedPredicate predicate) { return new AllElementsPredicate<>(predicate); } diff --git a/archunit/src/main/java/com/tngtech/archunit/base/ForwardingCollection.java b/archunit/src/main/java/com/tngtech/archunit/base/ForwardingCollection.java index 2745b4f896..0ae410de05 100644 --- a/archunit/src/main/java/com/tngtech/archunit/base/ForwardingCollection.java +++ b/archunit/src/main/java/com/tngtech/archunit/base/ForwardingCollection.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/base/ForwardingList.java b/archunit/src/main/java/com/tngtech/archunit/base/ForwardingList.java index 0abf593fe9..f6a0649093 100644 --- a/archunit/src/main/java/com/tngtech/archunit/base/ForwardingList.java +++ b/archunit/src/main/java/com/tngtech/archunit/base/ForwardingList.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/base/ForwardingSet.java b/archunit/src/main/java/com/tngtech/archunit/base/ForwardingSet.java index 3fb9476d9f..b1f164d1f8 100644 --- a/archunit/src/main/java/com/tngtech/archunit/base/ForwardingSet.java +++ b/archunit/src/main/java/com/tngtech/archunit/base/ForwardingSet.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/base/HasDescription.java b/archunit/src/main/java/com/tngtech/archunit/base/HasDescription.java index 4dcfc4a09d..506599ca25 100644 --- a/archunit/src/main/java/com/tngtech/archunit/base/HasDescription.java +++ b/archunit/src/main/java/com/tngtech/archunit/base/HasDescription.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -19,6 +19,7 @@ import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +@PublicAPI(usage = ACCESS) public interface HasDescription { @PublicAPI(usage = ACCESS) String getDescription(); diff --git a/archunit/src/main/java/com/tngtech/archunit/base/MayResolveTypesViaReflection.java b/archunit/src/main/java/com/tngtech/archunit/base/MayResolveTypesViaReflection.java index 686c150515..e194df228a 100644 --- a/archunit/src/main/java/com/tngtech/archunit/base/MayResolveTypesViaReflection.java +++ b/archunit/src/main/java/com/tngtech/archunit/base/MayResolveTypesViaReflection.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/base/Optionals.java b/archunit/src/main/java/com/tngtech/archunit/base/Optionals.java index 80fe155a96..1b3830e0fc 100644 --- a/archunit/src/main/java/com/tngtech/archunit/base/Optionals.java +++ b/archunit/src/main/java/com/tngtech/archunit/base/Optionals.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/base/Predicates.java b/archunit/src/main/java/com/tngtech/archunit/base/Predicates.java index c10213306a..049e92bf4b 100644 --- a/archunit/src/main/java/com/tngtech/archunit/base/Predicates.java +++ b/archunit/src/main/java/com/tngtech/archunit/base/Predicates.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/base/ReflectionUtils.java b/archunit/src/main/java/com/tngtech/archunit/base/ReflectionUtils.java index 24d95fb479..79c7fd53e2 100644 --- a/archunit/src/main/java/com/tngtech/archunit/base/ReflectionUtils.java +++ b/archunit/src/main/java/com/tngtech/archunit/base/ReflectionUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/base/ResolvesTypesViaReflection.java b/archunit/src/main/java/com/tngtech/archunit/base/ResolvesTypesViaReflection.java index 95e9bba7b4..7533a93ace 100644 --- a/archunit/src/main/java/com/tngtech/archunit/base/ResolvesTypesViaReflection.java +++ b/archunit/src/main/java/com/tngtech/archunit/base/ResolvesTypesViaReflection.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/base/Suppliers.java b/archunit/src/main/java/com/tngtech/archunit/base/Suppliers.java index 244258bb0f..d38733a12b 100644 --- a/archunit/src/main/java/com/tngtech/archunit/base/Suppliers.java +++ b/archunit/src/main/java/com/tngtech/archunit/base/Suppliers.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/core/Convertible.java b/archunit/src/main/java/com/tngtech/archunit/core/Convertible.java new file mode 100644 index 0000000000..a27b6b852c --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/core/Convertible.java @@ -0,0 +1,46 @@ +/* + * Copyright 2014-2025 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.core; + +import java.util.Set; + +import com.tngtech.archunit.PublicAPI; +import com.tngtech.archunit.core.domain.Dependency; +import com.tngtech.archunit.core.domain.JavaAccess; + +import static com.tngtech.archunit.PublicAPI.Usage.INHERITANCE; + +/** + * Can be implemented to express that this object might also be considered as object(s) of a different type. + * E.g. {@link JavaAccess} and {@link Dependency} (compare {@link #convertTo(Class)}). + */ +@PublicAPI(usage = INHERITANCE) +public interface Convertible { + /** + * Converts this type to a set of other types. + * For example a {@link JavaAccess} can also be + * considered a {@link Dependency}, so javaAccess.convertTo(Dependency.class) + * will yield a set with a single {@link Dependency} representing this access. + * Or a component dependency grouping many class dependencies could be considered a set of exactly + * these class dependencies. + * The result will be an empty set if no conversion is possible + * (e.g. calling javaAccess.convertTo(Integer.class). + * + * @param type The type to convert to + * @return A set of converted elements, empty if no conversion is possible + */ + Set convertTo(Class type); +} diff --git a/archunit/src/main/java/com/tngtech/archunit/core/InitialConfiguration.java b/archunit/src/main/java/com/tngtech/archunit/core/InitialConfiguration.java index 46cfea5c54..572f08f067 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/InitialConfiguration.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/InitialConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/core/PluginLoader.java b/archunit/src/main/java/com/tngtech/archunit/core/PluginLoader.java index d8fa3c3237..64216c8279 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/PluginLoader.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/PluginLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -15,6 +15,7 @@ */ package com.tngtech.archunit.core; +import java.lang.reflect.Constructor; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -83,7 +84,9 @@ private T create(String className) { try { Class clazz = Class.forName(className); checkCompatibility(className, clazz); - return (T) clazz.getConstructor().newInstance(); + Constructor constructor = clazz.getDeclaredConstructor(); + constructor.setAccessible(true); + return (T) constructor.newInstance(); } catch (Exception e) { throw new PluginLoadingFailedException(e, "Couldn't load plugin of type %s", className); } @@ -136,7 +139,8 @@ public Creator load(String pluginClassName) { public enum JavaVersion { JAVA_9(9), - JAVA_14(14); + JAVA_14(14), + JAVA_21(21); private final int releaseVersion; diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/AccessTarget.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/AccessTarget.java index 2ff8d87341..787d6c8bbd 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/AccessTarget.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/AccessTarget.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -82,6 +82,7 @@ * * @see #resolveMember() */ +@PublicAPI(usage = ACCESS) public abstract class AccessTarget implements HasName.AndFullName, CanBeAnnotated, HasOwner, HasDescription { private final String name; private final JavaClass owner; @@ -152,7 +153,7 @@ public boolean isAnnotatedWith(Class annotationType) { */ @Override @PublicAPI(usage = ACCESS) - public boolean isAnnotatedWith(final String annotationTypeName) { + public boolean isAnnotatedWith(String annotationTypeName) { return resolveMember().isPresent() && resolveMember().get().isAnnotatedWith(annotationTypeName); } @@ -165,7 +166,7 @@ public boolean isAnnotatedWith(final String annotationTypeName) { */ @Override @PublicAPI(usage = ACCESS) - public boolean isAnnotatedWith(final DescribedPredicate> predicate) { + public boolean isAnnotatedWith(DescribedPredicate> predicate) { return resolveMember().isPresent() && resolveMember().get().isAnnotatedWith(predicate); } @@ -187,7 +188,7 @@ public boolean isMetaAnnotatedWith(Class annotationType) { */ @Override @PublicAPI(usage = ACCESS) - public boolean isMetaAnnotatedWith(final String annotationTypeName) { + public boolean isMetaAnnotatedWith(String annotationTypeName) { return resolveMember().isPresent() && resolveMember().get().isMetaAnnotatedWith(annotationTypeName); } @@ -200,7 +201,7 @@ public boolean isMetaAnnotatedWith(final String annotationTypeName) { */ @Override @PublicAPI(usage = ACCESS) - public boolean isMetaAnnotatedWith(final DescribedPredicate> predicate) { + public boolean isMetaAnnotatedWith(DescribedPredicate> predicate) { return resolveMember().isPresent() && resolveMember().get().isMetaAnnotatedWith(predicate); } @@ -217,7 +218,7 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } - final AccessTarget other = (AccessTarget) obj; + AccessTarget other = (AccessTarget) obj; return Objects.equals(this.fullName, other.fullName); } @@ -229,6 +230,7 @@ public String toString() { /** * Predefined {@link ChainableFunction functions} to transform {@link AccessTarget}. */ + @PublicAPI(usage = ACCESS) public static final class Functions { private Functions() { } @@ -248,6 +250,7 @@ public Optional apply(AccessTarget input) { * Represents an {@link AccessTarget} where the target is a field. For further elaboration about the necessity to distinguish * {@link FieldAccessTarget FieldAccessTarget} from {@link JavaField}, refer to the documentation at {@link AccessTarget}. */ + @PublicAPI(usage = ACCESS) public static final class FieldAccessTarget extends AccessTarget implements HasType { private final JavaClass type; @@ -317,6 +320,7 @@ public String getDescription() { /** * Predefined {@link ChainableFunction functions} to transform {@link FieldAccessTarget}. */ + @PublicAPI(usage = ACCESS) public static final class Functions { private Functions() { } @@ -336,6 +340,7 @@ public Optional apply(FieldAccessTarget input) { * Represents an {@link AccessTarget} where the target is a code unit. For further elaboration about the necessity to distinguish * {@link CodeUnitAccessTarget CodeUnitAccessTarget} from {@link JavaCodeUnit}, refer to the documentation at {@link AccessTarget}. */ + @PublicAPI(usage = ACCESS) public abstract static class CodeUnitAccessTarget extends AccessTarget implements HasParameterTypes, HasReturnType, HasThrowsClause { private final ImmutableList parameters; @@ -399,6 +404,7 @@ public Optional resolveMember() { /** * Predefined {@link ChainableFunction functions} to transform {@link CodeUnitAccessTarget}. */ + @PublicAPI(usage = ACCESS) public static final class Functions { private Functions() { } @@ -419,6 +425,7 @@ public Optional apply(CodeUnitAccessTarget input) { * Represents an {@link AccessTarget} where the target is a code unit. For further elaboration about the necessity to distinguish * {@link CodeUnitCallTarget CodeUnitCallTarget} from {@link JavaCodeUnit} refer to the documentation at {@link AccessTarget}. */ + @PublicAPI(usage = ACCESS) public abstract static class CodeUnitCallTarget extends CodeUnitAccessTarget { CodeUnitCallTarget(CodeUnitAccessTargetBuilder builder) { super(builder); @@ -436,6 +443,7 @@ public ThrowsClause getThrowsClause() { * Represents an {@link AccessTarget} where the target is a code unit. For further elaboration about the necessity to distinguish * {@link CodeUnitReferenceTarget CodeUnitReferenceTarget} from {@link JavaCodeUnit} refer to the documentation at {@link AccessTarget}. */ + @PublicAPI(usage = ACCESS) public abstract static class CodeUnitReferenceTarget extends CodeUnitAccessTarget { CodeUnitReferenceTarget(CodeUnitAccessTargetBuilder builder) { super(builder); @@ -453,6 +461,7 @@ public ThrowsClause getThrowsClause() { * Represents a {@link CodeUnitCallTarget} where the target is a constructor. For further elaboration about the necessity to distinguish * {@link ConstructorCallTarget ConstructorCallTarget} from {@link JavaConstructor} refer to the documentation at {@link AccessTarget}. */ + @PublicAPI(usage = ACCESS) public static final class ConstructorCallTarget extends CodeUnitCallTarget { ConstructorCallTarget(CodeUnitAccessTargetBuilder builder) { super(builder); @@ -485,6 +494,7 @@ public String getDescription() { /** * Predefined {@link ChainableFunction functions} to transform {@link ConstructorCallTarget}. */ + @PublicAPI(usage = ACCESS) public static final class Functions { private Functions() { } @@ -504,6 +514,7 @@ public Optional apply(ConstructorCallTarget input) { * Represents a {@link CodeUnitReferenceTarget} where the target is a constructor. For further elaboration about the necessity to distinguish * {@link ConstructorReferenceTarget ConstructorReferenceTarget} from {@link JavaConstructor} refer to the documentation at {@link AccessTarget}. */ + @PublicAPI(usage = ACCESS) public static final class ConstructorReferenceTarget extends CodeUnitReferenceTarget { ConstructorReferenceTarget(CodeUnitAccessTargetBuilder builder) { super(builder); @@ -536,6 +547,7 @@ public String getDescription() { /** * Predefined {@link ChainableFunction functions} to transform {@link ConstructorReferenceTarget}. */ + @PublicAPI(usage = ACCESS) public static final class Functions { private Functions() { } @@ -555,6 +567,7 @@ public Optional apply(ConstructorReferenceTarget input) { * Represents a {@link CodeUnitCallTarget} where the target is a method. For further elaboration about the necessity to distinguish * {@link MethodCallTarget MethodCallTarget} from {@link JavaMethod} refer to the documentation at {@link AccessTarget}. */ + @PublicAPI(usage = ACCESS) public static final class MethodCallTarget extends CodeUnitCallTarget { MethodCallTarget(CodeUnitAccessTargetBuilder builder) { super(builder); @@ -611,6 +624,7 @@ public String getDescription() { /** * Predefined {@link ChainableFunction functions} to transform {@link MethodCallTarget}. */ + @PublicAPI(usage = ACCESS) public static final class Functions { private Functions() { } @@ -630,6 +644,7 @@ public Optional apply(MethodCallTarget input) { * Represents a {@link CodeUnitReferenceTarget} where the target is a method. For further elaboration about the necessity to distinguish * {@link MethodReferenceTarget MethodReferenceTarget} from {@link JavaMethod} refer to the documentation at {@link AccessTarget}. */ + @PublicAPI(usage = ACCESS) public static final class MethodReferenceTarget extends CodeUnitReferenceTarget { MethodReferenceTarget(CodeUnitAccessTargetBuilder builder) { super(builder); @@ -664,6 +679,7 @@ public String getDescription() { /** * Predefined {@link ChainableFunction functions} to transform {@link MethodReferenceTarget}. */ + @PublicAPI(usage = ACCESS) public static final class Functions { private Functions() { } @@ -689,6 +705,7 @@ public Optional apply(MethodReferenceTarget input) { *
  • {@link HasOwner.Predicates}
  • * */ + @PublicAPI(usage = ACCESS) public static final class Predicates { private Predicates() { } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/AnnotationFormatter.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/AnnotationFormatter.java new file mode 100644 index 0000000000..5fd20c13f7 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/AnnotationFormatter.java @@ -0,0 +1,157 @@ +/* + * Copyright 2014-2025 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.core.domain; + +import java.lang.reflect.Array; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.stream.IntStream; + +import com.google.common.base.Joiner; + +import static com.google.common.base.Preconditions.checkNotNull; +import static java.util.function.Function.identity; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toList; + +class AnnotationFormatter { + private final Function annotationTypeFormatter; + private final AnnotationPropertiesFormatter propertiesFormatter; + + AnnotationFormatter(Function annotationTypeFormatter, AnnotationPropertiesFormatter propertiesFormatter) { + this.annotationTypeFormatter = annotationTypeFormatter; + this.propertiesFormatter = propertiesFormatter; + } + + String format(JavaClass annotationType, Map annotationProperties) { + return String.format("@%s(%s)", annotationTypeFormatter.apply(annotationType), propertiesFormatter.formatProperties(annotationProperties)); + } + + static Builder formatAnnotationType(Function annotationTypeFormatter) { + return new Builder(annotationTypeFormatter); + } + + static class Builder { + private final Function annotationTypeFormatter; + + private Builder(Function annotationTypeFormatter) { + this.annotationTypeFormatter = annotationTypeFormatter; + } + + AnnotationFormatter formatProperties(Consumer config) { + AnnotationPropertiesFormatter.Builder propertiesFormatterBuilder = AnnotationPropertiesFormatter.configure(); + config.accept(propertiesFormatterBuilder); + return new AnnotationFormatter(annotationTypeFormatter, propertiesFormatterBuilder.build()); + } + } + + static class AnnotationPropertiesFormatter { + private final Function, String> arrayFormatter; + private final Function, String> typeFormatter; + private final Function stringFormatter; + private final boolean omitOptionalIdentifierForSingleElementAnnotations; + + private AnnotationPropertiesFormatter(Builder builder) { + this.arrayFormatter = checkNotNull(builder.arrayFormatter); + this.typeFormatter = checkNotNull(builder.typeFormatter); + this.stringFormatter = checkNotNull(builder.stringFormatter); + this.omitOptionalIdentifierForSingleElementAnnotations = builder.omitOptionalIdentifierForSingleElementAnnotations; + } + + String formatProperties(Map properties) { + // see Builder#omitOptionalIdentifierForSingleElementAnnotations() for documentation + if (properties.size() == 1 && properties.containsKey("value") && omitOptionalIdentifierForSingleElementAnnotations) { + return formatValue(properties.get("value")); + } + + return properties.entrySet().stream() + .map(entry -> entry.getKey() + "=" + formatValue(entry.getValue())) + .collect(joining(", ")); + } + + String formatValue(Object input) { + if (input instanceof Class) { + return typeFormatter.apply((Class) input); + } + if (input instanceof String) { + return stringFormatter.apply((String) input); + } + if (!input.getClass().isArray()) { + return String.valueOf(input); + } + + List elemToString = IntStream.range(0, Array.getLength(input)) + .mapToObj(i -> formatValue(Array.get(input, i))) + .collect(toList()); + return arrayFormatter.apply(elemToString); + } + + static Builder configure() { + return new Builder(); + } + + static class Builder { + private Function, String> arrayFormatter; + private Function, String> typeFormatter; + private Function stringFormatter = identity(); + private boolean omitOptionalIdentifierForSingleElementAnnotations = false; + + Builder formattingArraysWithSquareBrackets() { + arrayFormatter = input -> "[" + Joiner.on(", ").join(input) + "]"; + return this; + } + + Builder formattingArraysWithCurlyBrackets() { + arrayFormatter = input -> "{" + Joiner.on(", ").join(input) + "}"; + return this; + } + + Builder formattingTypesToString() { + typeFormatter = String::valueOf; + return this; + } + + Builder formattingTypesAsClassNames() { + typeFormatter = input -> input.getName() + ".class"; + return this; + } + + Builder quotingStrings() { + stringFormatter = input -> "\"" + input + "\""; + return this; + } + + /** + * Configures that the identifier is omitted if the annotation is a + * single-element annotation + * and the identifier of the only element is "value". + * + *
    • Example with this configuration: {@code @Copyright("2020 Acme Corporation")}
    • + *
    • Example without this configuration: {@code @Copyright(value="2020 Acme Corporation")}
    + */ + Builder omitOptionalIdentifierForSingleElementAnnotations() { + omitOptionalIdentifierForSingleElementAnnotations = true; + return this; + } + + AnnotationPropertiesFormatter build() { + return new AnnotationPropertiesFormatter(this); + } + } + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/AnnotationPropertiesFormatter.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/AnnotationPropertiesFormatter.java deleted file mode 100644 index 0cae705c87..0000000000 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/AnnotationPropertiesFormatter.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * 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.core.domain; - -import java.lang.reflect.Array; -import java.util.List; -import java.util.Map; -import java.util.function.Function; -import java.util.stream.IntStream; - -import com.google.common.base.Joiner; - -import static com.google.common.base.Preconditions.checkNotNull; -import static java.util.function.Function.identity; -import static java.util.stream.Collectors.joining; -import static java.util.stream.Collectors.toList; - -class AnnotationPropertiesFormatter { - private final Function, String> arrayFormatter; - private final Function, String> typeFormatter; - private final Function stringFormatter; - private final boolean omitOptionalIdentifierForSingleElementAnnotations; - - private AnnotationPropertiesFormatter(Builder builder) { - this.arrayFormatter = checkNotNull(builder.arrayFormatter); - this.typeFormatter = checkNotNull(builder.typeFormatter); - this.stringFormatter = checkNotNull(builder.stringFormatter); - this.omitOptionalIdentifierForSingleElementAnnotations = builder.omitOptionalIdentifierForSingleElementAnnotations; - } - - String formatProperties(Map properties) { - // see Builder#omitOptionalIdentifierForSingleElementAnnotations() for documentation - if (properties.size() == 1 && properties.containsKey("value") && omitOptionalIdentifierForSingleElementAnnotations) { - return formatValue(properties.get("value")); - } - - return properties.entrySet().stream() - .map(entry -> entry.getKey() + "=" + formatValue(entry.getValue())) - .collect(joining(", ")); - } - - String formatValue(Object input) { - if (input instanceof Class) { - return typeFormatter.apply((Class) input); - } - if (input instanceof String) { - return stringFormatter.apply((String) input); - } - if (!input.getClass().isArray()) { - return String.valueOf(input); - } - - List elemToString = IntStream.range(0, Array.getLength(input)) - .mapToObj(i -> formatValue(Array.get(input, i))) - .collect(toList()); - return arrayFormatter.apply(elemToString); - } - - static Builder configure() { - return new Builder(); - } - - static class Builder { - private Function, String> arrayFormatter; - private Function, String> typeFormatter; - private Function stringFormatter = identity(); - private boolean omitOptionalIdentifierForSingleElementAnnotations = false; - - Builder formattingArraysWithSquareBrackets() { - arrayFormatter = input -> "[" + Joiner.on(", ").join(input) + "]"; - return this; - } - - Builder formattingArraysWithCurlyBrackets() { - arrayFormatter = input -> "{" + Joiner.on(", ").join(input) + "}"; - return this; - } - - Builder formattingTypesToString() { - typeFormatter = String::valueOf; - return this; - } - - Builder formattingTypesAsClassNames() { - typeFormatter = input -> input.getName() + ".class"; - return this; - } - - Builder quotingStrings() { - stringFormatter = input -> "\"" + input + "\""; - return this; - } - - /** - * Configures that the identifier is omitted if the annotation is a - * single-element annotation - * and the identifier of the only element is "value". - * - *
    • Example with this configuration: {@code @Copyright("2020 Acme Corporation")}
    • - *
    • Example without this configuration: {@code @Copyright(value="2020 Acme Corporation")}
    - */ - Builder omitOptionalIdentifierForSingleElementAnnotations() { - omitOptionalIdentifierForSingleElementAnnotations = true; - return this; - } - - AnnotationPropertiesFormatter build() { - return new AnnotationPropertiesFormatter(this); - } - } -} diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/AnnotationProxy.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/AnnotationProxy.java index db40c335b6..8426ce08b9 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/AnnotationProxy.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/AnnotationProxy.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -37,10 +37,10 @@ @MayResolveTypesViaReflection(reason = "We depend on the classpath, if we proxy an annotation type") class AnnotationProxy { - private static final InitialConfiguration propertiesFormatter = new InitialConfiguration<>(); + private static final InitialConfiguration annotationFormatter = new InitialConfiguration<>(); static { - DomainPlugin.Loader.loadForCurrentPlatform().plugInAnnotationPropertiesFormatter(propertiesFormatter); + DomainPlugin.Loader.loadForCurrentPlatform().plugInAnnotationFormatter(annotationFormatter); } public static A of(Class annotationType, JavaAnnotation toProxy) { @@ -277,12 +277,7 @@ private ToStringHandler(Class annotationType, JavaAnnotation toProxy, Conv @Override public Object handle(Object proxy, Method method, Object[] args) { - return String.format("@%s(%s)", toProxy.getRawType().getName(), propertiesString()); - } - - private String propertiesString() { - Map unwrappedProperties = unwrapProxiedProperties(); - return propertiesFormatter.get().formatProperties(unwrappedProperties); + return annotationFormatter.get().format(toProxy.getRawType(), unwrapProxiedProperties()); } private Map unwrapProxiedProperties() { @@ -364,7 +359,7 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } - final MethodKey other = (MethodKey) obj; + MethodKey other = (MethodKey) obj; return Objects.equals(this.name, other.name) && Objects.equals(this.paramTypeNames, other.paramTypeNames); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/Dependency.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/Dependency.java index 4bd24a0114..ca8f4d6839 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/Dependency.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/Dependency.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -27,6 +27,7 @@ import com.tngtech.archunit.base.ChainableFunction; import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.base.HasDescription; +import com.tngtech.archunit.core.Convertible; import com.tngtech.archunit.core.domain.properties.HasName; import com.tngtech.archunit.core.domain.properties.HasOwner; import com.tngtech.archunit.core.domain.properties.HasSourceCodeLocation; @@ -34,6 +35,8 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; import static com.tngtech.archunit.base.Optionals.asSet; +import static java.util.Collections.emptySet; +import static java.util.Collections.singleton; /** * Represents a dependency of one Java class on another Java class. Such a dependency can occur by either of the @@ -54,25 +57,24 @@ * Note that a {@link Dependency} will by definition never be a self-reference, * i.e. origin will never be equal to target. */ -public class Dependency implements HasDescription, Comparable, HasSourceCodeLocation { +@PublicAPI(usage = ACCESS) +public class Dependency implements HasDescription, Comparable, HasSourceCodeLocation, Convertible { private final JavaClass originClass; private final JavaClass targetClass; - private final int lineNumber; private final String description; private final SourceCodeLocation sourceCodeLocation; private final int hashCode; - private Dependency(JavaClass originClass, JavaClass targetClass, int lineNumber, String description) { + private Dependency(JavaClass originClass, JavaClass targetClass, SourceCodeLocation sourceCodeLocation, String description) { checkArgument(!originClass.equals(targetClass) || targetClass.isPrimitive(), "Tried to create illegal dependency '%s' (%s -> %s), this is likely a bug!", description, originClass.getSimpleName(), targetClass.getSimpleName()); this.originClass = originClass; this.targetClass = targetClass; - this.lineNumber = lineNumber; this.description = description; - this.sourceCodeLocation = SourceCodeLocation.of(originClass, lineNumber); - hashCode = Objects.hash(originClass, targetClass, lineNumber, description); + this.sourceCodeLocation = sourceCodeLocation; + hashCode = Objects.hash(originClass, targetClass, sourceCodeLocation, description); } static Set tryCreateFromAccess(JavaAccess access) { @@ -80,7 +82,9 @@ static Set tryCreateFromAccess(JavaAccess access) { JavaClass targetOwner = access.getTargetOwner(); ImmutableSet.Builder dependencies = ImmutableSet.builder() .addAll(createComponentTypeDependencies(originOwner, access.getOrigin().getDescription(), targetOwner, access.getSourceCodeLocation())); - dependencies.addAll(asSet(tryCreateDependency(originOwner, targetOwner, access.getDescription(), access.getLineNumber()))); + if (!originOwner.equals(targetOwner) && !targetOwner.isPrimitive()) { + dependencies.add(new Dependency.FromAccess(access)); + } return dependencies.build(); } @@ -96,7 +100,7 @@ static Dependency fromInheritance(JavaClass origin, JavaClass targetSupertype) { String dependencyDescription = originDescription + " " + dependencyType + " " + targetType + " " + targetDescription; String description = dependencyDescription + " in " + origin.getSourceCodeLocation(); - Optional result = tryCreateDependency(origin, targetSupertype, description, 0); + Optional result = tryCreateDependency(origin, targetSupertype, description, origin.getSourceCodeLocation()); if (!result.isPresent()) { throw new IllegalStateException(String.format("Tried to create illegal inheritance dependency '%s' (%s -> %s), this is likely a bug!", @@ -213,7 +217,7 @@ private static Set tryCreateDependency( String targetDescription = bracketFormat(targetClass.getName()); String dependencyDescription = originDescription + " " + dependencyType + " " + targetDescription; String description = dependencyDescription + " in " + sourceCodeLocation; - dependencies.addAll(asSet(tryCreateDependency(originClass, targetClass, description, sourceCodeLocation.getLineNumber()))); + dependencies.addAll(asSet(tryCreateDependency(originClass, targetClass, description, sourceCodeLocation))); return dependencies.build(); } @@ -226,28 +230,34 @@ private static Set createComponentTypeDependencies( String componentTypeTargetDescription = bracketFormat(componentType.get().getName()); String componentTypeDependencyDescription = originDescription + " depends on component type " + componentTypeTargetDescription; String componentTypeDescription = componentTypeDependencyDescription + " in " + sourceCodeLocation; - result.addAll(asSet(tryCreateDependency(originClass, componentType.get(), componentTypeDescription, sourceCodeLocation.getLineNumber()))); + result.addAll(asSet(tryCreateDependency(originClass, componentType.get(), componentTypeDescription, sourceCodeLocation))); componentType = componentType.get().tryGetComponentType(); } return result.build(); } - private static Optional tryCreateDependency(JavaClass originClass, JavaClass targetClass, String description, int lineNumber) { + private static Optional tryCreateDependency(JavaClass originClass, JavaClass targetClass, String description, SourceCodeLocation sourceCodeLocation) { if (originClass.equals(targetClass) || targetClass.isPrimitive()) { return Optional.empty(); } - return Optional.of(new Dependency(originClass, targetClass, lineNumber, description)); + return Optional.of(new Dependency(originClass, targetClass, sourceCodeLocation, description)); } private static String bracketFormat(String name) { return "<" + name + ">"; } + /** + * @return The class where this dependency originates from (e.g. because the origin class calls a method of another class) + */ @PublicAPI(usage = ACCESS) public JavaClass getOriginClass() { return originClass; } + /** + * @return The class that is targeted by this dependency (e.g. because it contains a method that is called from another class) + */ @PublicAPI(usage = ACCESS) public JavaClass getTargetClass() { return targetClass; @@ -265,11 +275,20 @@ public SourceCodeLocation getSourceCodeLocation() { return sourceCodeLocation; } + @Override + @SuppressWarnings("unchecked") // compatibility is explicitly checked + public Set convertTo(Class type) { + if (type.isAssignableFrom(Dependency.class)) { + return (Set) singleton(this); + } + return emptySet(); + } + @Override @PublicAPI(usage = ACCESS) public int compareTo(Dependency o) { return ComparisonChain.start() - .compare(lineNumber, o.lineNumber) + .compare(sourceCodeLocation.getLineNumber(), o.sourceCodeLocation.getLineNumber()) .compare(getDescription(), o.getDescription()) .result(); } @@ -287,10 +306,10 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } - final Dependency other = (Dependency) obj; + Dependency other = (Dependency) obj; return Objects.equals(this.originClass, other.originClass) && Objects.equals(this.targetClass, other.targetClass) - && Objects.equals(this.lineNumber, other.lineNumber) + && Objects.equals(this.sourceCodeLocation.getLineNumber(), other.sourceCodeLocation.getLineNumber()) && Objects.equals(this.description, other.description); } @@ -299,7 +318,7 @@ public String toString() { return MoreObjects.toStringHelper(this) .add("originClass", originClass) .add("targetClass", targetClass) - .add("lineNumber", lineNumber) + .add("sourceCodeLocation", sourceCodeLocation) .add("description", description) .toString(); } @@ -313,6 +332,49 @@ public static JavaClasses toTargetClasses(Iterable dependencies) { return JavaClasses.of(classes); } + private static class FromAccess extends Dependency { + private final JavaAccess access; + + FromAccess(JavaAccess access) { + super(access.getOriginOwner(), access.getTargetOwner(), access.getSourceCodeLocation(), access.getDescription()); + this.access = access; + } + + @Override + @SuppressWarnings("unchecked") // compatibility is explicitly checked + public Set convertTo(Class type) { + if (type.isAssignableFrom(getClass())) { + return (Set) singleton(this); + } + if (type.isAssignableFrom(access.getClass())) { + return (Set) singleton(access); + } + return super.convertTo(type); + } + + @Override + public int hashCode() { + return access.hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + final FromAccess other = (FromAccess) obj; + return Objects.equals(this.access, other.access); + } + + @Override + public String toString() { + return getClass().getEnclosingClass().getSimpleName() + "." + super.toString(); + } + } + private static class Origin implements HasOwner, HasDescription { private final JavaClass originClass; private final String originDescription; @@ -336,6 +398,7 @@ public String getDescription() { /** * Predefined {@link DescribedPredicate predicates} targeting {@link Dependency}. */ + @PublicAPI(usage = ACCESS) public static final class Predicates { private Predicates() { } @@ -370,7 +433,7 @@ public static DescribedPredicate dependencyOrigin(String className) } @PublicAPI(usage = ACCESS) - public static DescribedPredicate dependencyOrigin(final DescribedPredicate predicate) { + public static DescribedPredicate dependencyOrigin(DescribedPredicate predicate) { return Functions.GET_ORIGIN_CLASS.is(predicate).as("origin " + predicate.getDescription()); } @@ -385,7 +448,7 @@ public static DescribedPredicate dependencyTarget(String className) } @PublicAPI(usage = ACCESS) - public static DescribedPredicate dependencyTarget(final DescribedPredicate predicate) { + public static DescribedPredicate dependencyTarget(DescribedPredicate predicate) { return Functions.GET_TARGET_CLASS.is(predicate).as("target " + predicate.getDescription()); } } @@ -393,6 +456,7 @@ public static DescribedPredicate dependencyTarget(final DescribedPre /** * Predefined {@link ChainableFunction functions} to transform {@link Dependency}. */ + @PublicAPI(usage = ACCESS) public static final class Functions { private Functions() { } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/DomainObjectCreationContext.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/DomainObjectCreationContext.java index 11cf906c86..f8cbdb9ce9 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/DomainObjectCreationContext.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/DomainObjectCreationContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -172,16 +172,16 @@ public static Source createSource(URI uri, Optional sourceFileName, bool return new Source(uri, sourceFileName, md5InClassSourcesEnabled); } - public static ReferencedClassObject createReferencedClassObject(JavaCodeUnit codeUnit, JavaClass javaClass, int lineNumber) { - return ReferencedClassObject.from(codeUnit, javaClass, lineNumber); + public static ReferencedClassObject createReferencedClassObject(JavaCodeUnit codeUnit, JavaClass javaClass, int lineNumber, boolean declaredInLambda) { + return ReferencedClassObject.from(codeUnit, javaClass, lineNumber, declaredInLambda); } public static ThrowsClause createThrowsClause(CODE_UNIT codeUnit, List types) { return ThrowsClause.from(codeUnit, types); } - public static InstanceofCheck createInstanceofCheck(JavaCodeUnit codeUnit, JavaClass target, int lineNumber) { - return InstanceofCheck.from(codeUnit, target, lineNumber); + public static InstanceofCheck createInstanceofCheck(JavaCodeUnit codeUnit, JavaClass type, int lineNumber, boolean declaredInLambda) { + return InstanceofCheck.from(codeUnit, type, lineNumber, declaredInLambda); } public static JavaTypeVariable createTypeVariable(String name, OWNER owner, JavaClass erasure) { diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/DomainPlugin.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/DomainPlugin.java index f9cfac31e9..d488226b32 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/DomainPlugin.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/DomainPlugin.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -20,10 +20,11 @@ import com.tngtech.archunit.core.PluginLoader; import static com.tngtech.archunit.core.PluginLoader.JavaVersion.JAVA_14; +import static com.tngtech.archunit.core.PluginLoader.JavaVersion.JAVA_21; import static com.tngtech.archunit.core.PluginLoader.JavaVersion.JAVA_9; interface DomainPlugin { - void plugInAnnotationPropertiesFormatter(InitialConfiguration valueFormatter); + void plugInAnnotationFormatter(InitialConfiguration annotationFormatter); @Internal class Loader { @@ -31,6 +32,7 @@ class Loader { .forType(DomainPlugin.class) .ifVersionGreaterOrEqualTo(JAVA_9).load("com.tngtech.archunit.core.domain.Java9DomainPlugin") .ifVersionGreaterOrEqualTo(JAVA_14).load("com.tngtech.archunit.core.domain.Java14DomainPlugin") + .ifVersionGreaterOrEqualTo(JAVA_21).load("com.tngtech.archunit.core.domain.Java21DomainPlugin") .fallback(new LegacyDomainPlugin()); static DomainPlugin loadForCurrentPlatform() { @@ -39,11 +41,13 @@ static DomainPlugin loadForCurrentPlatform() { private static class LegacyDomainPlugin implements DomainPlugin { @Override - public void plugInAnnotationPropertiesFormatter(InitialConfiguration valueFormatter) { - valueFormatter.set(AnnotationPropertiesFormatter.configure() - .formattingArraysWithSquareBrackets() - .formattingTypesToString() - .build()); + public void plugInAnnotationFormatter(InitialConfiguration annotationFormatter) { + annotationFormatter.set( + AnnotationFormatter + .formatAnnotationType(JavaClass::getName) + .formatProperties(config -> config + .formattingArraysWithSquareBrackets() + .formattingTypesToString())); } } } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/Formatters.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/Formatters.java index 2971d619c0..7e846e04a0 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/Formatters.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/Formatters.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -30,6 +30,7 @@ import static java.util.stream.Collectors.joining; import static java.util.stream.Collectors.toList; +@PublicAPI(usage = ACCESS) public final class Formatters { private Formatters() { } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/ImportContext.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/ImportContext.java index 5b4aa1c14b..9137142cb0 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/ImportContext.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/ImportContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -63,5 +63,9 @@ public interface ImportContext { Set createTryCatchBlockBuilders(JavaCodeUnit codeUnit); + Set createReferencedClassObjectsFor(JavaCodeUnit codeUnit); + + Set createInstanceofChecksFor(JavaCodeUnit codeUnit); + JavaClass resolveClass(String fullyQualifiedClassName); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/InstanceofCheck.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/InstanceofCheck.java index 61484a9944..9e3ae2ab56 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/InstanceofCheck.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/InstanceofCheck.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -24,30 +24,31 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +@PublicAPI(usage = ACCESS) public final class InstanceofCheck implements HasType, HasOwner, HasSourceCodeLocation { private final JavaCodeUnit owner; - private final JavaClass target; - private final int lineNumber; + private final JavaClass type; + private final boolean declaredInLambda; private final SourceCodeLocation sourceCodeLocation; - private InstanceofCheck(JavaCodeUnit owner, JavaClass target, int lineNumber) { + private InstanceofCheck(JavaCodeUnit owner, JavaClass type, int lineNumber, boolean declaredInLambda) { this.owner = checkNotNull(owner); - this.target = checkNotNull(target); - this.lineNumber = lineNumber; + this.type = checkNotNull(type); + this.declaredInLambda = declaredInLambda; sourceCodeLocation = SourceCodeLocation.of(owner.getOwner(), lineNumber); } @Override @PublicAPI(usage = ACCESS) public JavaClass getRawType() { - return target; + return type; } @Override @PublicAPI(usage = ACCESS) public JavaType getType() { - return target; + return type; } @Override @@ -58,7 +59,12 @@ public JavaCodeUnit getOwner() { @PublicAPI(usage = ACCESS) public int getLineNumber() { - return lineNumber; + return sourceCodeLocation.getLineNumber(); + } + + @PublicAPI(usage = ACCESS) + public boolean isDeclaredInLambda() { + return declaredInLambda; } @Override @@ -70,12 +76,13 @@ public SourceCodeLocation getSourceCodeLocation() { public String toString() { return toStringHelper(this) .add("owner", owner) - .add("target", target) - .add("lineNumber", lineNumber) + .add("type", type) + .add("sourceCodeLocation", sourceCodeLocation) + .add("declaredInLambda", declaredInLambda) .toString(); } - static InstanceofCheck from(JavaCodeUnit owner, JavaClass target, int lineNumber) { - return new InstanceofCheck(owner, target, lineNumber); + static InstanceofCheck from(JavaCodeUnit owner, JavaClass type, int lineNumber, boolean declaredInLambda) { + return new InstanceofCheck(owner, type, lineNumber, declaredInLambda); } } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/Java14DomainPlugin.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/Java14DomainPlugin.java index b9c4dc3550..b094b31da8 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/Java14DomainPlugin.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/Java14DomainPlugin.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -15,7 +15,6 @@ */ package com.tngtech.archunit.core.domain; -import com.tngtech.archunit.Internal; import com.tngtech.archunit.core.InitialConfiguration; import com.tngtech.archunit.core.PluginLoader; @@ -23,15 +22,17 @@ * Resolved via {@link PluginLoader} */ @SuppressWarnings("unused") -@Internal -public class Java14DomainPlugin implements DomainPlugin { +class Java14DomainPlugin implements DomainPlugin { @Override - public void plugInAnnotationPropertiesFormatter(InitialConfiguration propertiesFormatter) { - propertiesFormatter.set(AnnotationPropertiesFormatter.configure() - .formattingArraysWithCurlyBrackets() - .formattingTypesAsClassNames() - .quotingStrings() - .omitOptionalIdentifierForSingleElementAnnotations() - .build()); + public void plugInAnnotationFormatter(InitialConfiguration propertiesFormatter) { + propertiesFormatter.set( + AnnotationFormatter + .formatAnnotationType(JavaClass::getName) + .formatProperties(config -> config + .formattingArraysWithCurlyBrackets() + .formattingTypesAsClassNames() + .quotingStrings() + .omitOptionalIdentifierForSingleElementAnnotations() + )); } } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/Java21DomainPlugin.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/Java21DomainPlugin.java new file mode 100644 index 0000000000..279b33ef66 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/Java21DomainPlugin.java @@ -0,0 +1,38 @@ +/* + * Copyright 2014-2025 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.core.domain; + +import com.tngtech.archunit.core.InitialConfiguration; +import com.tngtech.archunit.core.PluginLoader; + +/** + * Resolved via {@link PluginLoader} + */ +@SuppressWarnings("unused") +class Java21DomainPlugin implements DomainPlugin { + @Override + public void plugInAnnotationFormatter(InitialConfiguration propertiesFormatter) { + propertiesFormatter.set( + AnnotationFormatter + .formatAnnotationType(javaClass -> javaClass.getName().replace("$", ".")) + .formatProperties(config -> config + .formattingArraysWithCurlyBrackets() + .formattingTypesAsClassNames() + .quotingStrings() + .omitOptionalIdentifierForSingleElementAnnotations() + )); + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaAccess.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaAccess.java index 91df9b01f0..fffb680bef 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaAccess.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaAccess.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -15,12 +15,14 @@ */ package com.tngtech.archunit.core.domain; +import java.util.Collections; import java.util.Set; import com.tngtech.archunit.PublicAPI; import com.tngtech.archunit.base.ChainableFunction; import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.base.HasDescription; +import com.tngtech.archunit.core.Convertible; import com.tngtech.archunit.core.domain.properties.HasName; import com.tngtech.archunit.core.domain.properties.HasOwner; import com.tngtech.archunit.core.domain.properties.HasOwner.Functions.Get; @@ -31,20 +33,19 @@ import static com.google.common.collect.ImmutableSet.toImmutableSet; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +@PublicAPI(usage = ACCESS) public abstract class JavaAccess - implements HasName, HasDescription, HasOwner, HasSourceCodeLocation { + implements HasName, HasDescription, HasOwner, HasSourceCodeLocation, Convertible { private final JavaCodeUnit origin; private final TARGET target; - private final int lineNumber; private final SourceCodeLocation sourceCodeLocation; private final boolean declaredInLambda; JavaAccess(JavaAccessBuilder builder) { this.origin = checkNotNull(builder.getOrigin()); this.target = checkNotNull(builder.getTarget()); - this.lineNumber = builder.getLineNumber(); - this.sourceCodeLocation = SourceCodeLocation.of(getOriginOwner(), lineNumber); + this.sourceCodeLocation = SourceCodeLocation.of(getOriginOwner(), builder.getLineNumber()); this.declaredInLambda = builder.isDeclaredInLambda(); } @@ -76,7 +77,7 @@ public TARGET getTarget() { @PublicAPI(usage = ACCESS) public int getLineNumber() { - return lineNumber; + return sourceCodeLocation.getLineNumber(); } @Override @@ -96,10 +97,22 @@ public boolean isDeclaredInLambda() { return declaredInLambda; } + @Override + @SuppressWarnings("unchecked") // compatibility is explicitly checked + public Set convertTo(Class type) { + if (type.isAssignableFrom(getClass())) { + return (Set) Collections.singleton(this); + } + if (type.isAssignableFrom(Dependency.class)) { + return (Set) Dependency.tryCreateFromAccess(this); + } + return Collections.emptySet(); + } + @Override public String toString() { return getClass().getSimpleName() + - "{origin=" + origin + ", target=" + target + ", lineNumber=" + lineNumber + additionalToStringFields() + '}'; + "{origin=" + origin + ", target=" + target + ", lineNumber=" + getLineNumber() + additionalToStringFields() + '}'; } String additionalToStringFields() { @@ -133,6 +146,7 @@ public Set getContainingTryBlocks() { *
  • {@link HasOwner.Predicates}
  • * */ + @PublicAPI(usage = ACCESS) public static final class Predicates { private Predicates() { } @@ -153,12 +167,12 @@ public static DescribedPredicate> originOwnerEqualsTargetOwner() { } @PublicAPI(usage = ACCESS) - public static DescribedPredicate> targetOwner(final DescribedPredicate predicate) { + public static DescribedPredicate> targetOwner(DescribedPredicate predicate) { return target(Get.owner().is(predicate)); } @PublicAPI(usage = ACCESS) - public static DescribedPredicate> target(final DescribedPredicate predicate) { + public static DescribedPredicate> target(DescribedPredicate predicate) { return new TargetPredicate<>(predicate); } @@ -202,6 +216,7 @@ public static final class Functions { private Functions() { } + @PublicAPI(usage = ACCESS) public static final class Get { private Get() { } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaAnnotation.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaAnnotation.java index 9fc61870ea..75588b99bf 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaAnnotation.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaAnnotation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -75,6 +75,7 @@ * is annotated on a class or member, it is that class or member. If this * annotation is a member of another annotation, it is that annotation. */ +@PublicAPI(usage = ACCESS) public final class JavaAnnotation implements HasType, HasOwner, HasDescription { private final JavaClass type; private final OWNER owner; diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaAnnotationParameterVisitorAcceptor.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaAnnotationParameterVisitorAcceptor.java index c654359197..6285ab535a 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaAnnotationParameterVisitorAcceptor.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaAnnotationParameterVisitorAcceptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaCall.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaCall.java index 3b55246b1b..da31d0a3b9 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaCall.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaCall.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -23,6 +23,7 @@ import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +@PublicAPI(usage = ACCESS) public abstract class JavaCall extends JavaCodeUnitAccess { JavaCall(JavaAccessBuilder builder) { super(builder); @@ -32,12 +33,13 @@ public abstract class JavaCall extends JavaCodeUni * Predefined {@link DescribedPredicate predicates} targeting {@link JavaCall}. * Further predicates to be used with {@link JavaCall} can be found at {@link JavaCodeUnitAccess.Predicates}. */ + @PublicAPI(usage = ACCESS) public static final class Predicates { private Predicates() { } @PublicAPI(usage = ACCESS) - public static DescribedPredicate> target(final DescribedPredicate predicate) { + public static DescribedPredicate> target(DescribedPredicate predicate) { return new TargetPredicate<>(predicate); } } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClass.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClass.java index 4f4efafea8..e4480a4dae 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClass.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClass.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -45,6 +45,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.Sets.immutableEnumSet; import static com.google.common.collect.Sets.union; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; import static com.tngtech.archunit.base.ClassLoaders.getCurrentClassLoader; @@ -69,7 +70,8 @@ import static java.util.Collections.emptyMap; import static java.util.stream.Collectors.toSet; -public class JavaClass +@PublicAPI(usage = ACCESS) +public final class JavaClass implements JavaType, HasName.AndFullName, HasTypeParameters, HasAnnotations, HasModifiers, HasSourceCodeLocation { private final Optional source; @@ -137,7 +139,7 @@ public class JavaClass isRecord = builder.isRecord(); isAnonymousClass = builder.isAnonymousClass(); isMemberClass = builder.isMemberClass(); - modifiers = checkNotNull(builder.getModifiers()); + modifiers = immutableEnumSet(builder.getModifiers()); reflectSupplier = Suppliers.memoize(new ReflectClassSupplier()); sourceCodeLocation = SourceCodeLocation.of(this); javaPackage = JavaPackage.simple(this); @@ -655,6 +657,11 @@ public JavaClass toErasure() { return this; } + @Override + public void traverseSignature(SignatureVisitor visitor) { + SignatureTraversal.from(visitor).visitClass(this); + } + @PublicAPI(usage = ACCESS) public Optional getRawSuperclass() { return superclass.getRaw(); @@ -1387,7 +1394,7 @@ public boolean isAssignableTo(Class type) { } @PublicAPI(usage = ACCESS) - public boolean isAssignableTo(final String typeName) { + public boolean isAssignableTo(String typeName) { return isAssignableTo(GET_NAME.is(equalTo(typeName))); } @@ -1466,12 +1473,12 @@ void completeGenericInterfacesFrom(ImportContext context) { completionProcess.markGenericInterfacesComplete(); } - void completeMembers(final ImportContext context) { + void completeMembers(ImportContext context) { members = JavaClassMembers.create(this, context); completionProcess.markMembersComplete(); } - void completeAnnotations(final ImportContext context) { + void completeAnnotations(ImportContext context) { annotations = context.createAnnotations(this); members.completeAnnotations(context); completionProcess.markAnnotationsComplete(); @@ -1479,7 +1486,7 @@ void completeAnnotations(final ImportContext context) { JavaClassDependencies completeFrom(ImportContext context) { completeComponentType(context); - members.completeAccessesFrom(context); + members.completeFrom(context); javaClassDependencies = new JavaClassDependencies(this); return javaClassDependencies; } @@ -1757,6 +1764,7 @@ public void markDependenciesComplete() { *
  • {@link JavaType.Functions}
  • * */ + @PublicAPI(usage = ACCESS) public static final class Functions { private Functions() { } @@ -2054,6 +2062,7 @@ public Set apply(JavaClass input) { *
  • {@link CanBeAnnotated.Predicates}
  • * */ + @PublicAPI(usage = ACCESS) public static final class Predicates { private Predicates() { } @@ -2139,27 +2148,27 @@ public boolean test(JavaClass input) { }; @PublicAPI(usage = ACCESS) - public static DescribedPredicate type(final Class type) { + public static DescribedPredicate type(Class type) { return equalTo(type.getName()).onResultOf(GET_NAME).as("type " + type.getName()); } @PublicAPI(usage = ACCESS) - public static DescribedPredicate simpleName(final String name) { + public static DescribedPredicate simpleName(String name) { return equalTo(name).onResultOf(GET_SIMPLE_NAME).as("simple name '%s'", name); } @PublicAPI(usage = ACCESS) - public static DescribedPredicate simpleNameStartingWith(final String prefix) { + public static DescribedPredicate simpleNameStartingWith(String prefix) { return new SimpleNameStartingWithPredicate(prefix); } @PublicAPI(usage = ACCESS) - public static DescribedPredicate simpleNameContaining(final String infix) { + public static DescribedPredicate simpleNameContaining(String infix) { return new SimpleNameContainingPredicate(infix); } @PublicAPI(usage = ACCESS) - public static DescribedPredicate simpleNameEndingWith(final String suffix) { + public static DescribedPredicate simpleNameEndingWith(String suffix) { return new SimpleNameEndingWithPredicate(suffix); } @@ -2176,7 +2185,7 @@ public static DescribedPredicate simpleNameEndingWith(final String su * @see #assignableFrom(Class) */ @PublicAPI(usage = ACCESS) - public static DescribedPredicate assignableTo(final Class type) { + public static DescribedPredicate assignableTo(Class type) { return assignableTo(type.getName()); } @@ -2193,7 +2202,7 @@ public static DescribedPredicate assignableTo(final Class type) { * @see #assignableTo(Class) */ @PublicAPI(usage = ACCESS) - public static DescribedPredicate assignableFrom(final Class type) { + public static DescribedPredicate assignableFrom(Class type) { return assignableFrom(type.getName()); } @@ -2201,7 +2210,7 @@ public static DescribedPredicate assignableFrom(final Class type) * Same as {@link #assignableTo(Class)} but takes a fully qualified class name as an argument instead of a class object. */ @PublicAPI(usage = ACCESS) - public static DescribedPredicate assignableTo(final String typeName) { + public static DescribedPredicate assignableTo(String typeName) { return assignableTo(GET_NAME.is(equalTo(typeName)).as(typeName)); } @@ -2209,7 +2218,7 @@ public static DescribedPredicate assignableTo(final String typeName) * Same as {@link #assignableFrom(Class)} but takes a fully qualified class name as an argument instead of a class object. */ @PublicAPI(usage = ACCESS) - public static DescribedPredicate assignableFrom(final String typeName) { + public static DescribedPredicate assignableFrom(String typeName) { return assignableFrom(GET_NAME.is(equalTo(typeName)).as(typeName)); } @@ -2224,7 +2233,7 @@ public static DescribedPredicate assignableFrom(final String typeName * @see #assignableFrom(DescribedPredicate) */ @PublicAPI(usage = ACCESS) - public static DescribedPredicate assignableTo(final DescribedPredicate predicate) { + public static DescribedPredicate assignableTo(DescribedPredicate predicate) { return new AssignableToPredicate(predicate); } @@ -2232,14 +2241,14 @@ public static DescribedPredicate assignableTo(final DescribedPredicat * Same as {@link #assignableFrom(Class)}, but returns {@code true} whenever the tested {@link JavaClass} * is assignable from a class that matches the supplied predicate.
    * This is the opposite of {@link #assignableTo(DescribedPredicate)}: - * some class {@code B} is assignable from a class {@code A} if and only if {@code A} is assignable to {@code A}. + * some class {@code B} is assignable from a class {@code A} if and only if {@code A} is assignable to {@code B}. * * @see #assignableFrom(Class) * @see #assignableFrom(String) * @see #assignableTo(DescribedPredicate) */ @PublicAPI(usage = ACCESS) - public static DescribedPredicate assignableFrom(final DescribedPredicate predicate) { + public static DescribedPredicate assignableFrom(DescribedPredicate predicate) { return new AssignableFromPredicate(predicate); } @@ -2256,7 +2265,7 @@ public static DescribedPredicate assignableFrom(final DescribedPredic * @see #assignableTo(Class) */ @PublicAPI(usage = ACCESS) - public static DescribedPredicate implement(final Class type) { + public static DescribedPredicate implement(Class type) { if (!type.isInterface()) { throw new InvalidSyntaxUsageException(String.format( "implement(type) can only ever be true, if type is an interface, but type %s is not. " @@ -2269,7 +2278,7 @@ public static DescribedPredicate implement(final Class type) { * Same as {@link #implement(Class)} but takes a fully qualified class name as an argument instead of a class object. */ @PublicAPI(usage = ACCESS) - public static DescribedPredicate implement(final String typeName) { + public static DescribedPredicate implement(String typeName) { return implement(GET_NAME.is(equalTo(typeName)).as(typeName)); } @@ -2282,7 +2291,7 @@ public static DescribedPredicate implement(final String typeName) { * @see #assignableTo(DescribedPredicate) */ @PublicAPI(usage = ACCESS) - public static DescribedPredicate implement(final DescribedPredicate predicate) { + public static DescribedPredicate implement(DescribedPredicate predicate) { DescribedPredicate selfIsImplementation = not(INTERFACES); DescribedPredicate interfacePredicate = predicate.forSubtype().and(INTERFACES); return selfIsImplementation.and(assignableTo(interfacePredicate)) @@ -2299,7 +2308,7 @@ public static DescribedPredicate implement(final DescribedPredicate resideInAPackage(final String packageIdentifier) { + public static DescribedPredicate resideInAPackage(String packageIdentifier) { return resideInAnyPackage(new String[]{packageIdentifier}, String.format("reside in a package '%s'", packageIdentifier)); } @@ -2308,13 +2317,13 @@ public static DescribedPredicate resideInAPackage(final String packag * @see #resideInAPackage(String) */ @PublicAPI(usage = ACCESS) - public static DescribedPredicate resideInAnyPackage(final String... packageIdentifiers) { + public static DescribedPredicate resideInAnyPackage(String... packageIdentifiers) { return resideInAnyPackage(packageIdentifiers, String.format("reside in any package [%s]", joinSingleQuoted(packageIdentifiers))); } - private static DescribedPredicate resideInAnyPackage(final String[] packageIdentifiers, final String description) { - final Set packageMatchers = stream(packageIdentifiers).map(PackageMatcher::of).collect(toSet()); + private static DescribedPredicate resideInAnyPackage(String[] packageIdentifiers, String description) { + Set packageMatchers = stream(packageIdentifiers).map(PackageMatcher::of).collect(toSet()); return new PackageMatchesPredicate(packageMatchers, description); } @@ -2334,7 +2343,7 @@ public static DescribedPredicate resideOutsideOfPackages(String... pa * @see JavaClass#isEquivalentTo(Class) */ @PublicAPI(usage = ACCESS) - public static DescribedPredicate equivalentTo(final Class clazz) { + public static DescribedPredicate equivalentTo(Class clazz) { return new EquivalentToPredicate(clazz); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassDependencies.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassDependencies.java index f359087131..35e29d4aa7 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassDependencies.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassDependencies.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -247,8 +247,8 @@ private > Stream annota } private > Stream annotationDependencies(T annotated) { - final Stream.Builder addToStream = Stream.builder(); - for (final JavaAnnotation annotation : annotated.getAnnotations()) { + Stream.Builder addToStream = Stream.builder(); + for (JavaAnnotation annotation : annotated.getAnnotations()) { Dependency.tryCreateFromAnnotation(annotation).forEach(addToStream); annotation.accept(new DefaultParameterVisitor() { @Override diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassDescriptor.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassDescriptor.java index d3ea8454b0..b79c1d77b6 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassDescriptor.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -200,7 +200,7 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } - final JavaClassDescriptor other = (JavaClassDescriptor) obj; + JavaClassDescriptor other = (JavaClassDescriptor) obj; return Objects.equals(this.getFullyQualifiedClassName(), other.getFullyQualifiedClassName()); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassMembers.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassMembers.java index 21f6e57d30..1a5e1bcbbf 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassMembers.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassMembers.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -55,7 +55,7 @@ class JavaClassMembers { .addAll(getAllConstructors()) .build()); - JavaClassMembers(final JavaClass owner, Set fields, Set methods, Set constructors, Optional staticInitializer) { + JavaClassMembers(JavaClass owner, Set fields, Set methods, Set constructors, Optional staticInitializer) { this.owner = owner; this.fields = fields; this.methods = methods; @@ -318,9 +318,9 @@ void completeAnnotations(ImportContext context) { } } - void completeAccessesFrom(ImportContext context) { + void completeFrom(ImportContext context) { for (JavaCodeUnit codeUnit : codeUnits) { - codeUnit.completeAccessesFrom(context); + codeUnit.completeFrom(context); } } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassTransitiveDependencies.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassTransitiveDependencies.java index 3cf439d6b7..88c282224e 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassTransitiveDependencies.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClassTransitiveDependencies.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClasses.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClasses.java index 10b64e3533..2a1b9dc311 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClasses.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaClasses.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -33,6 +33,7 @@ import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; import static java.util.stream.Collectors.toMap; +@PublicAPI(usage = ACCESS) public final class JavaClasses extends ForwardingCollection implements DescribedIterable, CanOverrideDescription { private final ImmutableMap classes; private final JavaPackage defaultPackage; @@ -163,7 +164,7 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } - final JavaClasses other = (JavaClasses) obj; + JavaClasses other = (JavaClasses) obj; return Objects.equals(this.classes.keySet(), other.classes.keySet()) && Objects.equals(this.description, other.description); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaCodeUnit.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaCodeUnit.java index bfc3d44021..439067d16f 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaCodeUnit.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaCodeUnit.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -20,6 +20,7 @@ import java.util.List; import java.util.Optional; import java.util.Set; +import java.util.stream.Stream; import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableSet; @@ -43,6 +44,7 @@ import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; import static com.tngtech.archunit.core.domain.Formatters.formatMethod; import static com.tngtech.archunit.core.domain.properties.HasName.Utils.namesOf; +import static java.util.stream.Collectors.toSet; /** * Represents a unit of code containing accesses to other units of code. A unit of code can be @@ -54,6 +56,7 @@ * in particular every place, where Java code with behavior, like calling other methods or accessing fields, can * be defined. */ +@PublicAPI(usage = ACCESS) public abstract class JavaCodeUnit extends JavaMember implements HasParameterTypes, HasReturnType, HasTypeParameters, HasThrowsClause { @@ -62,8 +65,6 @@ public abstract class JavaCodeUnit private final Parameters parameters; private final String fullName; private final List> typeParameters; - private final Set referencedClassObjects; - private final Set instanceofChecks; private Set fieldAccesses = Collections.emptySet(); private Set methodCalls = Collections.emptySet(); @@ -71,6 +72,8 @@ public abstract class JavaCodeUnit private Set methodReferences = Collections.emptySet(); private Set constructorReferences = Collections.emptySet(); private Set tryCatchBlocks = Collections.emptySet(); + private Set referencedClassObjects; + private Set instanceofChecks; JavaCodeUnit(JavaCodeUnitBuilder builder) { super(builder); @@ -78,8 +81,6 @@ public abstract class JavaCodeUnit returnType = new ReturnType(this, builder); parameters = new Parameters(this, builder); fullName = formatMethod(getOwner().getName(), getName(), namesOf(getRawParameterTypes())); - referencedClassObjects = ImmutableSet.copyOf(builder.getReferencedClassObjects(this)); - instanceofChecks = ImmutableSet.copyOf(builder.getInstanceofChecks(this)); } /** @@ -170,6 +171,22 @@ public JavaClass getRawReturnType() { return returnType.getRaw(); } + /** + * @return All raw types involved in this code unit's signature, + * which is the union of all raw types involved in the {@link #getReturnType() return type}, + * the {@link #getParameterTypes() parameter types} and the {@link #getTypeParameters() type parameters} of this code unit. + * For a definition of "all raw types involved" consult {@link JavaType#getAllInvolvedRawTypes()}. + */ + @Override + @PublicAPI(usage = ACCESS) + public Set getAllInvolvedRawTypes() { + return Stream.of( + Stream.of(this.returnType.get()), + this.parameters.getParameterTypes().stream(), + this.typeParameters.stream() + ).flatMap(s -> s).map(JavaType::getAllInvolvedRawTypes).flatMap(Set::stream).collect(toSet()); + } + @PublicAPI(usage = ACCESS) public Set getFieldAccesses() { return fieldAccesses; @@ -270,7 +287,7 @@ public List>> getParameterAnnotations() { return parameters.getAnnotations(); } - void completeAccessesFrom(ImportContext context) { + void completeFrom(ImportContext context) { Set tryCatchBlockBuilders = context.createTryCatchBlockBuilders(this); fieldAccesses = context.createFieldAccessesFor(this, tryCatchBlockBuilders); methodCalls = context.createMethodCallsFor(this, tryCatchBlockBuilders); @@ -278,8 +295,10 @@ void completeAccessesFrom(ImportContext context) { methodReferences = context.createMethodReferencesFor(this, tryCatchBlockBuilders); constructorReferences = context.createConstructorReferencesFor(this, tryCatchBlockBuilders); tryCatchBlocks = tryCatchBlockBuilders.stream() - .map(builder -> builder.build(this, context)) + .map(builder -> builder.build(this)) .collect(toImmutableSet()); + referencedClassObjects = context.createReferencedClassObjectsFor(this); + instanceofChecks = context.createInstanceofChecksFor(this); } @ResolvesTypesViaReflection @@ -368,6 +387,7 @@ JavaType get() { *
  • {@link HasThrowsClause.Predicates}
  • * */ + @PublicAPI(usage = ACCESS) public static final class Predicates { private Predicates() { } @@ -408,6 +428,7 @@ public static final class Functions { private Functions() { } + @PublicAPI(usage = ACCESS) public static final class Get { private Get() { } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaCodeUnitAccess.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaCodeUnitAccess.java index a44a806605..062e696a0f 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaCodeUnitAccess.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaCodeUnitAccess.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -23,6 +23,7 @@ import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +@PublicAPI(usage = ACCESS) public abstract class JavaCodeUnitAccess extends JavaAccess { JavaCodeUnitAccess(JavaAccessBuilder builder) { super(builder); @@ -32,12 +33,13 @@ public abstract class JavaCodeUnitAccess extends * Predefined {@link DescribedPredicate predicates} targeting {@link JavaCodeUnitAccess}. * Further predicates to be used with {@link JavaCodeUnitAccess} can be found at {@link JavaAccess.Predicates}. */ + @PublicAPI(usage = ACCESS) public static final class Predicates { private Predicates() { } @PublicAPI(usage = ACCESS) - public static DescribedPredicate> target(final DescribedPredicate predicate) { + public static DescribedPredicate> target(DescribedPredicate predicate) { return new TargetPredicate<>(predicate); } } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaCodeUnitReference.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaCodeUnitReference.java index 7b41d82682..77fee3137f 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaCodeUnitReference.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaCodeUnitReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -23,6 +23,7 @@ import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +@PublicAPI(usage = ACCESS) public abstract class JavaCodeUnitReference extends JavaCodeUnitAccess { JavaCodeUnitReference(JavaAccessBuilder builder) { super(builder); @@ -32,12 +33,13 @@ public abstract class JavaCodeUnitReference e * Predefined {@link DescribedPredicate predicates} targeting {@link JavaCodeUnitReference}. * Further predicates to be used with {@link JavaCodeUnitReference} can be found at {@link JavaCodeUnitAccess.Predicates}. */ + @PublicAPI(usage = ACCESS) public static final class Predicates { private Predicates() { } @PublicAPI(usage = ACCESS) - public static DescribedPredicate> target(final DescribedPredicate predicate) { + public static DescribedPredicate> target(DescribedPredicate predicate) { return new TargetPredicate<>(predicate); } } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaConstructor.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaConstructor.java index 0652e8c903..2bc599a045 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaConstructor.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaConstructor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -33,6 +33,7 @@ import static com.tngtech.archunit.core.domain.Formatters.formatMethod; import static com.tngtech.archunit.core.domain.properties.HasName.Utils.namesOf; +@PublicAPI(usage = ACCESS) public final class JavaConstructor extends JavaCodeUnit { private final Supplier> constructorSupplier; private final ThrowsClause throwsClause; diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaConstructorCall.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaConstructorCall.java index 10425af259..1a7b908ca8 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaConstructorCall.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaConstructorCall.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -15,10 +15,14 @@ */ package com.tngtech.archunit.core.domain; +import com.tngtech.archunit.PublicAPI; import com.tngtech.archunit.core.domain.AccessTarget.ConstructorCallTarget; import com.tngtech.archunit.core.importer.DomainBuilders.JavaConstructorCallBuilder; -public class JavaConstructorCall extends JavaCall { +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; + +@PublicAPI(usage = ACCESS) +public final class JavaConstructorCall extends JavaCall { JavaConstructorCall(JavaConstructorCallBuilder builder) { super(builder); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaConstructorReference.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaConstructorReference.java index 2c5e08a438..cd10226d64 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaConstructorReference.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaConstructorReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -15,10 +15,14 @@ */ package com.tngtech.archunit.core.domain; +import com.tngtech.archunit.PublicAPI; import com.tngtech.archunit.core.domain.AccessTarget.ConstructorReferenceTarget; import com.tngtech.archunit.core.importer.DomainBuilders.JavaConstructorReferenceBuilder; -public class JavaConstructorReference extends JavaCodeUnitReference { +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; + +@PublicAPI(usage = ACCESS) +public final class JavaConstructorReference extends JavaCodeUnitReference { JavaConstructorReference(JavaConstructorReferenceBuilder builder) { super(builder); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaEnumConstant.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaEnumConstant.java index fea3825032..57c80905b0 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaEnumConstant.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaEnumConstant.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -23,6 +23,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +@PublicAPI(usage = ACCESS) public final class JavaEnumConstant { private final JavaClass declaringClass; private final String name; @@ -59,7 +60,7 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } - final JavaEnumConstant other = (JavaEnumConstant) obj; + JavaEnumConstant other = (JavaEnumConstant) obj; return Objects.equals(this.declaringClass, other.declaringClass) && Objects.equals(this.name, other.name); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaField.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaField.java index a8ee77198f..b87ccfbf53 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaField.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaField.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -30,7 +30,8 @@ import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; -public class JavaField extends JavaMember implements HasType { +@PublicAPI(usage = ACCESS) +public final class JavaField extends JavaMember implements HasType { private final JavaType type; private final Supplier fieldSupplier; @@ -49,10 +50,6 @@ public String getFullName() { return getOwner().getName() + "." + getName(); } - /** - * Note: This is still work in progress and thus does not support generic types at the moment. - * In the future the result can possibly also be a {@link JavaParameterizedType} or {@link JavaTypeVariable} - */ @Override @PublicAPI(usage = ACCESS) public JavaType getType() { @@ -65,6 +62,15 @@ public JavaClass getRawType() { return type.toErasure(); } + /** + * @return All raw types involved in this field's signature, which is equivalent to {@link #getType()}.{@link JavaType#getAllInvolvedRawTypes() getAllInvolvedRawTypes()}. + */ + @Override + @PublicAPI(usage = ACCESS) + public Set getAllInvolvedRawTypes() { + return getType().getAllInvolvedRawTypes(); + } + @Override @PublicAPI(usage = ACCESS) public Set getAccessesToSelf() { diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaFieldAccess.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaFieldAccess.java index 2cbb1245c6..b18dde9209 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaFieldAccess.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaFieldAccess.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -30,7 +30,8 @@ import static com.tngtech.archunit.core.domain.JavaFieldAccess.AccessType.GET; import static com.tngtech.archunit.core.domain.JavaFieldAccess.AccessType.SET; -public class JavaFieldAccess extends JavaAccess { +@PublicAPI(usage = ACCESS) +public final class JavaFieldAccess extends JavaAccess { private static final Map MESSAGE_VERB = ImmutableMap.of( GET, "gets", SET, "sets"); @@ -57,6 +58,7 @@ protected String descriptionVerb() { return MESSAGE_VERB.get(accessType); } + @PublicAPI(usage = ACCESS) public enum AccessType { @PublicAPI(usage = ACCESS) GET(Opcodes.GETFIELD | Opcodes.GETSTATIC), @@ -85,17 +87,18 @@ public static AccessType forOpCode(int opCode) { * Predefined {@link DescribedPredicate predicates} targeting {@link JavaFieldAccess}. * Further predicates to be used with {@link JavaFieldAccess} can be found at {@link JavaAccess.Predicates}. */ + @PublicAPI(usage = ACCESS) public static final class Predicates { private Predicates() { } @PublicAPI(usage = ACCESS) - public static DescribedPredicate accessType(final AccessType accessType) { + public static DescribedPredicate accessType(AccessType accessType) { return new AccessTypePredicate(accessType); } @PublicAPI(usage = ACCESS) - public static DescribedPredicate target(final DescribedPredicate predicate) { + public static DescribedPredicate target(DescribedPredicate predicate) { return new TargetPredicate<>(predicate); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaGenericArrayType.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaGenericArrayType.java index 0e1ac560a1..0cf136a9e7 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaGenericArrayType.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaGenericArrayType.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -67,6 +67,11 @@ public JavaClass toErasure() { return erasure; } + @Override + public void traverseSignature(SignatureVisitor visitor) { + SignatureTraversal.from(visitor).visitGenericArrayType(this); + } + @Override public String toString() { return getClass().getSimpleName() + '{' + getName() + '}'; diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaMember.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaMember.java index 19d73363a4..d6df4e451f 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaMember.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaMember.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -43,6 +43,7 @@ import static com.tngtech.archunit.core.domain.properties.HasName.Functions.GET_NAME; import static com.tngtech.archunit.core.domain.properties.HasType.Functions.GET_RAW_TYPE; +@PublicAPI(usage = ACCESS) public abstract class JavaMember implements HasName.AndFullName, HasDescriptor, HasAnnotations, HasModifiers, HasOwner, HasSourceCodeLocation { @@ -62,6 +63,15 @@ public abstract class JavaMember implements this.modifiers = checkNotNull(builder.getModifiers()); } + /** + * Similar to {@link JavaType#getAllInvolvedRawTypes()}, this method returns all raw types involved in this {@link JavaMember member's} signature. + * For more concrete details refer to {@link JavaField#getAllInvolvedRawTypes()} and {@link JavaCodeUnit#getAllInvolvedRawTypes()}. + * + * @return All raw types involved in the signature of this member + */ + @PublicAPI(usage = ACCESS) + public abstract Set getAllInvolvedRawTypes(); + @Override @PublicAPI(usage = ACCESS) public Set> getAnnotations() { @@ -208,6 +218,7 @@ public String toString() { *
  • {@link HasOwner.Predicates}
  • * */ + @PublicAPI(usage = ACCESS) public static final class Predicates { private Predicates() { } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaMethod.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaMethod.java index 369de70ffe..dd314129b9 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaMethod.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaMethod.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -34,7 +34,8 @@ import static com.tngtech.archunit.core.domain.Formatters.formatMethod; import static com.tngtech.archunit.core.domain.properties.HasName.Utils.namesOf; -public class JavaMethod extends JavaCodeUnit { +@PublicAPI(usage = ACCESS) +public final class JavaMethod extends JavaCodeUnit { private final Supplier methodSupplier; private final ThrowsClause throwsClause; private final Optional annotationDefaultValue; @@ -76,6 +77,7 @@ public boolean isMethod() { return true; } + @Override @PublicAPI(usage = ACCESS) public Set getCallsOfSelf() { return getReverseDependencies().getCallsTo(this); diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaMethodCall.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaMethodCall.java index cadb5c635c..bab0d4ba7e 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaMethodCall.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaMethodCall.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -15,10 +15,14 @@ */ package com.tngtech.archunit.core.domain; +import com.tngtech.archunit.PublicAPI; import com.tngtech.archunit.core.domain.AccessTarget.MethodCallTarget; import com.tngtech.archunit.core.importer.DomainBuilders.JavaMethodCallBuilder; -public class JavaMethodCall extends JavaCall { +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; + +@PublicAPI(usage = ACCESS) +public final class JavaMethodCall extends JavaCall { JavaMethodCall(JavaMethodCallBuilder builder) { super(builder); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaMethodReference.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaMethodReference.java index 1dba81475b..add10c3ba3 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaMethodReference.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaMethodReference.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -15,10 +15,14 @@ */ package com.tngtech.archunit.core.domain; +import com.tngtech.archunit.PublicAPI; import com.tngtech.archunit.core.domain.AccessTarget.MethodReferenceTarget; import com.tngtech.archunit.core.importer.DomainBuilders.JavaMethodReferenceBuilder; -public class JavaMethodReference extends JavaCodeUnitReference { +import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; + +@PublicAPI(usage = ACCESS) +public final class JavaMethodReference extends JavaCodeUnitReference { JavaMethodReference(JavaMethodReferenceBuilder builder) { super(builder); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaModifier.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaModifier.java index ebd2172c8c..32d03da969 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaModifier.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaModifier.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -18,6 +18,7 @@ import java.util.EnumSet; import java.util.Set; +import com.google.common.collect.ImmutableSet; import com.google.common.collect.Sets; import com.tngtech.archunit.PublicAPI; import org.objectweb.asm.Opcodes; @@ -27,6 +28,7 @@ import static java.util.Collections.emptySet; import static java.util.stream.Collectors.toSet; +@PublicAPI(usage = ACCESS) public enum JavaModifier { @PublicAPI(usage = ACCESS) PUBLIC(EnumSet.allOf(ApplicableType.class), Opcodes.ACC_PUBLIC), @@ -63,16 +65,47 @@ public enum JavaModifier { this.asmAccessFlag = asmAccessFlag; } + /** + * @deprecated This seems like an unnecessary API for users of ArchUnit, but limits us to do internal refactorings. + * If you think you need this API, please reach out to us on GitHub by creating an issue at + * https://github.com/TNG/ArchUnit/issues. + * Otherwise, at some point in the future we will remove this API without any replacement. + */ + @Deprecated @PublicAPI(usage = ACCESS) public static Set getModifiersForClass(int asmAccess) { - return getModifiersFor(ApplicableType.CLASS, asmAccess); + Set modifiers = getModifiersFor(ApplicableType.CLASS, asmAccess); + boolean opCodeForRecordIsPresent = (asmAccess & Opcodes.ACC_RECORD) != 0; + if (opCodeForRecordIsPresent) { + // As records are implicitly static and final (compare JLS 8.10 Record Declarations), + // we ensure that those modifiers are always present. (asmAccess does not contain STATIC.) + return ImmutableSet.builder() + .addAll(modifiers) + .add(JavaModifier.STATIC, JavaModifier.FINAL) + .build(); + } + return modifiers; } + /** + * @deprecated This seems like an unnecessary API for users of ArchUnit, but limits us to do internal refactorings. + * If you think you need this API, please reach out to us on GitHub by creating an issue at + * https://github.com/TNG/ArchUnit/issues. + * Otherwise, at some point in the future we will remove this API without any replacement. + */ + @Deprecated @PublicAPI(usage = ACCESS) public static Set getModifiersForField(int asmAccess) { return getModifiersFor(ApplicableType.FIELD, asmAccess); } + /** + * @deprecated This seems like an unnecessary API for users of ArchUnit, but limits us to do internal refactorings. + * If you think you need this API, please reach out to us on GitHub by creating an issue at + * https://github.com/TNG/ArchUnit/issues. + * Otherwise, at some point in the future we will remove this API without any replacement. + */ + @Deprecated @PublicAPI(usage = ACCESS) public static Set getModifiersForMethod(int asmAccess) { return getModifiersFor(ApplicableType.METHOD, asmAccess); diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaPackage.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaPackage.java index 5aca8f3f16..9bc6d222ab 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaPackage.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaPackage.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -40,6 +40,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; +import static com.google.common.collect.ImmutableSet.toImmutableSet; import static com.google.common.collect.Iterables.getOnlyElement; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; import static com.tngtech.archunit.PublicAPI.Usage.INHERITANCE; @@ -50,6 +51,41 @@ import static java.util.Collections.singleton; import static java.util.stream.Collectors.toSet; +/** + * Represents a package of Java classes as defined by the + * Java Language Specification. + * I.e. a namespace/group for related classes, where each package can also contain further subpackages. + * Thus, packages define a hierarchical tree-like structure.
    + * An example would be the package {@code java.lang} which contains {@code java.lang.Object}.
    + * ArchUnit will consider the "classes of a package" to be the classes residing directly + * within the package. Furthermore, "subpackages" of a package are considered packages that are residing + * directly within this package. On the contrary, ArchUnit will call the hierarchical tree-like structure + * consisting of all packages that can be reached by traversing subpackages, subpackages of subpackages, etc., + * as "package tree". + *

    + * Take for example the classes + *
    
    + * com.example.TopLevel
    + * com.example.first.First
    + * com.example.first.nested.FirstNested
    + * com.example.first.nested.deeper_nested.FirstDeeperNested
    + * com.example.second.Second
    + * com.example.second.nested.SecondNested
    + * 
    + * Then the package {@code com.example} would contain only the class {@code com.example.TopLevel}. It would also + * contain two subpackages {@code com.example.first} and {@code com.example.second} (but not {@code com.example.first.nested} + * as that is not directly contained within {@code com.example}).
    + * The package tree of {@code com.example} would contain all packages (and classes within) + *
    
    + * {@code com.example}
    + * {@code com.example.first}
    + * {@code com.example.first.nested}
    + * {@code com.example.first.nested.deeper_nested}
    + * {@code com.example.second}
    + * {@code com.example.second.nested}
    + * 
    + */ +@PublicAPI(usage = ACCESS) public final class JavaPackage implements HasName, HasAnnotations { private final String name; private final String relativeName; @@ -83,6 +119,11 @@ public String getRelativeName() { return relativeName; } + /** + * @return The {@link JavaClass} representing the compiled {@code package-info.class} file of this {@link JavaPackage} + * (for details refer to the Java Language Specification). + * Will throw an {@link IllegalArgumentException} if no {@code package-info} exists in this package. + */ @PublicAPI(usage = ACCESS) public HasAnnotations getPackageInfo() { Optional> packageInfo = tryGetPackageInfo(); @@ -92,26 +133,51 @@ public HasAnnotations getPackageInfo() { return packageInfo.get(); } + /** + * @return The {@link JavaClass} representing the compiled {@code package-info.class} file of this {@link JavaPackage} + * or {@link Optional#empty()} if no {@code package-info} exists in this package + * (for details refer to the Java Language Specification). + */ @PublicAPI(usage = ACCESS) public Optional> tryGetPackageInfo() { return packageInfo; } + /** + * @return All annotations on the compiled {@link #getPackageInfo() package-info.class} file + * (for details refer to the Java Language Specification). + */ @Override @PublicAPI(usage = ACCESS) public Set> getAnnotations() { - if (packageInfo.isPresent()) { - return packageInfo.get().getAnnotations().stream().map(withSelfAsOwner).collect(toSet()); - } - return emptySet(); + return packageInfo + .map(it -> it.getAnnotations().stream().map(withSelfAsOwner).collect(toSet())) + .orElse(emptySet()); } + /** + * @return The {@link Annotation} of the given type on the {@link #getPackageInfo() package-info.class} of this package + * (for details refer to the Java Language Specification). + * Will throw an {@link IllegalArgumentException} if either there is no {@code package-info} + * or the {@code package-info} is not annotated with the respective annotation type. + * @param The type of the {@link Annotation} to retrieve + * @see #tryGetAnnotationOfType(Class) + * @see #getAnnotationOfType(String) + */ @Override @PublicAPI(usage = ACCESS) public A getAnnotationOfType(Class type) { return getAnnotationOfType(type.getName()).as(type); } + /** + * @return The {@link JavaAnnotation} matching the given type on the {@link #getPackageInfo() package-info.class} of this package + * (for details refer to the Java Language Specification). + * Will throw an {@link IllegalArgumentException} if either there is no {@code package-info} + * or the {@code package-info} is not annotated with the respective annotation type. + * @see #tryGetAnnotationOfType(String) + * @see #getAnnotationOfType(Class) + */ @Override @PublicAPI(usage = ACCESS) public JavaAnnotation getAnnotationOfType(String typeName) { @@ -122,6 +188,14 @@ public JavaAnnotation getAnnotationOfType(String typeName) { return annotation.get(); } + /** + * @return The {@link Annotation} of the given type on the {@link #getPackageInfo() package-info.class} + * of this package or {@link Optional#empty()} if either there is no {@code package-info} + * or the {@code package-info} is not annotated with the respective annotation type. + * @param The type of the {@link Annotation} to retrieve + * @see #getAnnotationOfType(Class) + * @see #tryGetAnnotationOfType(String) + */ @Override @PublicAPI(usage = ACCESS) public Optional tryGetAnnotationOfType(Class type) { @@ -131,67 +205,89 @@ public Optional tryGetAnnotationOfType(Class type) return Optional.empty(); } + /** + * @return The {@link JavaAnnotation} matching the given type on the {@link #getPackageInfo() package-info.class} + * of this package or {@link Optional#empty()} if either there is no {@code package-info} + * or the {@code package-info} is not annotated with the respective annotation type. + * @see #tryGetAnnotationOfType(Class) + * @see #getAnnotationOfType(String) + */ @Override @PublicAPI(usage = ACCESS) public Optional> tryGetAnnotationOfType(String typeName) { - if (packageInfo.isPresent()) { - return packageInfo.get().tryGetAnnotationOfType(typeName).map(withSelfAsOwner); - } - return Optional.empty(); + return packageInfo.flatMap(it -> it.tryGetAnnotationOfType(typeName).map(withSelfAsOwner)); } + /** + * @return {@code true} if and only if there is a {@link #getPackageInfo() package-info.class} in this package + * that is annotated with an {@link Annotation} of the given type. + */ @Override @PublicAPI(usage = ACCESS) public boolean isAnnotatedWith(Class annotationType) { - if (packageInfo.isPresent()) { - return packageInfo.get().isAnnotatedWith(annotationType); - } - return false; + return packageInfo.map(it -> it.isAnnotatedWith(annotationType)).orElse(false); } + /** + * @return {@code true} if and only if there is a {@link #getPackageInfo() package-info.class} in this package + * that is annotated with an {@link Annotation} of the given type. + */ @Override @PublicAPI(usage = ACCESS) public boolean isAnnotatedWith(String annotationTypeName) { - if (packageInfo.isPresent()) { - return packageInfo.get().isAnnotatedWith(annotationTypeName); - } - return false; + return packageInfo.map(it -> it.isAnnotatedWith(annotationTypeName)).orElse(false); } + /** + * @return {@code true} if and only if there is a {@link #getPackageInfo() package-info.class} in this package + * that is annotated with an {@link Annotation} matching the given predicate. + */ @Override @PublicAPI(usage = ACCESS) public boolean isAnnotatedWith(DescribedPredicate> predicate) { - if (packageInfo.isPresent()) { - return packageInfo.get().isAnnotatedWith(predicate); - } - return false; + return packageInfo.map(it -> it.isAnnotatedWith(predicate)).orElse(false); } + /** + * @return {@code true} if and only if there is a {@link #getPackageInfo() package-info.class} in this package + * that is meta-annotated with an {@link Annotation} of the given type. + * A meta-annotation is an annotation that is declared on another annotation. + *

    + * This method also returns {@code true} if this element is directly annotated with the given annotation type. + *

    + */ @Override @PublicAPI(usage = ACCESS) public boolean isMetaAnnotatedWith(Class annotationType) { - if (packageInfo.isPresent()) { - return packageInfo.get().isMetaAnnotatedWith(annotationType); - } - return false; + return packageInfo.map(it -> it.isMetaAnnotatedWith(annotationType)).orElse(false); } + /** + * @return {@code true} if and only if there is a {@link #getPackageInfo() package-info.class} in this package + * that is meta-annotated with an {@link Annotation} of the given type. + * A meta-annotation is an annotation that is declared on another annotation. + *

    + * This method also returns {@code true} if this element is directly annotated with the given annotation type. + *

    + */ @Override @PublicAPI(usage = ACCESS) public boolean isMetaAnnotatedWith(String annotationTypeName) { - if (packageInfo.isPresent()) { - return packageInfo.get().isMetaAnnotatedWith(annotationTypeName); - } - return false; + return packageInfo.map(it -> it.isMetaAnnotatedWith(annotationTypeName)).orElse(false); } + /** + * @return {@code true} if and only if there is a {@link #getPackageInfo() package-info.class} in this package + * that is annotated with an {@link Annotation} matching the given predicate. + * A meta-annotation is an annotation that is declared on another annotation. + *

    + * This method also returns {@code true} if this element is directly annotated with the given annotation type. + *

    + */ @Override @PublicAPI(usage = ACCESS) public boolean isMetaAnnotatedWith(DescribedPredicate> predicate) { - if (packageInfo.isPresent()) { - return packageInfo.get().isMetaAnnotatedWith(predicate); - } - return false; + return packageInfo.map(it -> it.isMetaAnnotatedWith(predicate)).orElse(false); } /** @@ -207,7 +303,7 @@ private void setParent(JavaPackage parent) { } /** - * @return all classes directly contained in this package, no classes in sub-packages (compare {@link #getAllClasses()}) + * @return all classes directly contained in this package, but not classes in the lower levels of the package tree (compare {@link #getClassesInPackageTree()}) */ @PublicAPI(usage = ACCESS) public Set getClasses() { @@ -215,20 +311,21 @@ public Set getClasses() { } /** - * @return all classes contained in this package or any sub-package (compare {@link #getClasses()}) + * @return all classes contained in this {@link JavaPackage package tree}, i.e. all classes in this package, + * subpackages, subpackages of subpackages, and so on (compare {@link #getClasses()}) */ @PublicAPI(usage = ACCESS) - public Set getAllClasses() { + public Set getClassesInPackageTree() { ImmutableSet.Builder result = ImmutableSet.builder().addAll(classes); for (JavaPackage subpackage : getSubpackages()) { - result.addAll(subpackage.getAllClasses()); + result.addAll(subpackage.getClassesInPackageTree()); } return result.build(); } /** - * @return all (direct) sub-packages contained in this package, e.g. {@code [java.lang, java.io, ...]} for package {@code java} - * (compare {@link #getAllSubpackages()}) + * @return all (direct) subpackages contained in this package, e.g. for package {@code java} this would be + * {@code [java.lang, java.io, ...]} (compare {@link #getSubpackagesInTree()}) */ @PublicAPI(usage = ACCESS) public Set getSubpackages() { @@ -236,23 +333,23 @@ public Set getSubpackages() { } /** - * @return all sub-packages including nested sub-packages contained in this package, - * e.g. {@code [java.lang, java.lang.annotation, java.util, java.util.concurrent, ...]} for package {@code java} - * (compare {@link #getSubpackages()}) + * @return all subpackages contained in the package tree of this package. I.e. all subpackages, subpackages + * of subpackages, and so on. For package {@code java} this would be + * {@code [java.lang, java.lang.annotation, java.util, java.util.concurrent, ...]} (compare {@link #getSubpackages()}) */ @PublicAPI(usage = ACCESS) - public Set getAllSubpackages() { + public Set getSubpackagesInTree() { ImmutableSet.Builder result = ImmutableSet.builder(); for (JavaPackage subpackage : getSubpackages()) { result.add(subpackage); - result.addAll(subpackage.getAllSubpackages()); + result.addAll(subpackage.getSubpackagesInTree()); } return result.build(); } /** * @param clazz a {@link JavaClass} - * @return true if this package (directly) contains this {@link JavaClass} + * @return {@code true} if this package (directly) contains this {@link JavaClass} */ @PublicAPI(usage = ACCESS) public boolean containsClass(JavaClass clazz) { @@ -261,7 +358,7 @@ public boolean containsClass(JavaClass clazz) { /** * @param clazz a Java {@link Class} - * @return true if this package (directly) contains a {@link JavaClass} equivalent to the supplied {@link Class} + * @return {@code true} if this package (directly) contains a {@link JavaClass} equivalent to the supplied {@link Class} * @see #getClass(Class) * @see #containsClassWithFullyQualifiedName(String) * @see #containsClassWithSimpleName(String) @@ -272,8 +369,8 @@ public boolean containsClass(Class clazz) { } /** - * @param clazz A Java class - * @return the class if contained in this package, otherwise an Exception is thrown + * @param clazz A Java {@link Class} + * @return the class if (directly) contained in this package, otherwise an Exception is thrown * @see #containsClass(Class) * @see #getClassWithFullyQualifiedName(String) * @see #getClassWithSimpleName(String) @@ -285,7 +382,7 @@ public JavaClass getClass(Class clazz) { /** * @param className fully qualified name of a Java class - * @return true if this package (directly) contains a {@link JavaClass} with the given fully qualified name + * @return {@code true} if this package (directly) contains a {@link JavaClass} with the given fully qualified name * @see #getClassWithFullyQualifiedName(String) * @see #containsClass(Class) * @see #containsClassWithSimpleName(String) @@ -297,7 +394,7 @@ public boolean containsClassWithFullyQualifiedName(String className) { /** * @param className fully qualified name of a Java class - * @return the class if contained in this package, otherwise an Exception is thrown + * @return the class if (directly) contained in this package, otherwise an Exception is thrown * @see #containsClassWithFullyQualifiedName(String) * @see #getClass(Class) * @see #getClassWithSimpleName(String) @@ -314,7 +411,7 @@ private Optional tryGetClassWithFullyQualifiedName(String className) /** * @param className simple name of a Java class - * @return true if this package (directly) contains a {@link JavaClass} with the given simple name + * @return {@code true} if this package (directly) contains a {@link JavaClass} with the given simple name * @see #getClassWithSimpleName(String) * @see #containsClass(Class) * @see #containsClassWithFullyQualifiedName(String) @@ -326,7 +423,7 @@ public boolean containsClassWithSimpleName(String className) { /** * @param className simple name of a Java class - * @return the class if contained in this package, otherwise an Exception is thrown + * @return the class if (directly) contained in this package, otherwise an Exception is thrown * @see #containsClassWithSimpleName(String) * @see #getClass(Class) * @see #getClassWithFullyQualifiedName(String) @@ -351,8 +448,8 @@ private Set getClassesWith(Predicate predicate) { } /** - * @param packageName name of a package, may consist of several parts, e.g. {@code some.subpackage} - * @return true if this package contains the supplied (sub-) package + * @param packageName (relative) name of a package, may consist of several parts, e.g. {@code some.subpackage} + * @return true if this package contains the supplied (sub-)package with the given (relative) name * @see #getPackage(String) */ @PublicAPI(usage = ACCESS) @@ -361,8 +458,8 @@ public boolean containsPackage(String packageName) { } /** - * @param packageName name of a package, may consist of several parts, e.g. {@code some.subpackage} - * @return the (sub-) package with the given (relative) name; throws an exception if there is no such package contained + * @param packageName (relative) name of a package, may consist of several parts, e.g. {@code some.subpackage} + * @return the (sub-)package with the given (relative) name; throws an exception if there is no such package contained * @see #containsPackage(String) */ @PublicAPI(usage = ACCESS) @@ -391,78 +488,151 @@ private T getValue(Optional optional, String errorMessageTemplate, Object } /** - * @return All {@link Dependency dependencies} that originate from a {@link JavaClass} within this package - * to a {@link JavaClass} outside of this package + * @return All {@link Dependency dependencies} that originate from a {@link JavaClass} (directly) within this package + * to a {@link JavaClass} outside of this package. For dependencies from the package tree + * (this package, subpackages, subpackages of subpackages, etc.) please refer to {@link #getClassDependenciesFromThisPackageTree()}. + * @see #getClassDependenciesToThisPackage() */ @PublicAPI(usage = ACCESS) - public Set getClassDependenciesFromSelf() { - ImmutableSet.Builder result = ImmutableSet.builder(); - for (JavaClass javaClass : getAllClasses()) { - addAllNonSelfDependencies(result, javaClass.getDirectDependenciesFromSelf()); - } - return result.build(); + public Set getClassDependenciesFromThisPackage() { + return getClassDependenciesFrom(getClasses()); + } + + private static Set getClassDependenciesFrom(Set classes) { + return classes.stream() + .flatMap(javaClass -> javaClass.getDirectDependenciesFromSelf().stream()) + .filter(dependency -> !classes.contains(dependency.getTargetClass())) + .collect(toImmutableSet()); + } + + /** + * @return All {@link Dependency dependencies} that originate from a {@link JavaClass} within this package tree + * (this package, subpackages, subpackages of subpackages, etc.) to a {@link JavaClass} outside of this package tree. + * To limit this to dependencies that originate (directly) from this package please refer to {@link #getClassDependenciesFromThisPackage()}. + * @see #getClassDependenciesToThisPackageTree() + */ + @PublicAPI(usage = ACCESS) + public Set getClassDependenciesFromThisPackageTree() { + return getClassDependenciesFrom(getClassesInPackageTree()); } /** * @return All {@link Dependency dependencies} that originate from a {@link JavaClass} outside of this package - * to a {@link JavaClass} within this package + * to a {@link JavaClass} (directly) within this package. For dependencies to this package tree + * (this package, subpackages, subpackages of subpackages, etc.) please refer to {@link #getClassDependenciesToThisPackageTree()}. + * @see #getClassDependenciesFromThisPackage() */ @PublicAPI(usage = ACCESS) - public Set getClassDependenciesToSelf() { - ImmutableSet.Builder result = ImmutableSet.builder(); - for (JavaClass javaClass : getAllClasses()) { - addAllNonSelfDependencies(result, javaClass.getDirectDependenciesToSelf()); - } - return result.build(); + public Set getClassDependenciesToThisPackage() { + return getClassDependenciesTo(getClasses()); } - private void addAllNonSelfDependencies(ImmutableSet.Builder result, Set dependencies) { - for (Dependency dependency : dependencies) { - if (!containsClass(dependency.getOriginClass()) || !containsClass(dependency.getTargetClass())) { - result.add(dependency); - } - } + private static ImmutableSet getClassDependenciesTo(Set classes) { + return classes.stream() + .flatMap(javaClass -> javaClass.getDirectDependenciesToSelf().stream()) + .filter(dependency -> !classes.contains(dependency.getOriginClass())) + .collect(toImmutableSet()); + } + + /** + * @return All {@link Dependency dependencies} that originate from a {@link JavaClass} outside of this package tree + * (this package, subpackages, subpackages of subpackages, etc.) to a {@link JavaClass} within this package tree. + * To limit this to dependencies that directly target this package refer to {@link #getClassDependenciesToThisPackage()}. + * @see #getClassDependenciesFromThisPackageTree() + */ + @PublicAPI(usage = ACCESS) + public Set getClassDependenciesToThisPackageTree() { + return getClassDependenciesTo(getClassesInPackageTree()); } /** * @return All {@link JavaPackage packages} that this package has a dependency on. I.e. all {@link JavaPackage packages} - * that contain a class such that a class in this package depends on that class. + * that contain a class such that a class (directly) in this package depends on that class. + * For example

    + * + *

    + * For dependencies to all packages that any class in this package tree (this package, subpackages, subpackages of subpackages, etc.) + * depends on refer to {@link #getPackageDependenciesFromThisPackageTree()}. + * + * @see #getClassDependenciesFromThisPackage() + * @see #getPackageDependenciesToThisPackage() */ @PublicAPI(usage = ACCESS) - public Set getPackageDependenciesFromSelf() { - ImmutableSet.Builder result = ImmutableSet.builder(); - for (Dependency dependency : getClassDependenciesFromSelf()) { - result.add(dependency.getTargetClass().getPackage()); - } - return result.build(); + public Set getPackageDependenciesFromThisPackage() { + return getPackageDependencies(getClassDependenciesFromThisPackage(), Dependency::getTargetClass); } /** - * @return All {@link JavaPackage packages} that have a dependency on this package. I.e. all {@link JavaPackage packages} - * that contain a class that depends on a class in this package. + * @return All {@link JavaPackage packages} that this package tree (this package, subpackages, subpackages of subpackages, etc.) + * has a dependency on. I.e. all {@link JavaPackage packages} that contain a class such that a class in this package tree + * depends on that class. For example + *

    + * + *

    + * To limit this to only those packages that classes (directly) in this package depend on + * refer to {@link #getPackageDependenciesFromThisPackage()}. + * + * @see #getClassDependenciesFromThisPackageTree() + * @see #getPackageDependenciesToThisPackageTree() */ @PublicAPI(usage = ACCESS) - public Set getPackageDependenciesToSelf() { - ImmutableSet.Builder result = ImmutableSet.builder(); - for (Dependency dependency : getClassDependenciesToSelf()) { - result.add(dependency.getOriginClass().getPackage()); - } - return result.build(); + public Set getPackageDependenciesFromThisPackageTree() { + return getPackageDependencies(getClassDependenciesFromThisPackageTree(), Dependency::getTargetClass); + } + + /** + * @return All {@link JavaPackage packages} that have a dependency on this package. + * I.e. all {@link JavaPackage packages} that contain a class that depends on a class (directly) in this package. + * For example

    + * + *

    + * For dependencies from all packages that depend on any class in this package tree (this package, subpackages, subpackages of subpackages, etc.) + * refer to {@link #getPackageDependenciesToThisPackageTree()}. + * + * @see #getClassDependenciesToThisPackage() + * @see #getPackageDependenciesFromThisPackage() + */ + @PublicAPI(usage = ACCESS) + public Set getPackageDependenciesToThisPackage() { + return getPackageDependencies(getClassDependenciesToThisPackage(), Dependency::getOriginClass); + } + + /** + * @return All {@link JavaPackage packages} that have a dependency on this package tree (this package, subpackages, subpackages of subpackages, etc.). + * I.e. all {@link JavaPackage packages} that contain a class that depends on a class in this package tree. For example

    + * + *

    + * To limit this to only those packages + * that depend on classes (directly) in this package refer to {@link #getPackageDependenciesToThisPackage()}. + * + * @see #getClassDependenciesToThisPackageTree() + * @see #getPackageDependenciesFromThisPackageTree() + */ + @PublicAPI(usage = ACCESS) + public Set getPackageDependenciesToThisPackageTree() { + return getPackageDependencies(getClassDependenciesToThisPackageTree(), Dependency::getOriginClass); + } + + private Set getPackageDependencies(Set dependencies, Function javaClassFromDependency) { + return dependencies.stream() + .map(javaClassFromDependency) + .map(JavaClass::getPackage) + .collect(toImmutableSet()); } /** * Traverses the package tree visiting each matching class. * @param predicate determines which classes within the package tree should be visited * @param visitor will receive each class in the package tree matching the given predicate - * @see #accept(Predicate, PackageVisitor) + * @see #traversePackageTree(Predicate, PackageVisitor) */ @PublicAPI(usage = ACCESS) - public void accept(Predicate predicate, ClassVisitor visitor) { + public void traversePackageTree(Predicate predicate, ClassVisitor visitor) { for (JavaClass javaClass : getClassesWith(predicate)) { visitor.visit(javaClass); } for (JavaPackage subpackage : getSubpackages()) { - subpackage.accept(predicate, visitor); + subpackage.traversePackageTree(predicate, visitor); } } @@ -470,15 +640,15 @@ public void accept(Predicate predicate, ClassVisitor visitor) * Traverses the package tree visiting each matching package. * @param predicate determines which packages within the package tree should be visited * @param visitor will receive each package in the package tree matching the given predicate - * @see #accept(Predicate, ClassVisitor) + * @see #traversePackageTree(Predicate, ClassVisitor) */ @PublicAPI(usage = ACCESS) - public void accept(Predicate predicate, PackageVisitor visitor) { + public void traversePackageTree(Predicate predicate, PackageVisitor visitor) { if (predicate.test(this)) { visitor.visit(this); } for (JavaPackage subpackage : getSubpackages()) { - subpackage.accept(predicate, visitor); + subpackage.traversePackageTree(predicate, visitor); } } @@ -579,6 +749,7 @@ public interface PackageVisitor { /** * Predefined {@link ChainableFunction functions} to transform {@link JavaPackage}. */ + @PublicAPI(usage = ACCESS) public static final class Functions { private Functions() { } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaParameter.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaParameter.java index e984e3c44e..5f2fea3872 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaParameter.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaParameter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaParameterizedType.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaParameterizedType.java index f5ae25ed47..80444b5ee2 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaParameterizedType.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaParameterizedType.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -35,4 +35,9 @@ public interface JavaParameterizedType extends JavaType { */ @PublicAPI(usage = ACCESS) List getActualTypeArguments(); + + @Override + default void traverseSignature(SignatureVisitor visitor) { + SignatureTraversal.from(visitor).visitParameterizedType(this); + } } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaStaticInitializer.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaStaticInitializer.java index 428c3093d2..95919a398a 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaStaticInitializer.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaStaticInitializer.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -39,7 +39,8 @@ * } * */ -public class JavaStaticInitializer extends JavaCodeUnit { +@PublicAPI(usage = ACCESS) +public final class JavaStaticInitializer extends JavaCodeUnit { @PublicAPI(usage = ACCESS) public static final String STATIC_INITIALIZER_NAME = ""; diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaType.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaType.java index 24100bb12a..8af3e66db3 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaType.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaType.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -15,12 +15,40 @@ */ package com.tngtech.archunit.core.domain; +import java.lang.reflect.Type; +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; +import java.util.function.Function; +import java.util.function.Supplier; + +import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Iterables; import com.tngtech.archunit.PublicAPI; import com.tngtech.archunit.base.ChainableFunction; import com.tngtech.archunit.core.domain.properties.HasName; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +import static com.tngtech.archunit.PublicAPI.Usage.INHERITANCE; +import static com.tngtech.archunit.core.domain.JavaType.SignatureVisitor.Result.CONTINUE; +import static com.tngtech.archunit.core.domain.JavaType.SignatureVisitor.Result.STOP; +import static java.util.Collections.singleton; +/** + * Represents a general Java type. This can e.g. be a class like {@code java.lang.String}, a parameterized type + * like {@code List} or a type variable like {@code T}.
    + * Besides having a {@link HasName#getName() name} and offering the possibility to being converted to an + * {@link #toErasure() erasure} (which is then always {@link JavaClass a raw class object}) {@link JavaType} doesn't offer + * an extensive API. Instead, users can check a {@link JavaType} for being an instance of a concrete subtype + * (like {@link JavaTypeVariable}) and then cast it to the respective subclass + * (same as with {@link Type} of the Java Reflection API). + * + * @see JavaClass + * @see JavaParameterizedType + * @see JavaTypeVariable + * @see JavaWildcardType + * @see JavaGenericArrayType + */ @PublicAPI(usage = ACCESS) public interface JavaType extends HasName { /** @@ -39,9 +67,141 @@ public interface JavaType extends HasName { @PublicAPI(usage = ACCESS) JavaClass toErasure(); + /** + * Returns the set of all raw types that are involved in this type. + * If this type is a {@link JavaClass}, then this method trivially returns only the class itself. + * If this type is a {@link JavaParameterizedType}, {@link JavaTypeVariable}, {@link JavaWildcardType}, etc., + * then this method returns all raw types involved in type arguments and upper and lower bounds recursively. + * If this type is an array type, then this method returns all raw types involved in the component type of the array type. + *

    + * Examples:
    + * For the parameterized type + *
    
    +     * List<String>
    + * the result would be the {@link JavaClass classes} [List, String].
    + * For the parameterized type + *
    
    +     * Map<? extends Serializable, List<? super Integer[]>>
    + * the result would be [Map, Serializable, List, Integer].
    + * And for the type variable + *
    
    +     * T extends List<? super Integer>
    + * the result would be [List, Integer].
    + * Thus, this method offers a quick way to determine all types a (possibly complex) type depends on. + * + * @return All raw types involved in this {@link JavaType} + */ + @PublicAPI(usage = ACCESS) + default Set getAllInvolvedRawTypes() { + ImmutableSet.Builder result = ImmutableSet.builder(); + traverseSignature(new SignatureVisitor() { + @Override + public Result visitClass(JavaClass type) { + result.add(type.getBaseComponentType()); + return CONTINUE; + } + + @Override + public Result visitParameterizedType(JavaParameterizedType type) { + result.add(type.toErasure()); + return CONTINUE; + } + }); + return result.build(); + } + + /** + * Traverses through the signature of this {@link JavaType}.
    + * This method considers the type signature as a tree, + * where e.g. a {@link JavaClass} is a simple leaf, + * but a {@link JavaParameterizedType} has the type as root and then + * branches out into its actual type arguments, which in turn can have type arguments + * or upper/lower bounds in case of {@link JavaTypeVariable} or {@link JavaWildcardType}.
    + * The following is a simple visualization of such a signature tree: + *
    
    +     * List<Map<? extends Serializable, String[]>>
    +     *                    |
    +     *    Map<? extends Serializable, String[]>
    +     *              /                   \
    +     *  ? extends Serializable         String[]
    +     *            |
    +     *      Serializable
    +     * 
    + * For every node visited the respective method of the provided {@code visitor} + * will be invoked. The traversal happens depth first, i.e. in this case the {@code visitor} + * would be invoked for all types down to {@code Serializable} before visiting the {@code String[]} + * array type of the second branch. At every step it is possible to continue the traversal + * by returning {@link SignatureVisitor.Result#CONTINUE CONTINUE} or stop at that point by + * returning {@link SignatureVisitor.Result#STOP STOP}.

    + * Note that the traversal will continue to traverse bounds of type variables, + * even if that type variable isn't declared in this signature itself.
    + * E.g. take the following scenario + *
    
    +     * class Example<T extends String> {
    +     *     T field;
    +     * }
    + * Traversing the {@link JavaField#getType() field type} of {@code field} will continue + * down to the upper bounds of the type variable {@code T} and thus end at the type {@code String}.

    + * Also, note that the traversal will not continue down the type parameters of a raw type + * declared in a signature.
    + * E.g. given the signature {@code class Example} the traversal would stop at + * {@code Map} and not traverse down the type parameters {@code K} and {@code V} of {@code Map}. + * + * @param visitor A {@link SignatureVisitor} to invoke for every encountered {@link JavaType} + * while traversing this signature. + */ + @PublicAPI(usage = ACCESS) + void traverseSignature(SignatureVisitor visitor); + + /** + * @see #traverseSignature(SignatureVisitor) + */ + @PublicAPI(usage = INHERITANCE) + interface SignatureVisitor { + default Result visitClass(JavaClass type) { + return CONTINUE; + } + + default Result visitParameterizedType(JavaParameterizedType type) { + return CONTINUE; + } + + default Result visitTypeVariable(JavaTypeVariable type) { + return CONTINUE; + } + + default Result visitGenericArrayType(JavaGenericArrayType type) { + return CONTINUE; + } + + default Result visitWildcardType(JavaWildcardType type) { + return CONTINUE; + } + + /** + * Result of a single step {@link #traverseSignature(SignatureVisitor) traversing a signature}. + * After each step it's possible to either {@link #STOP stop} or {@link #CONTINUE continue} + * the traversal. + */ + @PublicAPI(usage = ACCESS) + enum Result { + /** + * Causes the traversal to continue + */ + @PublicAPI(usage = ACCESS) + CONTINUE, + /** + * Causes the traversal to stop + */ + @PublicAPI(usage = ACCESS) + STOP + } + } + /** * Predefined {@link ChainableFunction functions} to transform {@link JavaType}. */ + @PublicAPI(usage = ACCESS) final class Functions { private Functions() { } @@ -55,3 +215,81 @@ public JavaClass apply(JavaType input) { }; } } + +class SignatureTraversal implements JavaType.SignatureVisitor { + private final Set visited = new HashSet<>(); + private final JavaType.SignatureVisitor delegate; + private Result lastResult; + + private SignatureTraversal(JavaType.SignatureVisitor delegate) { + this.delegate = delegate; + } + + @Override + public Result visitClass(JavaClass type) { + // We only traverse type parameters of a JavaClass if the traversal was started *at the JavaClass* itself. + // Otherwise, we can only encounter a regular class as a raw type in a type signature. + // In these cases we don't want to traverse further down, as that would be surprising behavior + // (consider `class MyClass`, traversing into the type variables `K` and `V` of `Map` would be surprising). + Supplier>> getFurtherTypesToTraverse = visited.isEmpty() ? type::getTypeParameters : Collections::emptyList; + return visit(type, delegate::visitClass, getFurtherTypesToTraverse); + } + + @Override + public Result visitParameterizedType(JavaParameterizedType type) { + return visit(type, delegate::visitParameterizedType, type::getActualTypeArguments); + } + + @Override + public Result visitTypeVariable(JavaTypeVariable type) { + return visit(type, delegate::visitTypeVariable, type::getUpperBounds); + } + + @Override + public Result visitGenericArrayType(JavaGenericArrayType type) { + return visit(type, delegate::visitGenericArrayType, () -> singleton(type.getComponentType())); + } + + @Override + public Result visitWildcardType(JavaWildcardType type) { + return visit(type, delegate::visitWildcardType, () -> Iterables.concat(type.getUpperBounds(), type.getLowerBounds())); + } + + private Result visit( + CURRENT type, + Function visitCurrent, + Supplier> nextTypes + ) { + if (visited.contains(type)) { + // if we've encountered this type already we continue traversing the siblings, + // but we won't descend further into this type signature + return setLast(CONTINUE); + } + visited.add(type); + if (visitCurrent.apply(type) == CONTINUE) { + Result result = visit(nextTypes.get()); + return setLast(result); + } else { + return setLast(STOP); + } + } + + private Result visit(Iterable types) { + for (JavaType nextType : types) { + nextType.traverseSignature(this); + if (lastResult == STOP) { + return STOP; + } + } + return CONTINUE; + } + + private Result setLast(Result result) { + lastResult = result; + return result; + } + + static SignatureTraversal from(JavaType.SignatureVisitor visitor) { + return visitor instanceof SignatureTraversal ? (SignatureTraversal) visitor : new SignatureTraversal(visitor); + } +} \ No newline at end of file diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaTypeVariable.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaTypeVariable.java index b6a06fca15..ad0459341a 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaTypeVariable.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaTypeVariable.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -119,6 +119,11 @@ public JavaClass toErasure() { return erasure; } + @Override + public void traverseSignature(SignatureVisitor visitor) { + SignatureTraversal.from(visitor).visitTypeVariable(this); + } + @Override public String toString() { String bounds = printExtendsClause() ? " extends " + joinTypeNames(upperBounds) : ""; diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaWildcardType.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaWildcardType.java index 4bb44440d9..d6df6aa6ae 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaWildcardType.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/JavaWildcardType.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -37,7 +37,8 @@ * A lower bound denotes a common subtype that must be assignable to all substitutions * of this wildcard type. It is denoted by {@code ? super SomeType}. */ -public class JavaWildcardType implements JavaType, HasUpperBounds { +@PublicAPI(usage = ACCESS) +public final class JavaWildcardType implements JavaType, HasUpperBounds { private static final String WILDCARD_TYPE_NAME = "?"; private final List upperBounds; @@ -94,6 +95,11 @@ public JavaClass toErasure() { return erasure; } + @Override + public void traverseSignature(SignatureVisitor visitor) { + SignatureTraversal.from(visitor).visitWildcardType(this); + } + @Override public String toString() { return getClass().getSimpleName() + '{' + getName() + '}'; diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/PackageMatcher.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/PackageMatcher.java index 80a7013102..9a5a27fa1c 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/PackageMatcher.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/PackageMatcher.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -61,6 +61,7 @@ * * Create via {@link PackageMatcher#of(String) PackageMatcher.of(packageIdentifier)} */ +@PublicAPI(usage = ACCESS) public final class PackageMatcher { private static final String OPT_LETTERS_AT_START = "(?:^\\w*)?"; private static final String OPT_LETTERS_AT_END = "(?:\\w*$)?"; @@ -190,6 +191,7 @@ private static String nestedGroupRegex(char outerOpeningChar, char outerClosingC return "\\" + outerOpeningChar + "[^" + outerClosingChar + "]*\\" + nestedOpeningChar; } + @PublicAPI(usage = ACCESS) public static final class Result { private final Matcher matcher; @@ -211,6 +213,6 @@ public String getGroup(int number) { @PublicAPI(usage = ACCESS) public static final Function> TO_GROUPS = input -> IntStream.rangeClosed(1, input.getNumberOfGroups()) - .mapToObj(i -> input.getGroup(i)) + .mapToObj(input::getGroup) .collect(toList()); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/PackageMatchers.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/PackageMatchers.java index d1a3d8c8c3..f10d35958e 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/PackageMatchers.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/PackageMatchers.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/ReferencedClassObject.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/ReferencedClassObject.java index 294fc6a994..6d229ed442 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/ReferencedClassObject.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/ReferencedClassObject.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -29,14 +29,14 @@ public final class ReferencedClassObject implements HasType, HasOwner, HasSourceCodeLocation { private final JavaCodeUnit owner; private final JavaClass value; - private final int lineNumber; private final SourceCodeLocation sourceCodeLocation; + private final boolean declaredInLambda; - private ReferencedClassObject(JavaCodeUnit owner, JavaClass value, int lineNumber) { + private ReferencedClassObject(JavaCodeUnit owner, JavaClass value, int lineNumber, boolean declaredInLambda) { this.owner = checkNotNull(owner); this.value = checkNotNull(value); - this.lineNumber = lineNumber; sourceCodeLocation = SourceCodeLocation.of(owner.getOwner(), lineNumber); + this.declaredInLambda = declaredInLambda; } @Override @@ -64,7 +64,7 @@ public JavaClass getValue() { @PublicAPI(usage = ACCESS) public int getLineNumber() { - return lineNumber; + return sourceCodeLocation.getLineNumber(); } @Override @@ -73,17 +73,23 @@ public SourceCodeLocation getSourceCodeLocation() { return sourceCodeLocation; } + @PublicAPI(usage = ACCESS) + public boolean isDeclaredInLambda() { + return declaredInLambda; + } + @Override public String toString() { return toStringHelper(this) .add("owner", owner) .add("value", value) .add("sourceCodeLocation", sourceCodeLocation) + .add("declaredInLambda", declaredInLambda) .toString(); } - static ReferencedClassObject from(JavaCodeUnit owner, JavaClass javaClass, int lineNumber) { - return new ReferencedClassObject(owner, javaClass, lineNumber); + static ReferencedClassObject from(JavaCodeUnit owner, JavaClass javaClass, int lineNumber, boolean declaredInLambda) { + return new ReferencedClassObject(owner, javaClass, lineNumber, declaredInLambda); } /** diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/ReverseDependencies.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/ReverseDependencies.java index 900bfea671..e6a0fd7748 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/ReverseDependencies.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/ReverseDependencies.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -66,7 +66,7 @@ private ReverseDependencies(ReverseDependencies.Creation creation) { this.directDependenciesToClass = createDirectDependenciesToClassSupplier(creation.allDependencies); } - private static Supplier> createDirectDependenciesToClassSupplier(final List allDependencies) { + private static Supplier> createDirectDependenciesToClassSupplier(List allDependencies) { return Suppliers.memoize(() -> { ImmutableSetMultimap.Builder result = ImmutableSetMultimap.builder(); for (JavaClassDependencies dependencies : allDependencies) { @@ -221,7 +221,7 @@ private void registerConstructors(JavaClass clazz) { } private void registerAnnotations(JavaClass clazz) { - for (final JavaAnnotation annotation : findAnnotations(clazz)) { + for (JavaAnnotation annotation : findAnnotations(clazz)) { annotationTypeDependencies.put(annotation.getRawType(), annotation); annotation.accept(new JavaAnnotation.DefaultParameterVisitor() { @Override @@ -277,7 +277,7 @@ private ResolvingAccessLoader(SetMultimap accessesToSelf) { @Override public Set load(MEMBER member) { ImmutableSet.Builder result = ImmutableSet.builder(); - for (final JavaClass javaClass : getPossibleTargetClassesForAccess(member.getOwner())) { + for (JavaClass javaClass : getPossibleTargetClassesForAccess(member.getOwner())) { for (ACCESS access : this.accessesToSelf.get(javaClass)) { Optional target = access.getTarget().resolveMember(); if (target.isPresent() && target.get().equals(member)) { diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/Source.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/Source.java index 8aa0604814..20dace50a6 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/Source.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/Source.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -43,7 +43,8 @@ * to your {@value com.tngtech.archunit.ArchConfiguration#ARCHUNIT_PROPERTIES_RESOURCE_NAME}. *

    */ -public class Source { +@PublicAPI(usage = ACCESS) +public final class Source { private final URI uri; private final Optional fileName; private final Md5sum md5sum; @@ -82,7 +83,7 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } - final Source other = (Source) obj; + Source other = (Source) obj; return Objects.equals(this.uri, other.uri) && Objects.equals(this.md5sum, other.md5sum); } @@ -92,7 +93,8 @@ public String toString() { return uri + " [md5='" + md5sum + "']"; } - public static class Md5sum { + @PublicAPI(usage = ACCESS) + public static final class Md5sum { /** * We can't determine the md5 sum, because the platform is missing the digest algorithm */ @@ -149,7 +151,7 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } - final Md5sum other = (Md5sum) obj; + Md5sum other = (Md5sum) obj; return Arrays.equals(this.md5Bytes, other.md5Bytes) && Objects.equals(this.text, other.text); } @@ -173,7 +175,7 @@ private static Md5sum of(URI uri) { } Optional bytesFromUri = read(uri); - return bytesFromUri.isPresent() ? new Md5sum(bytesFromUri.get(), MD5_DIGEST) : UNDETERMINED; + return bytesFromUri.map(bytes -> new Md5sum(bytes, MD5_DIGEST)).orElse(UNDETERMINED); } private static Optional read(URI uri) { diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/SourceCodeLocation.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/SourceCodeLocation.java index ee007c2827..b13f7bd290 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/SourceCodeLocation.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/SourceCodeLocation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -110,7 +110,7 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } - final SourceCodeLocation other = (SourceCodeLocation) obj; + SourceCodeLocation other = (SourceCodeLocation) obj; return Objects.equals(this.sourceClass, other.sourceClass) && Objects.equals(this.lineNumber, other.lineNumber); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/ThrowsClause.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/ThrowsClause.java index fa80067a62..42145077f3 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/ThrowsClause.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/ThrowsClause.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -38,6 +38,7 @@ import static com.tngtech.archunit.core.domain.properties.HasType.Functions.GET_RAW_TYPE; import static java.util.stream.Collectors.toList; +@PublicAPI(usage = ACCESS) public final class ThrowsClause> extends ForwardingList> implements HasOwner { @@ -109,7 +110,7 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } - final ThrowsClause other = (ThrowsClause) obj; + ThrowsClause other = (ThrowsClause) obj; return Objects.equals(this.location, other.location) && Objects.equals(this.getTypes(), other.getTypes()); } @@ -137,6 +138,7 @@ protected List> delegate() { /** * Predefined {@link ChainableFunction functions} to transform {@link ThrowsClause}. */ + @PublicAPI(usage = ACCESS) public static final class Functions { private Functions() { } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/ThrowsDeclaration.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/ThrowsDeclaration.java index f682c59fa3..d623c3f106 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/ThrowsDeclaration.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/ThrowsDeclaration.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -50,6 +50,7 @@ * For further information about the resolution process of {@link AccessTarget AccessTargets} to * {@link JavaMember JavaMembers} consult the documentation at {@link AccessTarget}. */ +@PublicAPI(usage = ACCESS) public final class ThrowsDeclaration> implements HasType, HasOwner> { @@ -123,7 +124,7 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } - final ThrowsDeclaration other = (ThrowsDeclaration) obj; + ThrowsDeclaration other = (ThrowsDeclaration) obj; return Objects.equals(this.getLocation(), other.getLocation()) && Objects.equals(this.type, other.type); } @@ -146,6 +147,7 @@ public static final class Functions { private Functions() { } + @PublicAPI(usage = ACCESS) public static final class Get { private Get() { } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/TryCatchBlock.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/TryCatchBlock.java index 39c833e7ef..48d6a2f62a 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/TryCatchBlock.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/TryCatchBlock.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -34,12 +34,14 @@ public final class TryCatchBlock implements HasOwner, HasSourceCod private final Set caughtThrowables; private final SourceCodeLocation sourceCodeLocation; private final Set> accessesContainedInTryBlock; + private final boolean declaredInLambda; TryCatchBlock(TryCatchBlockBuilder builder) { this.owner = checkNotNull(builder.getOwner()); this.caughtThrowables = ImmutableSet.copyOf(builder.getCaughtThrowables()); this.sourceCodeLocation = checkNotNull(builder.getSourceCodeLocation()); this.accessesContainedInTryBlock = ImmutableSet.copyOf(builder.getAccessesContainedInTryBlock()); + declaredInLambda = builder.isDeclaredInLambda(); } @Override @@ -64,6 +66,11 @@ public Set> getAccessesContainedInTryBlock() { return accessesContainedInTryBlock; } + @PublicAPI(usage = ACCESS) + public boolean isDeclaredInLambda() { + return declaredInLambda; + } + @Override public String toString() { return toStringHelper(this) diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/CanBeAnnotated.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/CanBeAnnotated.java index bc94c72108..c466be5435 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/CanBeAnnotated.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/CanBeAnnotated.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -34,6 +34,7 @@ import static com.tngtech.archunit.core.domain.properties.HasName.Functions.GET_NAME; import static com.tngtech.archunit.core.domain.properties.HasType.Functions.GET_RAW_TYPE; +@PublicAPI(usage = ACCESS) public interface CanBeAnnotated { /** @@ -95,6 +96,7 @@ public interface CanBeAnnotated { /** * Predefined {@link DescribedPredicate predicates} targeting objects that implement {@link CanBeAnnotated} */ + @PublicAPI(usage = ACCESS) final class Predicates { private Predicates() { } @@ -105,7 +107,7 @@ private Predicates() { * @param annotationType The type of the annotation to check for */ @PublicAPI(usage = ACCESS) - public static DescribedPredicate annotatedWith(final Class annotationType) { + public static DescribedPredicate annotatedWith(Class annotationType) { checkAnnotationHasReasonableRetention(annotationType); return annotatedWith(annotationType.getName()); @@ -130,7 +132,7 @@ private static boolean isRetentionSource(Class annotationT * @see #annotatedWith(Class) */ @PublicAPI(usage = ACCESS) - public static DescribedPredicate annotatedWith(final String annotationTypeName) { + public static DescribedPredicate annotatedWith(String annotationTypeName) { DescribedPredicate typeNameMatches = GET_RAW_TYPE.then(GET_NAME).is(equalTo(annotationTypeName)); return annotatedWith(typeNameMatches.as("@" + ensureSimpleName(annotationTypeName))); } @@ -141,7 +143,7 @@ public static DescribedPredicate annotatedWith(final String anno * @param predicate Qualifies matching annotations */ @PublicAPI(usage = ACCESS) - public static DescribedPredicate annotatedWith(final DescribedPredicate> predicate) { + public static DescribedPredicate annotatedWith(DescribedPredicate> predicate) { return new AnnotatedPredicate(predicate); } @@ -170,7 +172,7 @@ public boolean test(CanBeAnnotated input) { * @param annotationType The type of the annotation to check for */ @PublicAPI(usage = ACCESS) - public static DescribedPredicate metaAnnotatedWith(final Class annotationType) { + public static DescribedPredicate metaAnnotatedWith(Class annotationType) { checkAnnotationHasReasonableRetention(annotationType); return metaAnnotatedWith(annotationType.getName()); @@ -180,7 +182,7 @@ public static DescribedPredicate metaAnnotatedWith(final Class metaAnnotatedWith(final String annotationTypeName) { + public static DescribedPredicate metaAnnotatedWith(String annotationTypeName) { DescribedPredicate typeNameMatches = GET_RAW_TYPE.then(GET_NAME).is(equalTo(annotationTypeName)); return metaAnnotatedWith(typeNameMatches.as("@" + ensureSimpleName(annotationTypeName))); } @@ -196,7 +198,7 @@ public static DescribedPredicate metaAnnotatedWith(final String * @param predicate Qualifies matching annotations */ @PublicAPI(usage = ACCESS) - public static DescribedPredicate metaAnnotatedWith(final DescribedPredicate> predicate) { + public static DescribedPredicate metaAnnotatedWith(DescribedPredicate> predicate) { return new MetaAnnotatedPredicate(predicate); } @@ -215,6 +217,7 @@ public boolean test(CanBeAnnotated input) { } } + @PublicAPI(usage = ACCESS) final class Utils { private Utils() { } @@ -253,7 +256,7 @@ private static boolean isMetaAnnotatedWith( } @PublicAPI(usage = ACCESS) - public static
    Function, A> toAnnotationOfType(final Class type) { + public static Function, A> toAnnotationOfType(Class type) { return input -> input.as(type); } } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/CanOverrideDescription.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/CanOverrideDescription.java index b2521b14bb..fa8f2c31a0 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/CanOverrideDescription.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/CanOverrideDescription.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -19,6 +19,7 @@ import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +@PublicAPI(usage = ACCESS) public interface CanOverrideDescription { /** * Allows to adjust the description of this object. Note that this method will not modify the current object, diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasAnnotations.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasAnnotations.java index 406e300571..7181495029 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasAnnotations.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasAnnotations.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -30,15 +30,45 @@ public interface HasAnnotations> extends CanBe @PublicAPI(usage = ACCESS) Set> getAnnotations(); + /** + * @param type The {@link Class} of the {@link Annotation} to retrieve. + * @return The {@link Annotation} of the given type. + * Will throw an {@link IllegalArgumentException} if no matching {@link Annotation} is present. + * @param The type of the {@link Annotation} to retrieve + * @see #tryGetAnnotationOfType(Class) + * @see #getAnnotationOfType(String) + */ @PublicAPI(usage = ACCESS) A getAnnotationOfType(Class type); + /** + * @param typeName The fully qualified class name of the {@link Annotation} type to retrieve. + * @return The {@link JavaAnnotation} matching the given type. + * Will throw an {@link IllegalArgumentException} if no matching {@link Annotation} is present. + * @see #tryGetAnnotationOfType(String) + * @see #getAnnotationOfType(Class) + */ @PublicAPI(usage = ACCESS) JavaAnnotation getAnnotationOfType(String typeName); + /** + * @param type The {@link Class} of the {@link Annotation} to retrieve. + * @return The {@link Annotation} of the given type or {@link Optional#empty()} + * if there is no {@link Annotation} with the respective annotation type. + * @param The type of the {@link Annotation} to retrieve + * @see #getAnnotationOfType(Class) + * @see #tryGetAnnotationOfType(String) + */ @PublicAPI(usage = ACCESS) Optional tryGetAnnotationOfType(Class type); + /** + * @param typeName The fully qualified class name of the {@link Annotation} type to retrieve. + * @return The {@link JavaAnnotation} matching the given type or {@link Optional#empty()} + * if there is no {@link Annotation} with the respective annotation type. + * @see #getAnnotationOfType(String) + * @see #tryGetAnnotationOfType(Class) + */ @PublicAPI(usage = ACCESS) Optional> tryGetAnnotationOfType(String typeName); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasDescriptor.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasDescriptor.java index 63130a5fe6..21e95c11c0 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasDescriptor.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasModifiers.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasModifiers.java index 52aa51f3a1..d2c7df8ecc 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasModifiers.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasModifiers.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -31,12 +31,13 @@ public interface HasModifiers { /** * Predefined {@link DescribedPredicate predicates} targeting objects that implement {@link HasModifiers} */ + @PublicAPI(usage = ACCESS) final class Predicates { private Predicates() { } @PublicAPI(usage = ACCESS) - public static DescribedPredicate modifier(final JavaModifier modifier) { + public static DescribedPredicate modifier(JavaModifier modifier) { return new ModifierPredicate(modifier); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasName.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasName.java index 85d75693cb..67b7c11fe7 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasName.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasName.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -26,10 +26,12 @@ import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; import static com.tngtech.archunit.core.domain.properties.HasName.Utils.namesOf; +@PublicAPI(usage = ACCESS) public interface HasName { @PublicAPI(usage = ACCESS) String getName(); + @PublicAPI(usage = ACCESS) interface AndFullName extends HasName { /** * @return The full name of the given object. Varies by context, for details consult Javadoc of the concrete subclass. @@ -40,6 +42,7 @@ interface AndFullName extends HasName { /** * Predefined {@link DescribedPredicate predicates} targeting objects that implement {@link HasName.AndFullName} */ + @PublicAPI(usage = ACCESS) final class Predicates { private Predicates() { } @@ -86,6 +89,7 @@ public boolean test(HasName.AndFullName input) { } } + @PublicAPI(usage = ACCESS) final class Functions { private Functions() { } @@ -103,12 +107,13 @@ public String apply(HasName.AndFullName input) { /** * Predefined {@link DescribedPredicate predicates} targeting objects that implement {@link HasName} */ + @PublicAPI(usage = ACCESS) final class Predicates { private Predicates() { } @PublicAPI(usage = ACCESS) - public static DescribedPredicate name(final String name) { + public static DescribedPredicate name(String name) { return new NameEqualsPredicate(name); } @@ -116,22 +121,22 @@ public static DescribedPredicate name(final String name) { * Matches names against a regular expression. */ @PublicAPI(usage = ACCESS) - public static DescribedPredicate nameMatching(final String regex) { + public static DescribedPredicate nameMatching(String regex) { return new NameMatchingPredicate(regex); } @PublicAPI(usage = ACCESS) - public static DescribedPredicate nameStartingWith(final String prefix) { + public static DescribedPredicate nameStartingWith(String prefix) { return new NameStartingWithPredicate(prefix); } @PublicAPI(usage = ACCESS) - public static DescribedPredicate nameContaining(final String infix) { + public static DescribedPredicate nameContaining(String infix) { return new NameContainingPredicate(infix); } @PublicAPI(usage = ACCESS) - public static DescribedPredicate nameEndingWith(final String postfix) { + public static DescribedPredicate nameEndingWith(String postfix) { return new NameEndingWithPredicate(postfix); } @@ -210,6 +215,7 @@ public boolean test(HasName input) { /** * Predefined {@link ChainableFunction functions} to transform {@link HasName}. */ + @PublicAPI(usage = ACCESS) final class Functions { private Functions() { } @@ -231,6 +237,7 @@ public List apply(List input) { }; } + @PublicAPI(usage = ACCESS) final class Utils { private Utils() { } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasOwner.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasOwner.java index 61080ed64d..8d3d66eb40 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasOwner.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasOwner.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -32,6 +32,7 @@ * an annotation parameter is owned by the annotation that declares it, etc. * @param The type of the "owner", e.g. a {@link JavaClass} as the owner of a {@link JavaMember} */ +@PublicAPI(usage = ACCESS) public interface HasOwner { /** * @return The "owner" of this object, compare {@link HasOwner} @@ -47,12 +48,13 @@ final class Predicates { private Predicates() { } + @PublicAPI(usage = ACCESS) public static final class With { private With() { } @PublicAPI(usage = ACCESS) - public static DescribedPredicate> owner(final DescribedPredicate predicate) { + public static DescribedPredicate> owner(DescribedPredicate predicate) { return new OwnerPredicate<>(predicate); } } @@ -80,6 +82,7 @@ final class Functions { private Functions() { } + @PublicAPI(usage = ACCESS) public static final class Get { private Get() { } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasParameterTypes.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasParameterTypes.java index bc72651935..393272be7a 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasParameterTypes.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasParameterTypes.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -30,6 +30,7 @@ import static com.tngtech.archunit.core.domain.Formatters.formatNamesOf; import static com.tngtech.archunit.core.domain.properties.HasName.Functions.GET_NAMES; +@PublicAPI(usage = ACCESS) public interface HasParameterTypes { /** @@ -68,23 +69,23 @@ private Predicates() { } @PublicAPI(usage = ACCESS) - public static DescribedPredicate rawParameterTypes(final Class... types) { + public static DescribedPredicate rawParameterTypes(Class... types) { return rawParameterTypes(formatNamesOf(types)); } @PublicAPI(usage = ACCESS) - public static DescribedPredicate rawParameterTypes(final String... types) { + public static DescribedPredicate rawParameterTypes(String... types) { return rawParameterTypes(ImmutableList.copyOf(types)); } @PublicAPI(usage = ACCESS) - public static DescribedPredicate rawParameterTypes(final List typeNames) { + public static DescribedPredicate rawParameterTypes(List typeNames) { return new RawParameterTypesPredicate(equalTo(typeNames).onResultOf(GET_NAMES) .as("[%s]", formatMethodParameterTypeNames(typeNames))); } @PublicAPI(usage = ACCESS) - public static DescribedPredicate rawParameterTypes(final DescribedPredicate> predicate) { + public static DescribedPredicate rawParameterTypes(DescribedPredicate> predicate) { return new RawParameterTypesPredicate(predicate); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasReturnType.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasReturnType.java index 690b3f714a..5a25ddda03 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasReturnType.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasReturnType.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -25,6 +25,7 @@ import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.name; import static com.tngtech.archunit.core.domain.properties.HasReturnType.Functions.GET_RAW_RETURN_TYPE; +@PublicAPI(usage = ACCESS) public interface HasReturnType { @PublicAPI(usage = ACCESS) @@ -61,6 +62,7 @@ public static DescribedPredicate rawReturnType(DescribedPredicate /** * Predefined {@link ChainableFunction functions} to transform {@link HasReturnType}. */ + @PublicAPI(usage = ACCESS) final class Functions { private Functions() { } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasSourceCodeLocation.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasSourceCodeLocation.java index 2a03c875f9..e42a29aa69 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasSourceCodeLocation.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasSourceCodeLocation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -20,6 +20,7 @@ import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; +@PublicAPI(usage = ACCESS) public interface HasSourceCodeLocation { /** * @return The {@link SourceCodeLocation} of this object, i.e. how to locate the respective object within the set of source files. diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasThrowsClause.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasThrowsClause.java index 89733ee267..34e94cc8d4 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasThrowsClause.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasThrowsClause.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -33,6 +33,7 @@ import static com.tngtech.archunit.core.domain.properties.HasName.Predicates.name; import static com.tngtech.archunit.core.domain.properties.HasType.Functions.GET_RAW_TYPE; +@PublicAPI(usage = ACCESS) public interface HasThrowsClause> { @PublicAPI(usage = ACCESS) ThrowsClause getThrowsClause(); @@ -47,28 +48,28 @@ private Predicates() { @PublicAPI(usage = ACCESS) @SafeVarargs - public static DescribedPredicate> throwsClauseWithTypes(final Class... types) { + public static DescribedPredicate> throwsClauseWithTypes(Class... types) { return throwsClauseWithTypes(formatNamesOf(types)); } @PublicAPI(usage = ACCESS) - public static DescribedPredicate> throwsClauseWithTypes(final String... typeNames) { + public static DescribedPredicate> throwsClauseWithTypes(String... typeNames) { return throwsClauseWithTypes(ImmutableList.copyOf(typeNames)); } @PublicAPI(usage = ACCESS) - public static DescribedPredicate> throwsClauseWithTypes(final List typeNames) { + public static DescribedPredicate> throwsClauseWithTypes(List typeNames) { return throwsClause(equalTo(typeNames).onResultOf(ThrowsClause.Functions.GET_TYPES.then(GET_NAMES)) .as("[%s]", formatThrowsDeclarationTypeNames(typeNames))); } @PublicAPI(usage = ACCESS) - public static DescribedPredicate> throwsClauseContainingType(final Class type) { + public static DescribedPredicate> throwsClauseContainingType(Class type) { return throwsClauseContainingType(type.getName()); } @PublicAPI(usage = ACCESS) - public static DescribedPredicate> throwsClauseContainingType(final String typeName) { + public static DescribedPredicate> throwsClauseContainingType(String typeName) { return throwsClauseContainingType(name(typeName).as(typeName)); } @@ -79,7 +80,7 @@ public static DescribedPredicate> throwsClauseContainingType( } @PublicAPI(usage = ACCESS) - public static DescribedPredicate> throwsClause(final DescribedPredicate> predicate) { + public static DescribedPredicate> throwsClause(DescribedPredicate> predicate) { return new ThrowsTypesPredicate(predicate); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasType.java b/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasType.java index 3bff04498d..e60f723830 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasType.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/domain/properties/HasType.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -19,6 +19,7 @@ import com.tngtech.archunit.base.ChainableFunction; import com.tngtech.archunit.base.DescribedPredicate; import com.tngtech.archunit.core.domain.JavaClass; +import com.tngtech.archunit.core.domain.JavaParameterizedType; import com.tngtech.archunit.core.domain.JavaType; import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; @@ -26,11 +27,24 @@ import static com.tngtech.archunit.core.domain.properties.HasName.Functions.GET_NAME; import static com.tngtech.archunit.core.domain.properties.HasType.Functions.GET_RAW_TYPE; +@PublicAPI(usage = ACCESS) public interface HasType { + /** + * @return The (possibly generic) {@link JavaType} of this object. Refer to the documentation of {@link JavaType} + * for further information. + * @see #getRawType() + */ @PublicAPI(usage = ACCESS) JavaType getType(); + /** + * @return The raw type of this object. This is effectively the same as calling + * {@link #getType()}.{@link JavaType#toErasure() toErasure()}. E.g. given a {@link JavaParameterizedType} + * {@code java.util.List} the raw type (i.e. type erasure) would be the + * {@link JavaClass} {@code java.util.List}. + * @see #getType() + */ @PublicAPI(usage = ACCESS) JavaClass getRawType(); @@ -61,6 +75,7 @@ public static DescribedPredicate rawType(DescribedPredicate imple RawAccessRecordProcessed(RawAccessRecord record, ImportedClasses classes, AccessTargetFactory accessTargetFactory) { this.record = record; this.classes = classes; - targetOwner = this.classes.getOrResolve(record.target.owner.getFullyQualifiedClassName()); + targetOwner = this.classes.getOrResolve(record.getTarget().owner.getFullyQualifiedClassName()); this.accessTargetFactory = accessTargetFactory; - originSupplier = createOriginSupplier(record.caller, classes); + originSupplier = createOriginSupplier(record.getOrigin(), classes); } @Override @@ -257,17 +257,17 @@ public JavaCodeUnit getOrigin() { @Override public TARGET getTarget() { - return accessTargetFactory.create(targetOwner, record.target, classes); + return accessTargetFactory.create(targetOwner, record.getTarget(), classes); } @Override public int getLineNumber() { - return record.lineNumber; + return record.getLineNumber(); } @Override public boolean isDeclaredInLambda() { - return record.declaredInLambda; + return record.isDeclaredInLambda(); } @Override @@ -290,18 +290,8 @@ public AccessType getAccessType() { } } - private static Supplier createOriginSupplier(final CodeUnit origin, final ImportedClasses classes) { - return Suppliers.memoize(() -> Factory.getOrigin(origin, classes)); - } - - private static JavaCodeUnit getOrigin(CodeUnit rawOrigin, ImportedClasses classes) { - for (JavaCodeUnit method : classes.getOrResolve(rawOrigin.getDeclaringClassName()).getCodeUnits()) { - if (rawOrigin.is(method)) { - return method; - } - } - throw new IllegalStateException("Never found a " + JavaCodeUnit.class.getSimpleName() + - " that matches supposed origin " + rawOrigin); + private static Supplier createOriginSupplier(CodeUnit origin, ImportedClasses classes) { + return Suppliers.memoize(() -> origin.resolveFrom(classes)); } private static List getArgumentTypesFrom(String descriptor, ImportedClasses classes) { diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileImportRecord.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileImportRecord.java index 601321194b..65600c28bd 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileImportRecord.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileImportRecord.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -28,11 +28,11 @@ import java.util.stream.Stream; import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ForwardingSet; import com.google.common.collect.HashMultimap; import com.google.common.collect.ListMultimap; import com.google.common.collect.SetMultimap; import com.tngtech.archunit.core.domain.JavaClass; -import com.tngtech.archunit.core.domain.JavaCodeUnit; import com.tngtech.archunit.core.domain.JavaMember; import com.tngtech.archunit.core.domain.JavaMethod; import com.tngtech.archunit.core.importer.DomainBuilders.JavaAnnotationBuilder; @@ -42,7 +42,6 @@ import com.tngtech.archunit.core.importer.DomainBuilders.JavaMethodBuilder; import com.tngtech.archunit.core.importer.DomainBuilders.JavaParameterizedTypeBuilder; import com.tngtech.archunit.core.importer.DomainBuilders.JavaStaticInitializerBuilder; -import com.tngtech.archunit.core.importer.DomainBuilders.TryCatchBlockBuilder; import com.tngtech.archunit.core.importer.RawAccessRecord.CodeUnit; import com.tngtech.archunit.core.importer.RawAccessRecord.MemberSignature; import org.slf4j.Logger; @@ -50,11 +49,13 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkState; -import static com.tngtech.archunit.base.Optionals.stream; import static com.tngtech.archunit.core.importer.JavaClassDescriptorImporter.isLambdaMethodName; import static com.tngtech.archunit.core.importer.JavaClassDescriptorImporter.isSyntheticAccessMethodName; import static com.tngtech.archunit.core.importer.JavaClassDescriptorImporter.isSyntheticEnumSwitchMapFieldName; import static java.util.Collections.emptyList; +import static java.util.Collections.emptySet; +import static java.util.Collections.singleton; +import static java.util.stream.Collectors.toSet; class ClassFileImportRecord { private static final Logger log = LoggerFactory.getLogger(ClassFileImportRecord.class); @@ -76,15 +77,18 @@ class ClassFileImportRecord { private final SetMultimap annotationsByOwner = HashMultimap.create(); private final Map annotationDefaultValuesByOwner = new HashMap<>(); private final EnclosingDeclarationsByInnerClasses enclosingDeclarationsByOwner = new EnclosingDeclarationsByInnerClasses(); - private final SetMultimap tryCatchBlocksByOwner = HashMultimap.create(); private final Set rawFieldAccessRecords = new HashSet<>(); private final Set rawMethodCallRecords = new HashSet<>(); private final Set rawConstructorCallRecords = new HashSet<>(); private final Set rawMethodReferenceRecords = new HashSet<>(); private final Set rawConstructorReferenceRecords = new HashSet<>(); + private final Set rawReferencedClassObjects = new HashSet<>(); + private final Set rawInstanceofChecks = new HashSet<>(); + private final Set rawTryCatchBlocks = new HashSet<>(); private final SyntheticAccessRecorder syntheticLambdaAccessRecorder = createSyntheticLambdaAccessRecorder(); private final SyntheticAccessRecorder syntheticPrivateAccessRecorder = createSyntheticPrivateAccessRecorder(); + private final SyntheticallyResolvedAccessRecords syntheticallyResolvedAccessRecords = new SyntheticallyResolvedAccessRecords(); void setSuperclass(String ownerName, String superclassName) { checkState(!superclassNamesByOwner.containsKey(ownerName), @@ -148,8 +152,8 @@ void setEnclosingCodeUnit(String ownerName, CodeUnit enclosingCodeUnit) { enclosingDeclarationsByOwner.registerEnclosingCodeUnit(ownerName, enclosingCodeUnit); } - void addTryCatchBlocks(String declaringClassName, String methodName, String descriptor, Set tryCatchBlocks) { - tryCatchBlocksByOwner.putAll(getMemberKey(declaringClassName, methodName, descriptor), tryCatchBlocks); + void addTryCatchBlocks(Set tryCatchBlocks) { + rawTryCatchBlocks.addAll(tryCatchBlocks); } Optional getSuperclassFor(String name) { @@ -211,18 +215,14 @@ Optional getEnclosingCodeUnitFor(String ownerName) { return enclosingDeclarationsByOwner.getEnclosingCodeUnit(ownerName); } - Set getTryCatchBlockBuildersFor(JavaCodeUnit codeUnit) { - return tryCatchBlocksByOwner.get(getMemberKey(codeUnit)); - } - void registerFieldAccess(RawAccessRecord.ForField record) { - if (!isSyntheticEnumSwitchMapFieldName(record.target.name)) { + if (!isSyntheticEnumSwitchMapFieldName(record.getTarget().name)) { rawFieldAccessRecords.add(record); } } void registerMethodCall(RawAccessRecord record) { - if (isSyntheticAccessMethodName(record.target.name)) { + if (isSyntheticAccessMethodName(record.getTarget().name)) { syntheticPrivateAccessRecorder.registerSyntheticMethodInvocation(record); } else { rawMethodCallRecords.add(record); @@ -245,52 +245,70 @@ void registerLambdaInvocation(RawAccessRecord record) { syntheticLambdaAccessRecorder.registerSyntheticMethodInvocation(record); } + void registerReferencedClassObject(RawReferencedClassObject referencedClassObject) { + rawReferencedClassObjects.add(referencedClassObject); + } + + void registerInstanceofCheck(RawInstanceofCheck instanceofCheck) { + rawInstanceofChecks.add(instanceofCheck); + } + void forEachRawFieldAccessRecord(Consumer doWithRecord) { - fixSyntheticOrigins( - rawFieldAccessRecords, COPY_RAW_FIELD_ACCESS_RECORD, - syntheticPrivateAccessRecorder, syntheticLambdaAccessRecorder - ).forEach(doWithRecord); + resolveSyntheticOrigins(rawFieldAccessRecords, COPY_RAW_FIELD_ACCESS_RECORD, syntheticPrivateAccessRecorder, syntheticLambdaAccessRecorder) + .forEach(doWithRecord); } void forEachRawMethodCallRecord(Consumer doWithRecord) { - fixSyntheticOrigins( - rawMethodCallRecords, COPY_RAW_ACCESS_RECORD, - syntheticPrivateAccessRecorder, syntheticLambdaAccessRecorder - ).forEach(doWithRecord); + resolveSyntheticOrigins(rawMethodCallRecords, COPY_RAW_ACCESS_RECORD, syntheticPrivateAccessRecorder, syntheticLambdaAccessRecorder) + .forEach(doWithRecord); } void forEachRawConstructorCallRecord(Consumer doWithRecord) { - fixSyntheticOrigins( - rawConstructorCallRecords, COPY_RAW_ACCESS_RECORD, - syntheticLambdaAccessRecorder - ).forEach(doWithRecord); + resolveSyntheticOrigins(rawConstructorCallRecords, COPY_RAW_ACCESS_RECORD, syntheticPrivateAccessRecorder, syntheticLambdaAccessRecorder) + .forEach(doWithRecord); } void forEachRawMethodReferenceRecord(Consumer doWithRecord) { - fixSyntheticOrigins( - rawMethodReferenceRecords, COPY_RAW_ACCESS_RECORD, - syntheticPrivateAccessRecorder, syntheticLambdaAccessRecorder - ).forEach(doWithRecord); + resolveSyntheticOrigins(rawMethodReferenceRecords, COPY_RAW_ACCESS_RECORD, syntheticPrivateAccessRecorder, syntheticLambdaAccessRecorder) + .forEach(doWithRecord); } void forEachRawConstructorReferenceRecord(Consumer doWithRecord) { - fixSyntheticOrigins( - rawConstructorReferenceRecords, COPY_RAW_ACCESS_RECORD, - syntheticLambdaAccessRecorder - ).forEach(doWithRecord); + resolveSyntheticOrigins(rawConstructorReferenceRecords, COPY_RAW_ACCESS_RECORD, syntheticLambdaAccessRecorder) + .forEach(doWithRecord); + } + + void forEachRawReferencedClassObject(Consumer doWithReferencedClassObject) { + resolveSyntheticOrigins(rawReferencedClassObjects, COPY_RAW_REFERENCED_CLASS_OBJECT, syntheticLambdaAccessRecorder) + .forEach(doWithReferencedClassObject); + } + + void forEachRawInstanceofCheck(Consumer doWithInstanceofCheck) { + resolveSyntheticOrigins(rawInstanceofChecks, COPY_RAW_INSTANCEOF_CHECK, syntheticLambdaAccessRecorder) + .forEach(doWithInstanceofCheck); + } + + public void forEachRawTryCatchBlock(Consumer doWithTryCatchBlock) { + resolveSyntheticOrigins(rawTryCatchBlocks, COPY_RAW_TRY_CATCH_BLOCK, syntheticLambdaAccessRecorder) + .map(rawTryCatchBlock -> { + Set fixedAccessesInTryBlock = + resolveSyntheticOrigins( + rawTryCatchBlock.getAccessesInTryBlock(), COPY_RAW_ACCESS_RECORD, + syntheticPrivateAccessRecorder, syntheticLambdaAccessRecorder + ).collect(toSet()); + return RawTryCatchBlock.Builder.from(rawTryCatchBlock).withRawAccessesContainedInTryBlock(fixedAccessesInTryBlock).build(); + }).forEach(doWithTryCatchBlock); } - private Stream fixSyntheticOrigins( - Set rawAccessRecordsIncludingSyntheticAccesses, - Function> createAccessWithNewOrigin, + private Stream resolveSyntheticOrigins( + Set objectsWithCodeUnitOrigins, + Function> copyObjectWithCodeUnitOrigin, SyntheticAccessRecorder... syntheticAccessRecorders ) { - - Stream result = rawAccessRecordsIncludingSyntheticAccesses.stream(); - for (SyntheticAccessRecorder syntheticAccessRecorder : syntheticAccessRecorders) { - result = result.flatMap(access -> stream(syntheticAccessRecorder.fixSyntheticAccess(access, createAccessWithNewOrigin))); - } - return result; + return objectsWithCodeUnitOrigins.stream() + .flatMap(objectWithCodeUnitOrigin -> syntheticallyResolvedAccessRecords.resolveSyntheticOrigin( + objectWithCodeUnitOrigin, copyObjectWithCodeUnitOrigin, syntheticAccessRecorders + )); } void add(JavaClass javaClass) { @@ -302,25 +320,34 @@ Map getClasses() { } private static final Function COPY_RAW_ACCESS_RECORD = - access -> new RawAccessRecord.Builder() - .withCaller(access.caller) - .withTarget(access.target) - .withLineNumber(access.lineNumber) - .withDeclaredInLambda(access.declaredInLambda); + access -> copyInto(new RawAccessRecord.Builder(), access); private static final Function COPY_RAW_FIELD_ACCESS_RECORD = - access -> new RawAccessRecord.ForField.Builder() - .withCaller(access.caller) - .withAccessType(access.accessType) - .withTarget(access.target) - .withLineNumber(access.lineNumber) - .withDeclaredInLambda(access.declaredInLambda); + access -> copyInto(new RawAccessRecord.ForField.Builder(), access) + .withAccessType(access.accessType); + + private static final Function COPY_RAW_REFERENCED_CLASS_OBJECT = + referencedClassObject -> copyInto(new RawReferencedClassObject.Builder(), referencedClassObject); + + private static final Function COPY_RAW_INSTANCEOF_CHECK = + instanceofCheck -> copyInto(new RawInstanceofCheck.Builder(), instanceofCheck); + + private static final Function COPY_RAW_TRY_CATCH_BLOCK = RawTryCatchBlock.Builder::from; + + private static > BUILDER copyInto(BUILDER builder, RawCodeUnitDependency referencedClassObject) { + builder + .withOrigin(referencedClassObject.getOrigin()) + .withTarget(referencedClassObject.getTarget()) + .withLineNumber(referencedClassObject.getLineNumber()) + .withDeclaredInLambda(referencedClassObject.isDeclaredInLambda()); + return builder; + } private static SyntheticAccessRecorder createSyntheticLambdaAccessRecorder() { return new SyntheticAccessRecorder( codeUnit -> isLambdaMethodName(codeUnit.getName()), (accessBuilder, newOrigin) -> accessBuilder - .withCaller(newOrigin) + .withOrigin(newOrigin) .withDeclaredInLambda(true) ); } @@ -328,7 +355,7 @@ private static SyntheticAccessRecorder createSyntheticLambdaAccessRecorder() { private static SyntheticAccessRecorder createSyntheticPrivateAccessRecorder() { return new SyntheticAccessRecorder( codeUnit -> isSyntheticAccessMethodName(codeUnit.getName()), - RawAccessRecord.BaseBuilder::withCaller + HasRawCodeUnitOrigin.Builder::withOrigin ); } @@ -374,57 +401,116 @@ Optional getEnclosingCodeUnit(String ownerName) { } private static class SyntheticAccessRecorder { - private final Map rawSyntheticMethodInvocationRecordsByTarget = new HashMap<>(); + private final SetMultimap rawSyntheticMethodInvocationRecordsByTarget = HashMultimap.create(); private final Predicate isSyntheticOrigin; - private final BiConsumer, CodeUnit> fixOrigin; + private final BiConsumer, CodeUnit> fixOrigin; SyntheticAccessRecorder( Predicate isSyntheticOrigin, - BiConsumer, CodeUnit> fixOrigin + BiConsumer, CodeUnit> fixOrigin ) { this.isSyntheticOrigin = isSyntheticOrigin; this.fixOrigin = fixOrigin; } void registerSyntheticMethodInvocation(RawAccessRecord record) { - rawSyntheticMethodInvocationRecordsByTarget.put(getMemberKey(record.target), record); + rawSyntheticMethodInvocationRecordsByTarget.put(getMemberKey(record.getTarget()), record); } - Optional fixSyntheticAccess( + Set fixSyntheticAccess( ACCESS access, - Function> copyAccess + Function> copyAccess ) { - return isSyntheticOrigin.test(access.caller) + return isSyntheticOrigin.test(access.getOrigin()) ? replaceOriginByFixedOrigin(access, copyAccess) - : Optional.of(access); + : singleton(access); } - private Optional replaceOriginByFixedOrigin( + private Set replaceOriginByFixedOrigin( ACCESS accessFromSyntheticMethod, - Function> copyAccess + Function> copyAccess ) { - RawAccessRecord accessWithCorrectOrigin = findNonSyntheticOriginOf(accessFromSyntheticMethod); - - if (accessWithCorrectOrigin != null) { - RawAccessRecord.BaseBuilder copiedBuilder = copyAccess.apply(accessFromSyntheticMethod); - fixOrigin.accept(copiedBuilder, accessWithCorrectOrigin.caller); - return Optional.of(copiedBuilder.build()); - } else { - log.warn("Could not find matching origin for synthetic method {}.{}|{}", - accessFromSyntheticMethod.target.getDeclaringClassName(), - accessFromSyntheticMethod.target.name, - accessFromSyntheticMethod.target.getDescriptor()); - return Optional.empty(); + Set result = findNonSyntheticOriginOf(accessFromSyntheticMethod) + .map(accessWithCorrectOrigin -> { + HasRawCodeUnitOrigin.Builder copiedBuilder = copyAccess.apply(accessFromSyntheticMethod); + fixOrigin.accept(copiedBuilder, accessWithCorrectOrigin.getOrigin()); + return copiedBuilder.build(); + }) + .collect(toSet()); + + if (result.isEmpty()) { + log.warn("Could not find matching origin for synthetic method {}", accessFromSyntheticMethod); } + + return result; } - private RawAccessRecord findNonSyntheticOriginOf(ACCESS accessFromSyntheticMethod) { - RawAccessRecord result = accessFromSyntheticMethod; - do { - result = rawSyntheticMethodInvocationRecordsByTarget.get(getMemberKey(result.caller)); - } while (result != null && isSyntheticOrigin.test(result.caller)); + private Stream findNonSyntheticOriginOf(ACCESS access) { + return isSyntheticOrigin.test(access.getOrigin()) + ? rawSyntheticMethodInvocationRecordsByTarget.get(getMemberKey(access.getOrigin())).stream().flatMap(this::findNonSyntheticOriginOf) + : Stream.of(access); + } + } - return result; + private static class SyntheticallyResolvedAccessRecords { + private final Map> resolvedAccessRecords = new HashMap<>(); + + Stream resolveSyntheticOrigin( + HAS_RAW_CODE_UNIT_ORIGIN hasRawCodeUnitOrigin, + Function> createWithNewOrigin, + SyntheticAccessRecorder... syntheticAccessRecorders + ) { + ResolvedAccesses resolvedAccesses = this.getResolvedAccessRecordsTyped() + .computeIfAbsent(hasRawCodeUnitOrigin, it -> fixSyntheticAccesses(it, createWithNewOrigin, syntheticAccessRecorders)); + return resolvedAccesses.areUnchanged() ? Stream.of(hasRawCodeUnitOrigin) : resolvedAccesses.stream(); + } + + private static ResolvedAccesses fixSyntheticAccesses( + HAS_RAW_CODE_UNIT_ORIGIN hasRawCodeUnitOrigin, + Function> createWithNewOrigin, + SyntheticAccessRecorder[] syntheticAccessRecorders) { + + Set unresolvedResult = singleton(hasRawCodeUnitOrigin); + Set result = unresolvedResult; + for (SyntheticAccessRecorder syntheticAccessRecorder : syntheticAccessRecorders) { + result = result.stream().flatMap(it -> syntheticAccessRecorder.fixSyntheticAccess(it, createWithNewOrigin).stream()).collect(toSet()); + } + return result.equals(unresolvedResult) ? ResolvedAccesses.unchanged() : new ResolvedAccesses<>(result); + } + + // The type of the key matching the type of the set values is an invariant that we ensure at all times, thus the cast is safe in this limited context + @SuppressWarnings({"unchecked", "rawtypes"}) + private Map> getResolvedAccessRecordsTyped() { + return (Map) resolvedAccessRecords; + } + + /** + * Encapsulates a performance hack to not store any set of (original) accesses in case nothing was resolved. + * I.e. for every method that is not synthetic we don't want to store additional objects to save memory. + * Thus, if nothing was resolved we represent this by the (single) value {@link #UNCHANGED} + */ + private static class ResolvedAccesses extends ForwardingSet { + private static final ResolvedAccesses UNCHANGED = new ResolvedAccesses<>(emptySet()); + + private final Set accesses; + + private ResolvedAccesses(Set accesses) { + this.accesses = accesses; + } + + @Override + protected Set delegate() { + return accesses; + } + + boolean areUnchanged() { + return this == UNCHANGED; + } + + @SuppressWarnings("unchecked") + static ResolvedAccesses unchanged() { + return (ResolvedAccesses) UNCHANGED; + } } } } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileImporter.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileImporter.java index dfbfb04e48..63a43a6a1f 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileImporter.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileImporter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -47,9 +47,9 @@ /** * The central API to import {@link JavaClasses} from compiled Java class files. * Supports various types of {@link Location}, e.g. {@link Path}, - * {@link JarFile} or {@link URL}. The {@link Location Locations} that are scanned can be filtered by passing any number of - * {@link ImportOption} to {@link #withImportOption(ImportOption)}, which will then be ANDed (compare - * {@link ImportOptions}). + * {@link JarFile} or {@link URL}. The {@link Location Locations} that are scanned can be filtered by + * {@link #withImportOption(ImportOption)} or {@link #withImportOptions(Collection)}, + * which will then join the {@link ImportOption ImportOptions} using AND semantics. *

    * Note that information about a class is only complete, if all necessary classes are imported. * For example, if class A is imported, and A accesses class B, @@ -78,6 +78,7 @@ * * @see ArchConfiguration */ +@PublicAPI(usage = ACCESS) public final class ClassFileImporter { private static final Logger LOG = LoggerFactory.getLogger(ClassFileImporter.class); @@ -89,7 +90,11 @@ public ClassFileImporter() { } @PublicAPI(usage = ACCESS) - public ClassFileImporter(ImportOptions importOptions) { + public ClassFileImporter(Collection importOptions) { + this(new ImportOptions().with(importOptions)); + } + + private ClassFileImporter(ImportOptions importOptions) { this.importOptions = importOptions; } @@ -105,6 +110,14 @@ public ClassFileImporter withImportOption(ImportOption option) { return new ClassFileImporter(importOptions.with(option)); } + /** + * Same as {@link #withImportOption(ImportOption)} but takes multiple {@link ImportOption ImportOptions}. + */ + @PublicAPI(usage = ACCESS) + public ClassFileImporter withImportOptions(Collection options) { + return new ClassFileImporter(importOptions.with(options)); + } + /** * Converts the given {@link String} to a {@link Path} and delegates to {@link #importPaths(Collection)}. */ @@ -229,18 +242,7 @@ public JavaClasses importPackagesOf(Collection> classes) { } /** - * Imports classes from the whole classpath without archives (JARs or JRTs). - *

    - * For information about the impact of the imported classes on the evaluation of rules, - * as well as configuration and details, refer to {@link ClassFileImporter}. - */ - @PublicAPI(usage = ACCESS) - public JavaClasses importClasspath() { - return importClasspath(importOptions.with(ImportOption.Predefined.DO_NOT_INCLUDE_ARCHIVES)); - } - - /** - * Imports classes from the whole classpath considering the supplied {@link ImportOptions}.
    + * Imports classes from the whole classpath.
    * Note that ArchUnit does not distinguish between the classpath and the modulepath for Java >= 9, * thus all classes from the classpath or the modulepath will be considered. *

    @@ -248,8 +250,8 @@ public JavaClasses importClasspath() { * as well as configuration and details, refer to {@link ClassFileImporter}. */ @PublicAPI(usage = ACCESS) - public JavaClasses importClasspath(ImportOptions options) { - return new ClassFileImporter(options).importLocations(Locations.inClassPath()); + public JavaClasses importClasspath() { + return importLocations(Locations.inClassPath()); } /** @@ -332,7 +334,7 @@ private void tryAdd(List sources, Location location) { } } - private ClassFileSource unify(final List sources) { + private ClassFileSource unify(List sources) { return Iterables.concat(sources)::iterator; } } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileLocation.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileLocation.java index 5f6168c717..bc92409391 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileLocation.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileLocation.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileProcessor.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileProcessor.java index a43b67d441..a3762bdaf3 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileProcessor.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -34,7 +34,6 @@ import com.tngtech.archunit.core.importer.DomainBuilders.JavaMethodBuilder; import com.tngtech.archunit.core.importer.DomainBuilders.JavaParameterizedTypeBuilder; import com.tngtech.archunit.core.importer.DomainBuilders.JavaStaticInitializerBuilder; -import com.tngtech.archunit.core.importer.DomainBuilders.TryCatchBlockBuilder; import com.tngtech.archunit.core.importer.JavaClassProcessor.AccessHandler; import com.tngtech.archunit.core.importer.RawAccessRecord.CodeUnit; import com.tngtech.archunit.core.importer.RawAccessRecord.TargetInfo; @@ -47,6 +46,7 @@ import org.slf4j.LoggerFactory; import static com.tngtech.archunit.core.domain.JavaConstructor.CONSTRUCTOR_NAME; +import static java.util.stream.Collectors.toSet; import static org.objectweb.asm.Opcodes.ASM9; class ClassFileProcessor { @@ -277,6 +277,26 @@ public void handleLambdaInstruction(String owner, String name, String desc) { importRecord.registerLambdaInvocation(filled(new RawAccessRecord.Builder(), target).build()); } + @Override + public void handleReferencedClassObject(JavaClassDescriptor type, int lineNumber) { + importRecord.registerReferencedClassObject(new RawReferencedClassObject.Builder() + .withOrigin(codeUnit) + .withTarget(type) + .withLineNumber(lineNumber) + .withDeclaredInLambda(false) + .build()); + } + + @Override + public void handleInstanceofCheck(JavaClassDescriptor instanceOfCheckType, int lineNumber) { + importRecord.registerInstanceofCheck(new RawInstanceofCheck.Builder() + .withOrigin(codeUnit) + .withTarget(instanceOfCheckType) + .withLineNumber(lineNumber) + .withDeclaredInLambda(false) + .build()); + } + @Override public void handleTryCatchBlock(Label start, Label end, Label handler, JavaClassDescriptor throwableType) { LOG.trace("Found try/catch block between {} and {} for throwable {}", start, end, throwableType); @@ -295,13 +315,14 @@ public void onMethodEnd() { } @Override - public void onTryCatchBlocksFinished(Set tryCatchBlocks) { - importRecord.addTryCatchBlocks(codeUnit.getDeclaringClassName(), codeUnit.getName(), codeUnit.getDescriptor(), tryCatchBlocks); + public void onTryCatchBlocksFinished(Set tryCatchBlocks) { + tryCatchBlocks.forEach(it -> it.withDeclaringCodeUnit(codeUnit)); + importRecord.addTryCatchBlocks(tryCatchBlocks.stream().map(RawTryCatchBlock.Builder::build).collect(toSet())); } private > BUILDER filled(BUILDER builder, TargetInfo target) { return builder - .withCaller(codeUnit) + .withOrigin(codeUnit) .withTarget(target) .withLineNumber(lineNumber); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileSource.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileSource.java index a0fb4fe304..befbdc0718 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileSource.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassFileSource.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -76,7 +76,7 @@ private boolean shouldBeConsidered(Path file) { && importOptions.include(Location.of(file)); } - private Supplier newInputStreamSupplierFor(final Path file) { + private Supplier newInputStreamSupplierFor(Path file) { return new InputStreamSupplier() { @Override InputStream getInputStream() throws IOException { @@ -108,16 +108,16 @@ class FromJar implements ClassFileSource { } } - private Predicate classFilesBeneath(final NormalizedResourceName prefix) { + private Predicate classFilesBeneath(NormalizedResourceName prefix) { return input -> input.getName().startsWith(prefix.toEntryName()) && FileToImport.isRelevant(input.getName()); } - private Function toClassFilesInJarOf(final JarURLConnection connection) { + private Function toClassFilesInJarOf(JarURLConnection connection) { return input -> new ClassFileInJar(connection, input); } - private Predicate by(final ImportOptions importOptions) { + private Predicate by(ImportOptions importOptions) { return input -> input.isIncludedIn(importOptions); } @@ -147,7 +147,7 @@ private ClassFileInJar(JarURLConnection connection, JarEntry jarEntry) { } private URI makeJarUri(JarEntry input) { - return Location.of(connection.getJarFileURL()).append(input.getName()).asURI(); + return Location.of(connection.getURL()).append(input.getName()).asURI(); } URI getUri() { diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassGraphCreator.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassGraphCreator.java index d2a55d0668..3133fb7778 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassGraphCreator.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/ClassGraphCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -34,6 +34,7 @@ import com.tngtech.archunit.core.domain.AccessTarget.MethodCallTarget; import com.tngtech.archunit.core.domain.AccessTarget.MethodReferenceTarget; import com.tngtech.archunit.core.domain.ImportContext; +import com.tngtech.archunit.core.domain.InstanceofCheck; import com.tngtech.archunit.core.domain.JavaAccess; import com.tngtech.archunit.core.domain.JavaAnnotation; import com.tngtech.archunit.core.domain.JavaClass; @@ -51,6 +52,7 @@ import com.tngtech.archunit.core.domain.JavaStaticInitializer; import com.tngtech.archunit.core.domain.JavaType; import com.tngtech.archunit.core.domain.JavaTypeVariable; +import com.tngtech.archunit.core.domain.ReferencedClassObject; import com.tngtech.archunit.core.importer.AccessRecord.FieldAccessRecord; import com.tngtech.archunit.core.importer.DomainBuilders.JavaClassTypeParametersBuilder; import com.tngtech.archunit.core.importer.DomainBuilders.JavaConstructorCallBuilder; @@ -64,6 +66,7 @@ import com.tngtech.archunit.core.importer.RawAccessRecord.CodeUnit; import com.tngtech.archunit.core.importer.resolvers.ClassResolver; +import static com.google.common.collect.ImmutableSet.toImmutableSet; import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.completeAnnotations; import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.completeClassHierarchy; import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.completeEnclosingDeclaration; @@ -71,7 +74,9 @@ import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.completeGenericSuperclass; import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.completeMembers; import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.completeTypeParameters; +import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createInstanceofCheck; import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createJavaClasses; +import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createReferencedClassObject; import static com.tngtech.archunit.core.importer.DomainBuilders.BuilderWithBuildParameter.BuildFinisher.build; import static com.tngtech.archunit.core.importer.DomainBuilders.buildAnnotations; import static com.tngtech.archunit.core.importer.JavaClassDescriptorImporter.isLambdaMethodName; @@ -88,6 +93,9 @@ class ClassGraphCreator implements ImportContext { private final SetMultimap> processedConstructorCallRecords = HashMultimap.create(); private final SetMultimap> processedMethodReferenceRecords = HashMultimap.create(); private final SetMultimap> processedConstructorReferenceRecords = HashMultimap.create(); + private final SetMultimap processedReferencedClassObjects = HashMultimap.create(); + private final SetMultimap processedInstanceofChecks = HashMultimap.create(); + private final SetMultimap processedTryCatchBlocks = HashMultimap.create(); ClassGraphCreator(ClassFileImportRecord importRecord, DependencyResolutionProcess dependencyResolutionProcess, ClassResolver classResolver) { this.importRecord = importRecord; @@ -98,7 +106,7 @@ class ClassGraphCreator implements ImportContext { JavaClasses complete() { dependencyResolutionProcess.resolve(classes); completeClasses(); - completeAccesses(); + completeCodeUnitDependencies(); return createJavaClasses(classes.getDirectlyImported(), classes.getAllWithOuterClassesSortedBeforeInnerClasses(), this); } @@ -114,7 +122,7 @@ private void completeClasses() { } } - private void completeAccesses() { + private void completeCodeUnitDependencies() { importRecord.forEachRawFieldAccessRecord(record -> tryProcess(record, AccessRecord.Factory.forFieldAccessRecord(), processedFieldAccessRecords)); importRecord.forEachRawMethodCallRecord(record -> @@ -125,6 +133,9 @@ private void completeAccesses() { tryProcess(record, AccessRecord.Factory.forMethodReferenceRecord(), processedMethodReferenceRecords)); importRecord.forEachRawConstructorReferenceRecord(record -> tryProcess(record, AccessRecord.Factory.forConstructorReferenceRecord(), processedConstructorReferenceRecords)); + importRecord.forEachRawReferencedClassObject(this::processReferencedClassObject); + importRecord.forEachRawInstanceofCheck(this::processInstanceofCheck); + importRecord.forEachRawTryCatchBlock(this::processTryCatchBlock); } private , B extends RawAccessRecord> void tryProcess( @@ -136,6 +147,42 @@ private , B extends RawAccessRecord> void tryProcess( processedAccessRecords.put(processed.getOrigin(), processed); } + private void processReferencedClassObject(RawReferencedClassObject rawReferencedClassObject) { + JavaCodeUnit origin = rawReferencedClassObject.getOrigin().resolveFrom(classes); + ReferencedClassObject referencedClassObject = createReferencedClassObject( + origin, + classes.getOrResolve(rawReferencedClassObject.getClassName()), + rawReferencedClassObject.getLineNumber(), + rawReferencedClassObject.isDeclaredInLambda() + ); + processedReferencedClassObjects.put(origin, referencedClassObject); + } + + private void processInstanceofCheck(RawInstanceofCheck rawInstanceofCheck) { + JavaCodeUnit origin = rawInstanceofCheck.getOrigin().resolveFrom(classes); + InstanceofCheck instanceofCheck = createInstanceofCheck( + origin, + classes.getOrResolve(rawInstanceofCheck.getTarget().getFullyQualifiedClassName()), + rawInstanceofCheck.getLineNumber(), + rawInstanceofCheck.isDeclaredInLambda() + ); + processedInstanceofChecks.put(origin, instanceofCheck); + } + + private void processTryCatchBlock(RawTryCatchBlock rawTryCatchBlock) { + JavaCodeUnit declaringCodeUnit = rawTryCatchBlock.getDeclaringCodeUnit().resolveFrom(classes); + TryCatchBlockBuilder tryCatchBlockBuilder = new TryCatchBlockBuilder() + .withCaughtThrowables( + rawTryCatchBlock.getCaughtThrowables().stream() + .map(it -> classes.getOrResolve(it.getFullyQualifiedClassName())) + .collect(toImmutableSet()) + ) + .withLineNumber(rawTryCatchBlock.getLineNumber()) + .withRawAccessesContainedInTryBlock(rawTryCatchBlock.getAccessesInTryBlock()) + .withDeclaredInLambda(rawTryCatchBlock.isDeclaredInLambda()); + processedTryCatchBlocks.put(declaringCodeUnit, tryCatchBlockBuilder); + } + @Override public Set createFieldAccessesFor(JavaCodeUnit codeUnit, Set tryCatchBlockBuilders) { ImmutableSet.Builder result = ImmutableSet.builder(); @@ -331,7 +378,17 @@ public Optional createEnclosingCodeUnit(JavaClass owner) { @Override public Set createTryCatchBlockBuilders(JavaCodeUnit codeUnit) { - return importRecord.getTryCatchBlockBuildersFor(codeUnit); + return processedTryCatchBlocks.get(codeUnit); + } + + @Override + public Set createReferencedClassObjectsFor(JavaCodeUnit codeUnit) { + return ImmutableSet.copyOf(processedReferencedClassObjects.get(codeUnit)); + } + + @Override + public Set createInstanceofChecksFor(JavaCodeUnit codeUnit) { + return ImmutableSet.copyOf(processedInstanceofChecks.get(codeUnit)); } @Override diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/DeclarationHandler.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/DeclarationHandler.java index ce4ed44721..a3c07c0956 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/DeclarationHandler.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/DeclarationHandler.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/DependencyResolutionProcess.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/DependencyResolutionProcess.java index c8adafe049..5f0702e76d 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/DependencyResolutionProcess.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/DependencyResolutionProcess.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -124,7 +124,7 @@ void resolve(ImportedClasses classes) { } private void logConfiguration() { - log.debug("Automatically resolving transitive class dependencies with the following configuration:{}{}{}{}{}{}", + log.trace("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), diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/DomainBuilders.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/DomainBuilders.java index 5b55658889..d1b9f509e7 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/DomainBuilders.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/DomainBuilders.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -43,8 +43,6 @@ import com.tngtech.archunit.core.domain.AccessTarget.MethodReferenceTarget; import com.tngtech.archunit.core.domain.DomainObjectCreationContext; import com.tngtech.archunit.core.domain.Formatters; -import com.tngtech.archunit.core.domain.ImportContext; -import com.tngtech.archunit.core.domain.InstanceofCheck; import com.tngtech.archunit.core.domain.JavaAccess; import com.tngtech.archunit.core.domain.JavaAnnotation; import com.tngtech.archunit.core.domain.JavaClass; @@ -68,7 +66,6 @@ import com.tngtech.archunit.core.domain.JavaType; import com.tngtech.archunit.core.domain.JavaTypeVariable; import com.tngtech.archunit.core.domain.JavaWildcardType; -import com.tngtech.archunit.core.domain.ReferencedClassObject; import com.tngtech.archunit.core.domain.Source; import com.tngtech.archunit.core.domain.SourceCodeLocation; import com.tngtech.archunit.core.domain.ThrowsClause; @@ -81,8 +78,6 @@ import static com.google.common.collect.Sets.union; import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.completeTypeVariable; import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createGenericArrayType; -import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createInstanceofCheck; -import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createReferencedClassObject; import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createSource; import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createThrowsClause; import static com.tngtech.archunit.core.domain.DomainObjectCreationContext.createTryCatchBlock; @@ -95,6 +90,7 @@ import static java.util.stream.Collectors.joining; @Internal +@SuppressWarnings("UnusedReturnValue") public final class DomainBuilders { private DomainBuilders() { } @@ -116,12 +112,12 @@ public static final class JavaEnumConstantBuilder { JavaEnumConstantBuilder() { } - JavaEnumConstantBuilder withDeclaringClass(final JavaClass declaringClass) { + JavaEnumConstantBuilder withDeclaringClass(JavaClass declaringClass) { this.declaringClass = declaringClass; return this; } - JavaEnumConstantBuilder withName(final String name) { + JavaEnumConstantBuilder withName(String name) { this.name = name; return this; } @@ -250,8 +246,6 @@ public abstract static class JavaCodeUnitBuilder parameterAnnotationsByIndex; private JavaCodeUnitTypeParametersBuilder typeParametersBuilder; private List throwsDeclarations; - private final Set rawReferencedClassObjects = new HashSet<>(); - private final List instanceOfChecks = new ArrayList<>(); private JavaCodeUnitBuilder() { } @@ -283,16 +277,6 @@ SELF withThrowsClause(List throwsDeclarations) { return self(); } - SELF addReferencedClassObject(RawReferencedClassObject rawReferencedClassObject) { - rawReferencedClassObjects.add(rawReferencedClassObject); - return self(); - } - - SELF addInstanceOfCheck(RawInstanceofCheck rawInstanceOfChecks) { - this.instanceOfChecks.add(rawInstanceOfChecks); - return self(); - } - String getReturnTypeName() { return rawReturnType.getFullyQualifiedClassName(); } @@ -339,22 +323,6 @@ public ThrowsClause getThrowsClause( return createThrowsClause(codeUnit, asJavaClasses(this.throwsDeclarations)); } - public Set getReferencedClassObjects(JavaCodeUnit codeUnit) { - ImmutableSet.Builder result = ImmutableSet.builder(); - for (RawReferencedClassObject rawReferencedClassObject : this.rawReferencedClassObjects) { - result.add(createReferencedClassObject(codeUnit, get(rawReferencedClassObject.getClassName()), rawReferencedClassObject.getLineNumber())); - } - return result.build(); - } - - public Set getInstanceofChecks(JavaCodeUnit codeUnit) { - ImmutableSet.Builder result = ImmutableSet.builder(); - for (RawInstanceofCheck instanceOfCheck : this.instanceOfChecks) { - result.add(createInstanceofCheck(codeUnit, get(instanceOfCheck.getTarget().getFullyQualifiedClassName()), instanceOfCheck.getLineNumber())); - } - return result.build(); - } - private List asJavaClasses(List descriptors) { ImmutableList.Builder result = ImmutableList.builder(); for (JavaClassDescriptor javaClassDescriptor : descriptors) { @@ -401,7 +369,7 @@ JavaMethodBuilder withAnnotationDefaultValue(Function JavaAnnotation build(T owner, ImportedClass abstract static class ValueBuilder { abstract Optional build(T owner, ImportedClasses importedClasses); - static ValueBuilder fromPrimitiveProperty(final Object value) { + static ValueBuilder fromPrimitiveProperty(Object value) { return new ValueBuilder() { @Override Optional build(T owner, ImportedClasses unused) { @@ -595,7 +565,7 @@ Optional build(T owner, ImportedClasses unuse }; } - public static ValueBuilder fromEnumProperty(final JavaClassDescriptor enumType, final String value) { + public static ValueBuilder fromEnumProperty(JavaClassDescriptor enumType, String value) { return new ValueBuilder() { @Override Optional build(T owner, ImportedClasses importedClasses) { @@ -608,7 +578,7 @@ Optional build(T owner, ImportedClasses impor }; } - static ValueBuilder fromClassProperty(final JavaClassDescriptor value) { + static ValueBuilder fromClassProperty(JavaClassDescriptor value) { return new ValueBuilder() { @Override Optional build(T owner, ImportedClasses importedClasses) { @@ -617,7 +587,7 @@ Optional build(T owner, ImportedClasses impor }; } - static ValueBuilder fromAnnotationProperty(final JavaAnnotationBuilder builder) { + static ValueBuilder fromAnnotationProperty(JavaAnnotationBuilder builder) { return new ValueBuilder() { @Override Optional build(T owner, ImportedClasses importedClasses) { @@ -656,7 +626,7 @@ private JavaTypeFinisher() { abstract String getFinishedName(String name); - JavaTypeFinisher after(final JavaTypeFinisher other) { + JavaTypeFinisher after(JavaTypeFinisher other) { return new JavaTypeFinisher() { @Override JavaType finish(JavaType input, ImportedClasses classes) { @@ -670,7 +640,7 @@ String getFinishedName(String name) { }; } - static JavaTypeFinisher IDENTITY = new JavaTypeFinisher() { + static final JavaTypeFinisher IDENTITY = new JavaTypeFinisher() { @Override JavaType finish(JavaType input, ImportedClasses classes) { return input; @@ -733,7 +703,7 @@ public List getUpperBounds(Iterable> all } } - private static abstract class AbstractTypeParametersBuilder { + private abstract static class AbstractTypeParametersBuilder { private final List> typeParameterBuilders; AbstractTypeParametersBuilder(List> typeParameterBuilders) { @@ -926,17 +896,17 @@ static Set build( @Internal public static class TryCatchBlockBuilder { - private Set caughtThrowables; + private Set caughtThrowables; private int lineNumber; private JavaCodeUnit owner; - private ImportContext context; private final Set> accessesContainedInTryBlock = new HashSet<>(); private Set rawAccessesContainedInTryBlock; + private boolean declaredInLambda; TryCatchBlockBuilder() { } - TryCatchBlockBuilder withCaughtThrowables(Set caughtThrowables) { + TryCatchBlockBuilder withCaughtThrowables(Set caughtThrowables) { this.caughtThrowables = caughtThrowables; return this; } @@ -946,7 +916,7 @@ TryCatchBlockBuilder withLineNumber(int lineNumber) { return this; } - TryCatchBlockBuilder withRawAccessesInTryBlock(Set accessRecords) { + TryCatchBlockBuilder withRawAccessesContainedInTryBlock(Set accessRecords) { this.rawAccessesContainedInTryBlock = accessRecords; return this; } @@ -957,9 +927,13 @@ void addIfContainedInTryBlock(RawAccessRecord rawRecord, JavaAccess access) { } } - public TryCatchBlock build(JavaCodeUnit owner, ImportContext context) { + TryCatchBlockBuilder withDeclaredInLambda(boolean declaredInLambda) { + this.declaredInLambda = declaredInLambda; + return this; + } + + public TryCatchBlock build(JavaCodeUnit owner) { this.owner = owner; - this.context = context; return createTryCatchBlock(this); } @@ -972,14 +946,16 @@ public Set> getAccessesContainedInTryBlock() { } public Set getCaughtThrowables() { - return caughtThrowables.stream() - .map(throwable -> context.resolveClass(throwable.getFullyQualifiedClassName())) - .collect(toImmutableSet()); + return caughtThrowables; } public SourceCodeLocation getSourceCodeLocation() { return SourceCodeLocation.of(owner.getOwner(), lineNumber); } + + public boolean isDeclaredInLambda() { + return declaredInLambda; + } } @Internal @@ -992,17 +968,17 @@ public abstract static class JavaAccessBuilder> { + public abstract static class AccessTargetBuilder> { private final Function createTarget; private JavaClass owner; @@ -1107,12 +1083,12 @@ public static abstract class AccessTargetBuilder withParameters(final List parameters) { + CodeUnitAccessTargetBuilder withParameters(List parameters) { this.parameters = parameters; return self(); } - CodeUnitAccessTargetBuilder withReturnType(final JavaClass returnType) { + CodeUnitAccessTargetBuilder withReturnType(JavaClass returnType) { this.returnType = returnType; return self(); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/GenericMemberTypeProcessor.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/GenericMemberTypeProcessor.java index 55191667f9..28868a5cd9 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/GenericMemberTypeProcessor.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/GenericMemberTypeProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/library/plantuml/ComponentIntersectionException.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/HasRawCodeUnitOrigin.java similarity index 54% rename from archunit/src/main/java/com/tngtech/archunit/library/plantuml/ComponentIntersectionException.java rename to archunit/src/main/java/com/tngtech/archunit/core/importer/HasRawCodeUnitOrigin.java index 3f68e6804c..c15cde6c81 100644 --- a/archunit/src/main/java/com/tngtech/archunit/library/plantuml/ComponentIntersectionException.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/HasRawCodeUnitOrigin.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -13,10 +13,19 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.tngtech.archunit.library.plantuml; +package com.tngtech.archunit.core.importer; -class ComponentIntersectionException extends RuntimeException { - ComponentIntersectionException(String format) { - super(format); +interface HasRawCodeUnitOrigin { + RawAccessRecord.CodeUnit getOrigin(); + + boolean isDeclaredInLambda(); + + interface Builder { + + Builder withOrigin(RawAccessRecord.CodeUnit origin); + + Builder withDeclaredInLambda(boolean declaredInLambda); + + HAS_RAW_CODE_UNIT_ORIGIN build(); } } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/ImportOption.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/ImportOption.java index 8ed2f974f4..613d614a1d 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/ImportOption.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/ImportOption.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -56,6 +56,17 @@ public boolean includes(Location location) { return onlyIncludeTests.includes(location); } }, + /** + * @see DoNotIncludeGradleTestFixtures + */ + DO_NOT_INCLUDE_TEST_FIXTURES { + private final DoNotIncludeGradleTestFixtures doNotIncludeGradleTestFixtures = new DoNotIncludeGradleTestFixtures(); + + @Override + public boolean includes(Location location) { + return doNotIncludeGradleTestFixtures.includes(location); + } + }, DO_NOT_INCLUDE_JARS { private final DoNotIncludeJars doNotIncludeJars = new DoNotIncludeJars(); @@ -132,6 +143,23 @@ public boolean includes(Location location) { } } + /** + * Best effort {@link ImportOption} to omit checking test fixtures defined by the + * Gradle Test Fixtures Plugin.
    + * NOTE: This excludes all class files residing in some directory + * ../build/classes/../testFixtures/.. or some JAR matching ../build/libs/..-test-fixtures.jar + * (the former as it would be added from the file system to the classpath, the latter as it would be added as a JAR library to the classpath) + */ + final class DoNotIncludeGradleTestFixtures implements ImportOption { + private static final Pattern TEST_FIXTURES_FILE_PATH_PATTERN = Pattern.compile(".*/build/classes/.*/testFixtures/.*"); + private static final Pattern TEST_FIXTURES_JAR_PATH_PATTERN = Pattern.compile(".*/build/libs/.*-test-fixtures.jar!.*"); + + @Override + public boolean includes(Location location) { + return !location.matches(TEST_FIXTURES_FILE_PATH_PATTERN) && !location.matches(TEST_FIXTURES_JAR_PATH_PATTERN); + } + } + final class DoNotIncludeJars implements ImportOption { @Override public boolean includes(Location location) { diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/ImportOptions.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/ImportOptions.java index 3b412addfc..a67a26d00e 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/ImportOptions.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/ImportOptions.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -15,25 +15,19 @@ */ package com.tngtech.archunit.core.importer; +import java.util.Collection; import java.util.Set; import com.google.common.collect.ImmutableSet; -import com.tngtech.archunit.PublicAPI; +import com.google.common.collect.Sets; import static com.google.common.base.Preconditions.checkNotNull; -import static com.tngtech.archunit.PublicAPI.Usage.ACCESS; import static java.util.Collections.emptySet; -/** - * A collection of {@link ImportOption} to filter class locations. All supplied {@link ImportOption}s will be joined - * with AND, i.e. only {@link Location}s that are accepted by all {@link ImportOption}s - * will be imported. - */ -public final class ImportOptions { +final class ImportOptions { private final Set options; - @PublicAPI(usage = ACCESS) - public ImportOptions() { + ImportOptions() { this(emptySet()); } @@ -41,15 +35,14 @@ private ImportOptions(Set options) { this.options = checkNotNull(options); } - /** - * @param option An {@link ImportOption} to evaluate on {@link Location}s of class files - * @return self to add further {@link ImportOption}s in a fluent way - */ - @PublicAPI(usage = ACCESS) - public ImportOptions with(ImportOption option) { + ImportOptions with(ImportOption option) { return new ImportOptions(ImmutableSet.builder().addAll(options).add(option).build()); } + ImportOptions with(Collection options) { + return new ImportOptions(Sets.union(this.options, ImmutableSet.copyOf(options))); + } + boolean include(Location location) { return options.stream().allMatch(option -> option.includes(location)); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/ImportPlugin.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/ImportPlugin.java index 7013dfd8f3..3c38914005 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/ImportPlugin.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/ImportPlugin.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -51,7 +51,7 @@ public void plugInLocationFactories(InitialConfiguration> @Override public void plugInLocationResolver(InitialConfiguration locationResolver) { - locationResolver.set(new LocationResolver.Legacy()); + locationResolver.set(new LocationResolver.FromClasspathAndUrlClassLoaders()); } } } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/ImportedClasses.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/ImportedClasses.java index 71d1d91b06..f7093af042 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/ImportedClasses.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/ImportedClasses.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -19,6 +19,7 @@ import java.util.HashMap; import java.util.Map; import java.util.Optional; +import java.util.Set; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -30,15 +31,20 @@ import com.tngtech.archunit.core.importer.DomainBuilders.JavaClassBuilder; import com.tngtech.archunit.core.importer.resolvers.ClassResolver; +import static com.google.common.collect.Sets.immutableEnumSet; import static com.tngtech.archunit.core.domain.JavaModifier.ABSTRACT; import static com.tngtech.archunit.core.domain.JavaModifier.FINAL; +import static com.tngtech.archunit.core.domain.JavaModifier.PRIVATE; +import static com.tngtech.archunit.core.domain.JavaModifier.PROTECTED; import static com.tngtech.archunit.core.domain.JavaModifier.PUBLIC; import static com.tngtech.archunit.core.importer.ImportedClasses.ImportedClassState.HAD_TO_BE_IMPORTED; import static com.tngtech.archunit.core.importer.ImportedClasses.ImportedClassState.WAS_ALREADY_PRESENT; class ImportedClasses { - private static final ImmutableSet PRIMITIVE_AND_ARRAY_TYPE_MODIFIERS = - Sets.immutableEnumSet(PUBLIC, ABSTRACT, FINAL); + private static final ImmutableSet PRIMITIVE_TYPE_MODIFIERS = + immutableEnumSet(PUBLIC, ABSTRACT, FINAL); + private static final ImmutableSet VISIBILITY_MODIFIERS = + immutableEnumSet(PUBLIC, PROTECTED, PRIVATE); private final ImmutableMap directlyImported; private final Map allClasses = new HashMap<>(); @@ -92,19 +98,37 @@ Collection getAllWithOuterClassesSortedBeforeInnerClasses() { return ImmutableSortedMap.copyOf(allClasses).values(); } - private static JavaClass stubClassOf(String typeName) { + private JavaClass stubClassOf(String typeName) { JavaClassDescriptor descriptor = JavaClassDescriptor.From.name(typeName); JavaClassBuilder builder = JavaClassBuilder.forStub().withDescriptor(descriptor); addModifiersIfPossible(builder, descriptor); return builder.build(); } - private static void addModifiersIfPossible(JavaClassBuilder builder, JavaClassDescriptor descriptor) { - if (descriptor.isPrimitive() || descriptor.isArray()) { - builder.withModifiers(PRIMITIVE_AND_ARRAY_TYPE_MODIFIERS); + /** + * See {@link Class#getModifiers()} + */ + private void addModifiersIfPossible(JavaClassBuilder builder, JavaClassDescriptor descriptor) { + if (descriptor.isPrimitive()) { + builder.withModifiers(PRIMITIVE_TYPE_MODIFIERS); + } else if (descriptor.isArray()) { + JavaClass elementType = getOrResolve(getElementType(descriptor).getFullyQualifiedClassName()); + Set modifiers = ImmutableSet.builder() + .addAll(getVisibility(elementType)) + .add(ABSTRACT, FINAL) + .build(); + builder.withModifiers(modifiers); } } + private JavaClassDescriptor getElementType(JavaClassDescriptor descriptor) { + return descriptor.tryGetComponentType().map(this::getElementType).orElse(descriptor); + } + + private Set getVisibility(JavaClass javaClass) { + return Sets.intersection(VISIBILITY_MODIFIERS, javaClass.getModifiers()); + } + public Optional getMethodReturnType(String declaringClassName, String methodName) { return getMethodReturnType.getReturnType(declaringClassName, methodName); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaClassDescriptorImporter.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaClassDescriptorImporter.java index e79835cafc..0a6ffa0030 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaClassDescriptorImporter.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaClassDescriptorImporter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaClassProcessor.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaClassProcessor.java index 7a7a00e741..9821079852 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaClassProcessor.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaClassProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -27,7 +27,6 @@ import com.google.common.collect.HashMultimap; import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableSet; import com.google.common.collect.SetMultimap; import com.google.common.primitives.Booleans; import com.google.common.primitives.Bytes; @@ -58,7 +57,6 @@ import org.objectweb.asm.Label; import org.objectweb.asm.MethodVisitor; import org.objectweb.asm.Opcodes; -import org.objectweb.asm.RecordComponentVisitor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -71,7 +69,6 @@ import static com.tngtech.archunit.core.importer.JavaClassDescriptorImporter.isAsmMethodHandle; import static com.tngtech.archunit.core.importer.JavaClassDescriptorImporter.isLambdaMetafactory; import static com.tngtech.archunit.core.importer.JavaClassDescriptorImporter.isLambdaMethod; -import static com.tngtech.archunit.core.importer.RawInstanceofCheck.from; import static java.util.Arrays.stream; import static java.util.stream.Collectors.toList; @@ -115,6 +112,7 @@ public void visit(int version, int access, String name, String signature, String boolean opCodeForInterfaceIsPresent = (access & Opcodes.ACC_INTERFACE) != 0; boolean opCodeForEnumIsPresent = (access & Opcodes.ACC_ENUM) != 0; boolean opCodeForAnnotationIsPresent = (access & Opcodes.ACC_ANNOTATION) != 0; + boolean opCodeForRecordIsPresent = (access & Opcodes.ACC_RECORD) != 0; Optional superclassName = getSuperclassName(superName, opCodeForInterfaceIsPresent); LOG.trace("Found superclass {} on class '{}'", superclassName.orElse(null), name); @@ -124,7 +122,8 @@ public void visit(int version, int access, String name, String signature, String .withInterface(opCodeForInterfaceIsPresent) .withEnum(opCodeForEnumIsPresent) .withAnnotation(opCodeForAnnotationIsPresent) - .withModifiers(JavaModifier.getModifiersForClass(access)); + .withModifiers(JavaModifier.getModifiersForClass(access)) + .withRecord(opCodeForRecordIsPresent); className = descriptor.getFullyQualifiedClassName(); declarationHandler.onNewClass(className, superclassName, interfaceNames); @@ -154,21 +153,6 @@ public void visitSource(String source, String debug) { } } - @Override - public RecordComponentVisitor visitRecordComponent(String name, String descriptor, String signature) { - javaClassBuilder.withRecord(true); - - // Records are implicitly static and final (compare JLS 8.10 Record Declarations) - // Thus we ensure that those modifiers are always present (the access flag in visit(..) does not contain STATIC) - ImmutableSet recordModifiers = ImmutableSet.builder() - .addAll(javaClassBuilder.getModifiers()) - .add(JavaModifier.STATIC, JavaModifier.FINAL) - .build(); - javaClassBuilder.withModifiers(recordModifiers); - - return super.visitRecordComponent(name, descriptor, signature); - } - @Override public void visitInnerClass(String name, String outerName, String innerName, int access) { if (importAborted()) { @@ -373,7 +357,7 @@ public void visitLabel(Label label) { public void visitLdcInsn(Object value) { if (JavaClassDescriptorImporter.isAsmType(value)) { JavaClassDescriptor type = JavaClassDescriptorImporter.importAsmType(value); - codeUnitBuilder.addReferencedClassObject(RawReferencedClassObject.from(type, actualLineNumber)); + accessHandler.handleReferencedClassObject(type, actualLineNumber); declarationHandler.onDeclaredClassObject(type.getFullyQualifiedClassName()); } } @@ -401,7 +385,7 @@ public void visitMethodInsn(int opcode, String owner, String name, String desc, public void visitTypeInsn(int opcode, String type) { if (opcode == Opcodes.INSTANCEOF) { JavaClassDescriptor instanceOfCheckType = JavaClassDescriptorImporter.createFromAsmObjectTypeName(type); - codeUnitBuilder.addInstanceOfCheck(from(instanceOfCheckType, actualLineNumber)); + accessHandler.handleInstanceofCheck(instanceOfCheckType, actualLineNumber); declarationHandler.onDeclaredInstanceofCheck(instanceOfCheckType.getFullyQualifiedClassName()); } } @@ -417,12 +401,7 @@ public AnnotationVisitor visitAnnotationDefault() { } @Override - public void visitInvokeDynamicInsn( - final String name, - final String descriptor, - final Handle bootstrapMethodHandle, - final Object... bootstrapMethodArguments - ) { + public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootstrapMethodHandle, Object... bootstrapMethodArguments) { if (isLambdaMetafactory(bootstrapMethodHandle.getOwner())) { Object methodHandleCandidate = bootstrapMethodArguments[1]; if (isAsmMethodHandle(methodHandleCandidate)) { @@ -540,6 +519,10 @@ interface AccessHandler { void handleLambdaInstruction(String owner, String name, String desc); + void handleReferencedClassObject(JavaClassDescriptor type, int lineNumber); + + void handleInstanceofCheck(JavaClassDescriptor instanceOfCheckType, int lineNumber); + void handleTryCatchBlock(Label start, Label end, Label handler, JavaClassDescriptor throwableType); void handleTryFinallyBlock(Label start, Label end, Label handler); @@ -576,6 +559,14 @@ public void handleMethodReferenceInstruction(String owner, String name, String d public void handleLambdaInstruction(String owner, String name, String desc) { } + @Override + public void handleReferencedClassObject(JavaClassDescriptor type, int lineNumber) { + } + + @Override + public void handleInstanceofCheck(JavaClassDescriptor instanceOfCheckType, int lineNumber) { + } + @Override public void handleTryCatchBlock(Label start, Label end, Label handler, JavaClassDescriptor throwableType) { } @@ -640,12 +631,12 @@ public void visitEnum(String name, String desc, String value) { } @Override - public AnnotationVisitor visitAnnotation(final String name, String desc) { + public AnnotationVisitor visitAnnotation(String name, String desc) { return new AnnotationProcessor(addAnnotationAsProperty(name, this.annotationBuilder), declarationHandler, handleAnnotationAnnotationProperty(desc, declarationHandler)); } @Override - public AnnotationVisitor visitArray(final String name) { + public AnnotationVisitor visitArray(String name) { return new AnnotationArrayProcessor(new AnnotationArrayContext() { @Override public String getDeclaringAnnotationTypeName() { @@ -670,11 +661,11 @@ public void visitEnd() { } } - private static TakesAnnotationBuilder addAnnotationAtIndex(final SetMultimap annotations, final int index) { + private static TakesAnnotationBuilder addAnnotationAtIndex(SetMultimap annotations, int index) { return annotation -> annotations.put(index, annotation); } - private static TakesAnnotationBuilder addAnnotationAsProperty(final String name, final JavaAnnotationBuilder annotationBuilder) { + private static TakesAnnotationBuilder addAnnotationAsProperty(String name, JavaAnnotationBuilder annotationBuilder) { return builder -> annotationBuilder.addProperty(name, ValueBuilder.fromAnnotationProperty(builder)); } @@ -716,7 +707,7 @@ public AnnotationVisitor visitAnnotation(String name, String desc) { } @Override - public void visitEnum(String name, final String desc, final String value) { + public void visitEnum(String name, String desc, String value) { setDerivedComponentType(JavaEnumConstant.class); values.add(handleAnnotationEnumProperty(desc, value, declarationHandler)); } @@ -856,7 +847,7 @@ private abstract static class ClassAndPrimitiveDistinguishingAnnotationVisitor e @Override public final void visit(String name, Object input) { - final Object value = JavaClassDescriptorImporter.importAsmTypeIfPossible(input); + Object value = JavaClassDescriptorImporter.importAsmTypeIfPossible(input); if (value instanceof JavaClassDescriptor) { visitClass(name, (JavaClassDescriptor) value); } else { diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaClassSignatureImporter.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaClassSignatureImporter.java index 42a14c31f3..68eaaf6c3e 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaClassSignatureImporter.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaClassSignatureImporter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaCodeUnitSignatureImporter.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaCodeUnitSignatureImporter.java index 443c7135c8..49ebb3ff1d 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaCodeUnitSignatureImporter.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaCodeUnitSignatureImporter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaFieldTypeSignatureImporter.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaFieldTypeSignatureImporter.java index 42779472c7..eabdf4c2e5 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaFieldTypeSignatureImporter.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/JavaFieldTypeSignatureImporter.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/Location.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/Location.java index ce97b6110f..cb2f018dc6 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/Location.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/Location.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -17,6 +17,7 @@ import java.io.File; import java.io.IOException; +import java.net.JarURLConnection; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; @@ -31,6 +32,7 @@ import java.util.Enumeration; import java.util.List; import java.util.Objects; +import java.util.Optional; import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.jar.JarEntry; @@ -61,6 +63,7 @@ *
  • jar:file:///home/someuser/.m2/repository/myproject/foolib.jar!/myproject/Foo.class
  • * */ +@PublicAPI(usage = ACCESS) public abstract class Location { private static final InitialConfiguration> factories = new InitialConfiguration<>(); @@ -167,7 +170,7 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } - final Location other = (Location) obj; + Location other = (Location) obj; return Objects.equals(this.uri, other.uri); } @@ -268,8 +271,8 @@ private static URI newJarUri(URI uri) { @Override ClassFileSource asClassFileSource(ImportOptions importOptions) { try { - String[] parts = uri.toString().split("!/", 2); - return new ClassFileSource.FromJar(new URL(parts[0] + "!/"), parts[1], importOptions); + ParsedUri parsedUri = ParsedUri.from(uri); + return new ClassFileSource.FromJar(new URL(parsedUri.base), parsedUri.path, importOptions); } catch (IOException e) { throw new LocationException(e); } @@ -287,28 +290,26 @@ public boolean isArchive() { @Override Collection readResourceEntries() { - File file = getFileOfJar(); - if (!file.exists()) { - return emptySet(); - } - - return readJarFileContent(file); + return getJarFile().map(this::readJarFileContent).orElse(emptySet()); } - private File getFileOfJar() { - return new File(URI.create(uri.toString() - .replaceAll("^" + SCHEME + ":", "") - .replaceAll("!/.*", ""))); + private Optional getJarFile() { + try { + // Note: We can't use a composed JAR URL like `jar:file:/path/to/file.jar!/com/example`, because opening the connection + // fails with an exception if the directory entry for this path is missing (which is possible, even if there is + // a class `com.example.SomeClass` in the JAR file). + String baseUri = ParsedUri.from(uri).base; + JarURLConnection jarUrlConnection = (JarURLConnection) new URL(baseUri).openConnection(); + return Optional.of(jarUrlConnection.getJarFile()); + } catch (IOException e) { + return Optional.empty(); + } } - private Collection readJarFileContent(File fileOfJar) { + private Collection readJarFileContent(JarFile jarFile) { ImmutableList.Builder result = ImmutableList.builder(); - String prefix = uri.toString().replaceAll(".*!/", ""); - try (JarFile jarFile = new JarFile(fileOfJar)) { - result.addAll(readEntries(prefix, jarFile)); - } catch (IOException e) { - throw new LocationException(e); - } + String prefix = ParsedUri.from(uri).path; + result.addAll(readEntries(prefix, jarFile)); return result.build(); } @@ -323,6 +324,22 @@ private List readEntries(String prefix, JarFile jarFile) } return result; } + + private static class ParsedUri { + final String base; + final String path; + + private ParsedUri(String base, String path) { + this.base = base; + this.path = path; + } + + static ParsedUri from(NormalizedUri uri) { + String uriString = uri.toString(); + int entryPathStartIndex = uriString.lastIndexOf("!/") + 2; + return new ParsedUri(uriString.substring(0, entryPathStartIndex), uriString.substring(entryPathStartIndex)); + } + } } private static class FilePathLocation extends Location { @@ -374,8 +391,8 @@ private List getAllFilesBeneath(NormalizedUri uri) throw return getAllFilesBeneath(rootFile.toPath()); } - private List getAllFilesBeneath(final Path root) throws IOException { - final ImmutableList.Builder result = ImmutableList.builder(); + private List getAllFilesBeneath(Path root) throws IOException { + ImmutableList.Builder result = ImmutableList.builder(); Files.walkFileTree(root, new SimpleFileVisitor() { @Override public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) { diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/LocationResolver.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/LocationResolver.java index 7f16b89f88..a7a8efaacb 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/LocationResolver.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/LocationResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -28,7 +28,7 @@ interface LocationResolver { UrlSource resolveClassPath(); @Internal - class Legacy implements LocationResolver { + class FromClasspathAndUrlClassLoaders implements LocationResolver { @Override public UrlSource resolveClassPath() { ImmutableList.Builder result = ImmutableList.builder(); diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/Locations.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/Locations.java index 220e9857c6..1ec2cc1f11 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/Locations.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/Locations.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -35,6 +35,7 @@ * Represents a set of {@link Location locations} of Java class files. Also offers methods to derive concrete locations (i.e. URIs) from * higher level concepts like packages or the classpath. */ +@PublicAPI(usage = ACCESS) public final class Locations { private static final InitialConfiguration locationResolver = new InitialConfiguration<>(); diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/NormalizedResourceName.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/NormalizedResourceName.java index 7b391b5899..06991cfff8 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/NormalizedResourceName.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/NormalizedResourceName.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -38,8 +38,8 @@ boolean isStartOf(String string) { return string.startsWith(resourceName); } - public boolean startsWith(NormalizedResourceName prefix) { - return equals(prefix) || isAncestorPath(prefix); + boolean startsWith(NormalizedResourceName prefix) { + return prefix.resourceName.isEmpty() || equals(prefix) || isAncestorPath(prefix); } private boolean isAncestorPath(NormalizedResourceName prefix) { @@ -59,10 +59,6 @@ String toAbsolutePath() { return result; } - boolean belongsToClassFile() { - return resourceName.endsWith(".class"); - } - /** * @return The resourceName as if it was an entry of an archive * (i.e. not starting with '/', but ending with '/' in case of directories) @@ -84,7 +80,7 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } - final NormalizedResourceName other = (NormalizedResourceName) obj; + NormalizedResourceName other = (NormalizedResourceName) obj; return Objects.equals(this.resourceName, other.resourceName); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/NormalizedUri.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/NormalizedUri.java index 0c69c8d86c..e281e6b4fd 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/NormalizedUri.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/NormalizedUri.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -66,7 +66,7 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } - final NormalizedUri other = (NormalizedUri) obj; + NormalizedUri other = (NormalizedUri) obj; return Objects.equals(this.uri, other.uri); } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/RawAccessRecord.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/RawAccessRecord.java index ea9432b28a..1399b89028 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/RawAccessRecord.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/RawAccessRecord.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -25,26 +25,46 @@ import static com.google.common.base.Preconditions.checkNotNull; -class RawAccessRecord { - final CodeUnit caller; - final TargetInfo target; - final int lineNumber; - public boolean declaredInLambda; +class RawAccessRecord implements RawCodeUnitDependency { + private final CodeUnit origin; + private final TargetInfo target; + private final int lineNumber; + private final boolean declaredInLambda; - RawAccessRecord(CodeUnit caller, TargetInfo target, int lineNumber, boolean declaredInLambda) { - this.caller = checkNotNull(caller); + RawAccessRecord(CodeUnit origin, TargetInfo target, int lineNumber, boolean declaredInLambda) { + this.origin = checkNotNull(origin); this.target = checkNotNull(target); this.lineNumber = lineNumber; this.declaredInLambda = declaredInLambda; } + @Override + public CodeUnit getOrigin() { + return origin; + } + + @Override + public TargetInfo getTarget() { + return target; + } + + @Override + public int getLineNumber() { + return lineNumber; + } + + @Override + public boolean isDeclaredInLambda() { + return declaredInLambda; + } + @Override public String toString() { return getClass().getSimpleName() + "{" + fieldsAsString() + '}'; } private String fieldsAsString() { - return "caller=" + caller + ", target=" + target + ", lineNumber=" + lineNumber; + return "origin=" + origin + ", target=" + target + ", lineNumber=" + lineNumber + ", declaredInLambda=" + declaredInLambda; } interface MemberSignature { @@ -103,7 +123,17 @@ public String getDeclaringClassName() { return declaringClassName; } - boolean is(JavaCodeUnit method) { + JavaCodeUnit resolveFrom(ImportedClasses classes) { + for (JavaCodeUnit method : classes.getOrResolve(getDeclaringClassName()).getCodeUnits()) { + if (is(method)) { + return method; + } + } + throw new IllegalStateException("Never found a " + JavaCodeUnit.class.getSimpleName() + + " that matches supposed origin " + this); + } + + private boolean is(JavaCodeUnit method) { return getName().equals(method.getName()) && descriptor.equals(method.getDescriptor()) && getDeclaringClassName().equals(method.getOwner().getName()); @@ -181,7 +211,7 @@ public boolean equals(Object obj) { if (obj == null || getClass() != obj.getClass()) { return false; } - final TargetInfo other = (TargetInfo) obj; + TargetInfo other = (TargetInfo) obj; return Objects.equals(this.owner, other.owner) && Objects.equals(this.name, other.name) && Objects.equals(this.desc, other.desc); @@ -195,33 +225,37 @@ public String toString() { static class Builder extends BaseBuilder { @Override - RawAccessRecord build() { - return new RawAccessRecord(caller, target, lineNumber, declaredInLambda); + public RawAccessRecord build() { + return new RawAccessRecord(origin, target, lineNumber, declaredInLambda); } } - abstract static class BaseBuilder> { - CodeUnit caller; + abstract static class BaseBuilder> implements RawCodeUnitDependency.Builder { + CodeUnit origin; TargetInfo target; int lineNumber = -1; boolean declaredInLambda = false; - SELF withCaller(CodeUnit caller) { - this.caller = caller; + @Override + public SELF withOrigin(CodeUnit origin) { + this.origin = origin; return self(); } - SELF withTarget(TargetInfo target) { + @Override + public SELF withTarget(TargetInfo target) { this.target = target; return self(); } - SELF withLineNumber(int lineNumber) { + @Override + public SELF withLineNumber(int lineNumber) { this.lineNumber = lineNumber; return self(); } - SELF withDeclaredInLambda(boolean declaredInLambda) { + @Override + public SELF withDeclaredInLambda(boolean declaredInLambda) { this.declaredInLambda = declaredInLambda; return self(); } @@ -230,15 +264,13 @@ SELF withDeclaredInLambda(boolean declaredInLambda) { SELF self() { return (SELF) this; } - - abstract ACCESS build(); } static class ForField extends RawAccessRecord { final AccessType accessType; - private ForField(CodeUnit caller, TargetInfo target, int lineNumber, AccessType accessType, boolean declaredInLambda) { - super(caller, target, lineNumber, declaredInLambda); + private ForField(CodeUnit origin, TargetInfo target, int lineNumber, AccessType accessType, boolean declaredInLambda) { + super(origin, target, lineNumber, declaredInLambda); this.accessType = accessType; } @@ -251,8 +283,8 @@ Builder withAccessType(AccessType accessType) { } @Override - ForField build() { - return new ForField(super.caller, super.target, super.lineNumber, accessType, declaredInLambda); + public ForField build() { + return new ForField(super.origin, super.target, super.lineNumber, accessType, declaredInLambda); } } } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/RawCodeUnitDependency.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/RawCodeUnitDependency.java new file mode 100644 index 0000000000..35324f3d6f --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/RawCodeUnitDependency.java @@ -0,0 +1,38 @@ +/* + * Copyright 2014-2025 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.core.importer; + +interface RawCodeUnitDependency extends HasRawCodeUnitOrigin { + + TARGET getTarget(); + + int getLineNumber(); + + interface Builder, TARGET> extends HasRawCodeUnitOrigin.Builder { + @Override + Builder withOrigin(RawAccessRecord.CodeUnit origin); + + Builder withTarget(TARGET target); + + Builder withLineNumber(int lineNumber); + + @Override + Builder withDeclaredInLambda(boolean declaredInLambda); + + @Override + CODE_UNIT_DEPENDENCY build(); + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/RawInstanceofCheck.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/RawInstanceofCheck.java index 021c9b33ee..ba11a60d19 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/RawInstanceofCheck.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/RawInstanceofCheck.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -20,32 +20,82 @@ import static com.google.common.base.MoreObjects.toStringHelper; import static com.google.common.base.Preconditions.checkNotNull; -class RawInstanceofCheck { +class RawInstanceofCheck implements RawCodeUnitDependency { + private final RawAccessRecord.CodeUnit origin; private final JavaClassDescriptor target; private final int lineNumber; + private final boolean declaredInLambda; - private RawInstanceofCheck(JavaClassDescriptor target, int lineNumber) { + private RawInstanceofCheck(RawAccessRecord.CodeUnit origin, JavaClassDescriptor target, int lineNumber, boolean declaredInLambda) { + this.origin = checkNotNull(origin); this.target = checkNotNull(target); this.lineNumber = lineNumber; + this.declaredInLambda = declaredInLambda; } - static RawInstanceofCheck from(JavaClassDescriptor target, int lineNumber) { - return new RawInstanceofCheck(target, lineNumber); + @Override + public RawAccessRecord.CodeUnit getOrigin() { + return origin; } - JavaClassDescriptor getTarget() { + @Override + public JavaClassDescriptor getTarget() { return target; } - int getLineNumber() { + @Override + public int getLineNumber() { return lineNumber; } + @Override + public boolean isDeclaredInLambda() { + return declaredInLambda; + } + @Override public String toString() { return toStringHelper(this) + .add("origin", origin) .add("target", target) .add("lineNumber", lineNumber) + .add("declaredInLambda", declaredInLambda) .toString(); } + + static class Builder implements RawCodeUnitDependency.Builder { + private RawAccessRecord.CodeUnit origin; + private JavaClassDescriptor target; + private int lineNumber; + private boolean declaredInLambda; + + @Override + public RawInstanceofCheck.Builder withOrigin(RawAccessRecord.CodeUnit origin) { + this.origin = origin; + return this; + } + + @Override + public RawInstanceofCheck.Builder withTarget(JavaClassDescriptor target) { + this.target = target; + return this; + } + + @Override + public RawInstanceofCheck.Builder withLineNumber(int lineNumber) { + this.lineNumber = lineNumber; + return this; + } + + @Override + public RawInstanceofCheck.Builder withDeclaredInLambda(boolean declaredInLambda) { + this.declaredInLambda = declaredInLambda; + return this; + } + + @Override + public RawInstanceofCheck build() { + return new RawInstanceofCheck(origin, target, lineNumber, declaredInLambda); + } + } } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/RawReferencedClassObject.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/RawReferencedClassObject.java index 7302d18524..05b1f9510a 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/RawReferencedClassObject.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/RawReferencedClassObject.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -16,36 +16,89 @@ package com.tngtech.archunit.core.importer; import com.tngtech.archunit.core.domain.JavaClassDescriptor; +import com.tngtech.archunit.core.importer.RawAccessRecord.CodeUnit; import static com.google.common.base.MoreObjects.toStringHelper; import static com.google.common.base.Preconditions.checkNotNull; -class RawReferencedClassObject { - private final JavaClassDescriptor type; +class RawReferencedClassObject implements RawCodeUnitDependency { + private final CodeUnit origin; + private final JavaClassDescriptor target; private final int lineNumber; + private final boolean declaredInLambda; - private RawReferencedClassObject(JavaClassDescriptor type, int lineNumber) { - this.type = checkNotNull(type); + private RawReferencedClassObject(CodeUnit origin, JavaClassDescriptor target, int lineNumber, boolean declaredInLambda) { + this.origin = checkNotNull(origin); + this.target = checkNotNull(target); this.lineNumber = lineNumber; + this.declaredInLambda = declaredInLambda; } - static RawReferencedClassObject from(JavaClassDescriptor target, int lineNumber) { - return new RawReferencedClassObject(target, lineNumber); + @Override + public CodeUnit getOrigin() { + return origin; + } + + @Override + public JavaClassDescriptor getTarget() { + return target; } String getClassName() { - return type.getFullyQualifiedClassName(); + return target.getFullyQualifiedClassName(); } - int getLineNumber() { + public int getLineNumber() { return lineNumber; } + public boolean isDeclaredInLambda() { + return declaredInLambda; + } + @Override public String toString() { return toStringHelper(this) - .add("type", type) + .add("origin", origin) + .add("target", target) .add("lineNumber", lineNumber) + .add("declaredInLambda", declaredInLambda) .toString(); } + + static class Builder implements RawCodeUnitDependency.Builder { + private CodeUnit origin; + private JavaClassDescriptor target; + private int lineNumber; + private boolean declaredInLambda; + + @Override + public Builder withOrigin(CodeUnit origin) { + this.origin = origin; + return this; + } + + @Override + public Builder withTarget(JavaClassDescriptor target) { + this.target = target; + return this; + } + + @Override + public Builder withLineNumber(int lineNumber) { + this.lineNumber = lineNumber; + return this; + } + + @Override + public Builder withDeclaredInLambda(boolean declaredInLambda) { + this.declaredInLambda = declaredInLambda; + return this; + } + + @Override + public RawReferencedClassObject build() { + return new RawReferencedClassObject(origin, target, lineNumber, declaredInLambda); + } + } } diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/RawTryCatchBlock.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/RawTryCatchBlock.java new file mode 100644 index 0000000000..77eb43d848 --- /dev/null +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/RawTryCatchBlock.java @@ -0,0 +1,130 @@ +/* + * Copyright 2014-2025 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.core.importer; + +import java.util.HashSet; +import java.util.Set; + +import com.google.common.collect.ImmutableSet; +import com.tngtech.archunit.core.domain.JavaClassDescriptor; +import com.tngtech.archunit.core.importer.RawAccessRecord.CodeUnit; + +import static com.google.common.base.Preconditions.checkNotNull; + +class RawTryCatchBlock implements HasRawCodeUnitOrigin { + private final Set caughtThrowables; + private final int lineNumber; + private final Set accessesInTryBlock; + private final CodeUnit declaringCodeUnit; + private final boolean declaredInLambda; + + private RawTryCatchBlock(Builder builder) { + this.caughtThrowables = ImmutableSet.copyOf(builder.caughtThrowables); + this.lineNumber = builder.lineNumber; + this.accessesInTryBlock = ImmutableSet.copyOf(builder.rawAccessesContainedInTryBlock); + this.declaringCodeUnit = checkNotNull(builder.declaringCodeUnit); + this.declaredInLambda = builder.declaredInLambda; + } + + Set getCaughtThrowables() { + return caughtThrowables; + } + + int getLineNumber() { + return lineNumber; + } + + Set getAccessesInTryBlock() { + return accessesInTryBlock; + } + + CodeUnit getDeclaringCodeUnit() { + return declaringCodeUnit; + } + + @Override + public CodeUnit getOrigin() { + return getDeclaringCodeUnit(); + } + + @Override + public boolean isDeclaredInLambda() { + return declaredInLambda; + } + + static class Builder implements HasRawCodeUnitOrigin.Builder { + private Set caughtThrowables = new HashSet<>(); + private int lineNumber; + private Set rawAccessesContainedInTryBlock = new HashSet<>(); + private CodeUnit declaringCodeUnit; + private boolean declaredInLambda = false; + + Builder withCaughtThrowables(Set caughtThrowables) { + this.caughtThrowables = caughtThrowables; + return this; + } + + Builder addCaughtThrowable(JavaClassDescriptor throwableType) { + caughtThrowables.add(throwableType); + return this; + } + + Builder withLineNumber(int lineNumber) { + this.lineNumber = lineNumber; + return this; + } + + Builder withRawAccessesContainedInTryBlock(Set accessRecords) { + this.rawAccessesContainedInTryBlock = accessRecords; + return this; + } + + Builder addRawAccessContainedInTryBlock(RawAccessRecord accessRecord) { + rawAccessesContainedInTryBlock.add(accessRecord); + return this; + } + + Builder withDeclaringCodeUnit(CodeUnit declaringCodeUnit) { + this.declaringCodeUnit = declaringCodeUnit; + return this; + } + + @Override + public Builder withOrigin(CodeUnit origin) { + return withDeclaringCodeUnit(origin); + } + + @Override + public Builder withDeclaredInLambda(boolean declaredInLambda) { + this.declaredInLambda = declaredInLambda; + return this; + } + + @Override + public RawTryCatchBlock build() { + return new RawTryCatchBlock(this); + } + + static Builder from(RawTryCatchBlock tryCatchBlock) { + return new RawTryCatchBlock.Builder() + .withCaughtThrowables(tryCatchBlock.getCaughtThrowables()) + .withLineNumber(tryCatchBlock.getLineNumber()) + .withRawAccessesContainedInTryBlock(tryCatchBlock.getAccessesInTryBlock()) + .withDeclaringCodeUnit(tryCatchBlock.getOrigin()) + .withDeclaredInLambda(tryCatchBlock.isDeclaredInLambda()); + } + } +} diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/SignatureTypeArgumentProcessor.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/SignatureTypeArgumentProcessor.java index 208b593a1a..92be2a08f1 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/SignatureTypeArgumentProcessor.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/SignatureTypeArgumentProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/SignatureTypeParameterProcessor.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/SignatureTypeParameterProcessor.java index 6a0ebd75ea..766dd072e7 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/SignatureTypeParameterProcessor.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/SignatureTypeParameterProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/SourceDescriptor.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/SourceDescriptor.java index 7f4de823cd..ddab56b43a 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/SourceDescriptor.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/SourceDescriptor.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. diff --git a/archunit/src/main/java/com/tngtech/archunit/core/importer/TryCatchRecorder.java b/archunit/src/main/java/com/tngtech/archunit/core/importer/TryCatchRecorder.java index 3cf58ad470..4f1ff37198 100644 --- a/archunit/src/main/java/com/tngtech/archunit/core/importer/TryCatchRecorder.java +++ b/archunit/src/main/java/com/tngtech/archunit/core/importer/TryCatchRecorder.java @@ -1,5 +1,5 @@ /* - * Copyright 2014-2022 TNG Technology Consulting GmbH + * Copyright 2014-2025 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. @@ -15,28 +15,24 @@ */ package com.tngtech.archunit.core.importer; -import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import com.google.common.collect.HashMultimap; -import com.google.common.collect.Multimap; +import com.google.common.collect.SetMultimap; import com.tngtech.archunit.core.domain.JavaClassDescriptor; -import com.tngtech.archunit.core.importer.DomainBuilders.TryCatchBlockBuilder; import org.objectweb.asm.Label; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import static com.google.common.collect.ImmutableSet.toImmutableSet; - class TryCatchRecorder { private static final Logger log = LoggerFactory.getLogger(TryCatchRecorder.class); private final TryCatchBlocksFinishedListener tryCatchBlocksFinishedListener; - private final Map> blocksByEndByStart = new HashMap<>(); - private final Multimap activeBlocksByEnd = HashMultimap.create(); + private final Map> blocksByEndByStart = new HashMap<>(); + private final SetMultimap activeBlocksByEnd = HashMultimap.create(); private final Set