├── docs ├── CNAME ├── assets │ └── images │ │ ├── taikai-header.png │ │ ├── taikai-logo-dark.png │ │ └── taikai-logo-light.png ├── contributing.md └── index.md ├── .idea └── icon.png ├── .github ├── FUNDING.yml ├── dependabot.yml ├── release.yml └── workflows │ ├── release-docs.yml │ ├── maven.yml │ └── release.yml ├── src ├── main │ └── java │ │ └── com │ │ └── enofex │ │ └── taikai │ │ ├── package-info.java │ │ ├── configures │ │ ├── Customizer.java │ │ ├── DisableableConfigurer.java │ │ ├── Configurer.java │ │ ├── ConfigurerContext.java │ │ ├── Configurers.java │ │ └── AbstractConfigurer.java │ │ ├── TaikaiException.java │ │ ├── test │ │ ├── JUnitDescribedPredicates.java │ │ ├── TestConfigurer.java │ │ └── ContainAssertionsOrVerifications.java │ │ ├── java │ │ ├── PackageNaming.java │ │ ├── MaxMethodParameters.java │ │ ├── NoSystemOutOrErr.java │ │ ├── ConstantNaming.java │ │ ├── ProtectedMembers.java │ │ ├── SerialVersionUID.java │ │ ├── HashCodeAndEquals.java │ │ ├── UtilityClasses.java │ │ ├── Deprecations.java │ │ └── ImportPatterns.java │ │ ├── logging │ │ └── LoggerConventions.java │ │ ├── internal │ │ ├── DescribedPredicates.java │ │ └── Modifiers.java │ │ ├── spring │ │ ├── SpringConfigurer.java │ │ ├── SpringDescribedPredicates.java │ │ ├── ValidatedController.java │ │ ├── ConfigurationsConfigurer.java │ │ └── BootConfigurer.java │ │ └── Namespace.java └── test │ └── java │ └── com │ └── enofex │ └── taikai │ ├── configures │ ├── ConfigurerTest.java │ ├── ConfigurerContextTest.java │ └── ConfigurersTest.java │ ├── java │ ├── PackageNamingTest.java │ ├── NoSystemOutOrErrTest.java │ ├── ProtectedMembersTest.java │ ├── SerialVersionUIDTest.java │ ├── MaxMethodParametersTest.java │ ├── HashCodeAndEqualsTest.java │ ├── DeprecationsTest.java │ ├── NoUsageOfSystemOutOrErrTest.java │ ├── ImportsConfigurerTest.java │ ├── ClassesShouldImplementTest.java │ ├── ConstantNamingTest.java │ ├── MethodsShouldBeAnnotatedWithTest.java │ ├── ClassRecordTest.java │ ├── UtilityClassesTest.java │ ├── MethodsAnnotatedWithShouldNotBeAnnotatedWithTest.java │ ├── NoUsageOfRuleTest.java │ ├── MethodsShouldBeAnnotatedWithAllTest.java │ ├── MethodExceptionRuleTest.java │ └── FieldModifierTest.java │ ├── spring │ ├── BootConfigurerTest.java │ ├── ConfigurationsConfigurerTest.java │ ├── RepositoriesConfigurerTest.java │ ├── ServicesConfigurerTest.java │ ├── PropertiesConfigurerTest.java │ └── ControllersConfigurerTest.java │ ├── NamespaceTest.java │ ├── ArchitectureTest.java │ ├── internal │ ├── DescribedPredicatesTest.java │ └── ModifiersTest.java │ ├── test │ └── JUnitDescribedPredicatesTest.java │ ├── logging │ └── LoggingConfigurerTest.java │ ├── TaikaiRuleTest.java │ └── TaikaiTest.java ├── SECURITY.md ├── .mvn └── wrapper │ └── maven-wrapper.properties ├── .gitignore ├── LICENSE ├── CONTRIBUTING.md ├── mkdocs.yml ├── README.md ├── CODE_OF_CONDUCT.md └── mvnw.cmd /docs/CNAME: -------------------------------------------------------------------------------- 1 | enofex.github.io/taikai 2 | -------------------------------------------------------------------------------- /.idea/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enofex/taikai/HEAD/.idea/icon.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [ mnhock ] 4 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/package-info.java: -------------------------------------------------------------------------------- 1 | @org.jspecify.annotations.NullMarked 2 | package com.enofex.taikai; -------------------------------------------------------------------------------- /docs/assets/images/taikai-header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enofex/taikai/HEAD/docs/assets/images/taikai-header.png -------------------------------------------------------------------------------- /docs/assets/images/taikai-logo-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enofex/taikai/HEAD/docs/assets/images/taikai-logo-dark.png -------------------------------------------------------------------------------- /docs/assets/images/taikai-logo-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/enofex/taikai/HEAD/docs/assets/images/taikai-logo-light.png -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/configures/Customizer.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.configures; 2 | 3 | @FunctionalInterface 4 | public interface Customizer { 5 | 6 | void customize(T t); 7 | 8 | } -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/TaikaiException.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai; 2 | 3 | public class TaikaiException extends RuntimeException { 4 | 5 | public TaikaiException(String message) { 6 | super(message); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/configures/DisableableConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.configures; 2 | 3 | public interface DisableableConfigurer extends Configurer { 4 | 5 | T disable(); 6 | 7 | } 8 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/configures/Configurer.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.configures; 2 | 3 | import com.enofex.taikai.TaikaiRule; 4 | import java.util.Collection; 5 | 6 | public interface Configurer { 7 | 8 | default void clear() { 9 | rules().clear(); 10 | } 11 | 12 | Collection rules(); 13 | } 14 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | This is an open source project that is provided as-is without warrenty or liability. 6 | As such no supportability commitment. The maintainers will do the best they can to address any report promptly and responsibly. 7 | 8 | ## Reporting a Vulnerability 9 | 10 | Please use the "Private vulnerability reporting" feature in the GitHub repository (under the "Security" tab). 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: maven 9 | directory: "/" 10 | schedule: 11 | interval: daily 12 | - package-ecosystem: github-actions 13 | directory: "/" 14 | schedule: 15 | interval: weekly 16 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - ignore-for-release 5 | authors: 6 | - octocat 7 | 8 | categories: 9 | - title: 🎉 New Features 10 | labels: 11 | - enhancement 12 | - feature 13 | 14 | - title: 🐞 Bug Fixes 15 | labels: 16 | - bug 17 | 18 | - title: 📔 Documentation 19 | labels: 20 | - documentation 21 | 22 | - title: 🔨 Dependency Upgrades 23 | labels: 24 | - dependencies 25 | 26 | - title: Other Changes 27 | labels: 28 | - "*" 29 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/configures/ConfigurerContext.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.configures; 2 | 3 | import org.jspecify.annotations.Nullable; 4 | 5 | public final class ConfigurerContext { 6 | 7 | @Nullable private final String namespace; 8 | private final Configurers configurers; 9 | 10 | public ConfigurerContext(@Nullable String namespace, Configurers configurers) { 11 | this.namespace = namespace; 12 | this.configurers = configurers; 13 | } 14 | 15 | public String namespace() { 16 | return this.namespace; 17 | } 18 | 19 | public Configurers configurers() { 20 | return this.configurers; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.github/workflows/release-docs.yml: -------------------------------------------------------------------------------- 1 | name: Release Docs 2 | on: 3 | push: 4 | paths: 5 | - 'docs/**' 6 | - 'mkdocs.yml' 7 | permissions: 8 | contents: write 9 | jobs: 10 | deploy: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v6 14 | - uses: actions/setup-python@v6 15 | with: 16 | python-version: 3.x 17 | - run: echo "cache_id=$(date --utc '+%V')" >> $GITHUB_ENV 18 | - uses: actions/cache@v5 19 | with: 20 | key: mkdocs-material-${{ env.cache_id }} 21 | path: .cache 22 | restore-keys: | 23 | mkdocs-material- 24 | - run: pip install mkdocs-material 25 | - run: pip install mkdocs-minify-plugin 26 | - run: mkdocs gh-deploy --force 27 | -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | # Licensed to the Apache Software Foundation (ASF) under one 2 | # or more contributor license agreements. See the NOTICE file 3 | # distributed with this work for additional information 4 | # regarding copyright ownership. The ASF licenses this file 5 | # to you under the Apache License, Version 2.0 (the 6 | # "License"); you may not use this file except in compliance 7 | # with the License. You may obtain a copy of the License at 8 | # 9 | # https://www.apache.org/licenses/LICENSE-2.0 10 | # 11 | # Unless required by applicable law or agreed to in writing, 12 | # software distributed under the License is distributed on an 13 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 14 | # KIND, either express or implied. See the License for the 15 | # specific language governing permissions and limitations 16 | # under the License. 17 | wrapperVersion=3.3.2 18 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.9/apache-maven-3.9.9-bin.zip 19 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/test/JUnitDescribedPredicates.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.test; 2 | 3 | import static com.enofex.taikai.internal.DescribedPredicates.annotatedWith; 4 | 5 | import com.tngtech.archunit.base.DescribedPredicate; 6 | import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; 7 | 8 | final class JUnitDescribedPredicates { 9 | 10 | static final String ANNOTATION_TEST = "org.junit.jupiter.api.Test"; 11 | static final String ANNOTATION_PARAMETRIZED_TEST = "org.junit.jupiter.params.ParameterizedTest"; 12 | static final String ANNOTATION_DISABLED = "org.junit.jupiter.api.Disabled"; 13 | static final String ANNOTATION_DISPLAY_NAME = "org.junit.jupiter.api.DisplayName"; 14 | 15 | private JUnitDescribedPredicates() { 16 | } 17 | 18 | static DescribedPredicate annotatedWithTestOrParameterizedTest( 19 | boolean isMetaAnnotated) { 20 | 21 | return annotatedWith(ANNOTATION_TEST, isMetaAnnotated) 22 | .or(annotatedWith(ANNOTATION_PARAMETRIZED_TEST, isMetaAnnotated)); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | HELP.md 2 | .gradle 3 | build/ 4 | !gradle/wrapper/gradle-wrapper.jar 5 | !**/src/main/**/build/ 6 | !**/src/test/**/build/ 7 | target/ 8 | *.gz 9 | *.log 10 | development/data/ 11 | pom.xml.versionsBackup 12 | 13 | ### STS ### 14 | .apt_generated 15 | .classpath 16 | .factorypath 17 | .project 18 | .settings 19 | .springBeans 20 | .sts4-cache 21 | bin/ 22 | !**/src/main/**/bin/ 23 | !**/src/test/**/bin/ 24 | 25 | ### IntelliJ IDEA ### 26 | .idea/* 27 | !.idea/icon.png 28 | *.iws 29 | *.iml 30 | *.ipr 31 | out/ 32 | !**/src/main/**/out/ 33 | !**/src/test/**/out/ 34 | 35 | ### NetBeans ### 36 | /nbproject/private/ 37 | /nbbuild/ 38 | /dist/ 39 | /nbdist/ 40 | /.nb-gradle/ 41 | 42 | ### VS Code ### 43 | .vscode/ 44 | 45 | # Compiled output 46 | dist 47 | tmp 48 | out-tsc 49 | bazel-out 50 | *.tmp 51 | 52 | # Node 53 | node_modules 54 | npm-debug.log 55 | yarn-error.log 56 | 57 | # Miscellaneous 58 | .angular/ 59 | .sass-cache/ 60 | connect.lock 61 | coverage 62 | libpeerconnection.log 63 | testem.log 64 | typings 65 | 66 | # System files 67 | .DS_Store 68 | Thumbs.db 69 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/configures/ConfigurerTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.configures; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertTrue; 4 | import static org.mockito.Mockito.mock; 5 | 6 | import com.enofex.taikai.TaikaiRule; 7 | import com.tngtech.archunit.lang.ArchRule; 8 | import java.util.ArrayList; 9 | import java.util.Collection; 10 | import org.junit.jupiter.api.Test; 11 | 12 | class ConfigurerTest { 13 | 14 | @Test 15 | void shouldClearRules() { 16 | ArchRule archRule = mock(ArchRule.class); 17 | TaikaiRule taikaiRule = TaikaiRule.of(archRule); 18 | 19 | TestConfigurer configurer = new TestConfigurer(); 20 | configurer.rules().add(taikaiRule); 21 | 22 | configurer.clear(); 23 | 24 | assertTrue(configurer.rules().isEmpty()); 25 | } 26 | 27 | 28 | private static final class TestConfigurer implements Configurer { 29 | 30 | private final Collection rules = new ArrayList<>(); 31 | 32 | @Override 33 | public Collection rules() { 34 | return this.rules; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/java/PackageNamingTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.enofex.taikai.Taikai; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class PackageNamingTest { 10 | 11 | @Test 12 | void shouldApplyPackageNamingConvention() { 13 | Taikai taikai = Taikai.builder() 14 | .classes(ValidPackageClass.class) 15 | .java(java -> java.naming(naming -> naming.packagesShouldMatch("com\\.enofex\\..*"))) 16 | .build(); 17 | 18 | assertDoesNotThrow(taikai::check); 19 | } 20 | 21 | @Test 22 | void shouldThrowWhenPackageDoesNotMatchConvention() { 23 | Taikai taikai = Taikai.builder() 24 | .classes(ValidPackageClass.class) 25 | .java(java -> java.naming(naming -> naming.packagesShouldMatch("org\\.enofex\\..*"))) 26 | .build(); 27 | 28 | assertThrows(AssertionError.class, taikai::check); 29 | } 30 | 31 | 32 | static class ValidPackageClass { 33 | 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/java/PackageNaming.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import com.tngtech.archunit.core.domain.JavaClass; 4 | import com.tngtech.archunit.lang.ArchCondition; 5 | import com.tngtech.archunit.lang.ConditionEvents; 6 | import com.tngtech.archunit.lang.SimpleConditionEvent; 7 | import java.util.regex.Pattern; 8 | 9 | final class PackageNaming { 10 | 11 | private PackageNaming() { 12 | } 13 | 14 | static ArchCondition resideInPackageWithProperNamingConvention(String regex) { 15 | return new ArchCondition<>("reside in package with proper naming convention") { 16 | private final Pattern pattern = Pattern.compile(regex); 17 | 18 | @Override 19 | public void check(JavaClass javaClass, ConditionEvents events) { 20 | String packageName = javaClass.getPackageName(); 21 | if (!this.pattern.matcher(packageName).matches()) { 22 | events.add(SimpleConditionEvent.violated(javaClass, 23 | "Package '%s' does not follow the naming convention".formatted( 24 | packageName))); 25 | } 26 | } 27 | }; 28 | } 29 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Enofex / Martin Hock 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/java/MaxMethodParameters.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import com.tngtech.archunit.core.domain.JavaMethod; 4 | import com.tngtech.archunit.lang.ArchCondition; 5 | import com.tngtech.archunit.lang.ConditionEvents; 6 | import com.tngtech.archunit.lang.SimpleConditionEvent; 7 | 8 | final class MaxMethodParameters { 9 | 10 | private MaxMethodParameters() { 11 | } 12 | 13 | static ArchCondition notExceedMaxParameters(int maxMethodParameters) { 14 | return new ArchCondition<>("not have more than %d parameters".formatted(maxMethodParameters)) { 15 | @Override 16 | public void check(JavaMethod method, ConditionEvents events) { 17 | int parameterCount = method.getRawParameterTypes().size(); 18 | 19 | if (parameterCount > maxMethodParameters) { 20 | String message = "Method %s has %d parameters, max allowed: %d".formatted( 21 | method.getFullName(), parameterCount, maxMethodParameters); 22 | events.add(SimpleConditionEvent.violated(method, message)); 23 | } 24 | } 25 | }; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/test/TestConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.test; 2 | 3 | import com.enofex.taikai.configures.AbstractConfigurer; 4 | import com.enofex.taikai.configures.ConfigurerContext; 5 | import com.enofex.taikai.configures.Customizer; 6 | import com.enofex.taikai.configures.DisableableConfigurer; 7 | 8 | public final class TestConfigurer extends AbstractConfigurer implements DisableableConfigurer { 9 | 10 | public TestConfigurer(ConfigurerContext configurerContext) { 11 | super(configurerContext); 12 | } 13 | 14 | /** 15 | * @deprecated Since only JUnit and above are supported, use {@link #junit(Customizer)} instead. 16 | * This method was retained for backward compatibility and delegates directly to 17 | * {@link #junit(Customizer)}. 18 | */ 19 | @Deprecated(forRemoval = true) 20 | public TestConfigurer junit5(Customizer customizer) { 21 | return junit(customizer); 22 | } 23 | 24 | public TestConfigurer junit(Customizer customizer) { 25 | return customizer(customizer, () -> new JUnitConfigurer(configurerContext())); 26 | } 27 | 28 | @Override 29 | public TestConfigurer disable() { 30 | disable(TestConfigurer.class); 31 | disable(JUnitConfigurer.class); 32 | 33 | return this; 34 | } 35 | } -------------------------------------------------------------------------------- /.github/workflows/maven.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven, and cache/restore any dependencies to improve the workflow execution time 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-java-with-maven 3 | 4 | # This workflow uses actions that are not certified by GitHub. 5 | # They are provided by a third-party and are governed by 6 | # separate terms of service, privacy policy, and support 7 | # documentation. 8 | 9 | name: Java CI with Maven 10 | 11 | on: 12 | push: 13 | branches: [ "main" ] 14 | pull_request: 15 | branches: [ "main" ] 16 | 17 | jobs: 18 | build: 19 | permissions: write-all 20 | runs-on: ubuntu-latest 21 | 22 | steps: 23 | - uses: actions/checkout@v6 24 | - name: Set up JDK 17 25 | uses: actions/setup-java@v5 26 | with: 27 | java-version: '17' 28 | distribution: 'temurin' 29 | cache: maven 30 | - name: Build with Maven 31 | run: ./mvnw -B package --file pom.xml 32 | 33 | # Optional: Uploads the full dependency graph to GitHub to improve the quality of Dependabot alerts this repository can receive 34 | - name: Update dependency graph 35 | uses: advanced-security/maven-dependency-submission-action@b275d12641ac2d2108b2cbb7598b154ad2f2cee8 36 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/java/NoSystemOutOrErr.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import com.tngtech.archunit.core.domain.JavaClass; 4 | import com.tngtech.archunit.lang.ArchCondition; 5 | import com.tngtech.archunit.lang.ConditionEvents; 6 | import com.tngtech.archunit.lang.SimpleConditionEvent; 7 | 8 | final class NoSystemOutOrErr { 9 | 10 | private NoSystemOutOrErr() { 11 | } 12 | 13 | static ArchCondition notUseSystemOutOrErr() { 14 | return new ArchCondition<>("not call System.out or System.err") { 15 | @Override 16 | public void check(JavaClass javaClass, ConditionEvents events) { 17 | javaClass.getFieldAccessesFromSelf().stream() 18 | .filter(fieldAccess -> fieldAccess.getTargetOwner().isEquivalentTo(System.class)) 19 | .forEach(fieldAccess -> { 20 | String fieldName = fieldAccess.getTarget().getName(); 21 | 22 | if ("out".equals(fieldName) || "err".equals(fieldName)) { 23 | events.add(SimpleConditionEvent.violated(fieldAccess, 24 | "Method %s calls %s.%s".formatted( 25 | fieldAccess.getOrigin().getFullName(), 26 | fieldAccess.getTargetOwner().getName(), 27 | fieldAccess.getTarget().getName()))); 28 | } 29 | }); 30 | } 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/java/NoSystemOutOrErrTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.enofex.taikai.Taikai; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class NoSystemOutOrErrTest { 10 | 11 | @Test 12 | void shouldApplyNoSystemOutOrErrRule() { 13 | Taikai taikai = Taikai.builder() 14 | .classes(ValidSystemUsage.class) 15 | .java(JavaConfigurer::noUsageOfSystemOutOrErr) 16 | .build(); 17 | 18 | assertDoesNotThrow(taikai::check); 19 | } 20 | 21 | @Test 22 | void shouldThrowWhenSystemOutOrErrIsUsed() { 23 | Taikai taikai = Taikai.builder() 24 | .classes(InvalidSystemUsage.class) 25 | .java(JavaConfigurer::noUsageOfSystemOutOrErr) 26 | .build(); 27 | 28 | assertThrows(AssertionError.class, taikai::check); 29 | } 30 | 31 | static class ValidSystemUsage { 32 | 33 | void logSomething() { 34 | String msg = "no system out or err here"; 35 | msg.toUpperCase(); 36 | } 37 | } 38 | 39 | static class InvalidSystemUsage { 40 | 41 | void logToConsole() { 42 | System.out.println("This should be forbidden"); 43 | } 44 | 45 | void logError() { 46 | System.err.println("This should also be forbidden"); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/java/ConstantNaming.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static com.enofex.taikai.internal.Modifiers.isFieldSynthetic; 4 | 5 | import com.tngtech.archunit.core.domain.JavaField; 6 | import com.tngtech.archunit.lang.ArchCondition; 7 | import com.tngtech.archunit.lang.ConditionEvents; 8 | import com.tngtech.archunit.lang.SimpleConditionEvent; 9 | import java.util.Collection; 10 | import java.util.regex.Pattern; 11 | 12 | final class ConstantNaming { 13 | 14 | private static final Pattern CONSTANT_NAME_PATTERN = Pattern.compile("^[A-Z][A-Z0-9_]*$"); 15 | 16 | private ConstantNaming() { 17 | } 18 | 19 | static ArchCondition shouldFollowConstantNamingConventions( 20 | Collection excludedFields) { 21 | return new ArchCondition<>("follow constant naming convention") { 22 | @Override 23 | public void check(JavaField field, ConditionEvents events) { 24 | if (!isFieldSynthetic(field) 25 | && !excludedFields.contains(field.getName()) 26 | && !CONSTANT_NAME_PATTERN.matcher(field.getName()).matches()) { 27 | events.add(SimpleConditionEvent.violated(field, 28 | "Constant %s in class %s does not follow the naming convention".formatted( 29 | field.getName(), 30 | field.getOwner().getName()))); 31 | } 32 | } 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/configures/Configurers.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.configures; 2 | 3 | import static java.util.Objects.requireNonNull; 4 | 5 | import java.util.Collection; 6 | import java.util.Iterator; 7 | import java.util.LinkedHashMap; 8 | import java.util.Map; 9 | import org.jspecify.annotations.NonNull; 10 | 11 | public final class Configurers implements Iterable { 12 | 13 | private final Map, Configurer> configurers; 14 | 15 | public Configurers() { 16 | this.configurers = new LinkedHashMap<>(); 17 | } 18 | 19 | public C getOrApply(C configurer) { 20 | requireNonNull(configurer); 21 | 22 | C existingConfigurer = (C) this.get(configurer.getClass()); 23 | return existingConfigurer != null ? existingConfigurer : this.apply(configurer); 24 | } 25 | 26 | private C apply(C configurer) { 27 | this.add(configurer); 28 | return configurer; 29 | } 30 | 31 | private void add(C configurer) { 32 | Class clazz = configurer.getClass(); 33 | this.configurers.putIfAbsent(clazz, configurer); 34 | } 35 | 36 | public C get(Class clazz) { 37 | return (C) this.configurers.get(clazz); 38 | } 39 | 40 | public Collection all() { 41 | return this.configurers.values(); 42 | } 43 | 44 | @Override 45 | public @NonNull Iterator iterator() { 46 | return this.configurers.values().iterator(); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/spring/BootConfigurerTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.spring; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.enofex.taikai.Taikai; 7 | import org.junit.jupiter.api.Nested; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.boot.autoconfigure.SpringBootApplication; 10 | 11 | class BootConfigurerTest { 12 | 13 | @Nested 14 | class ApplicationClassShouldResideInPackage { 15 | 16 | @Test 17 | void shouldNotThrowWhenApplicationClassInCorrectPackage() { 18 | Taikai taikai = Taikai.builder() 19 | .classes(CorrectPackageApplication.class) 20 | .spring(spring -> spring.boot( 21 | boot -> boot.applicationClassShouldResideInPackage("com.enofex.taikai.spring"))) 22 | .build(); 23 | 24 | assertDoesNotThrow(taikai::check); 25 | } 26 | 27 | @Test 28 | void shouldThrowWhenApplicationClassOutsideConfiguredPackage() { 29 | Taikai taikai = Taikai.builder() 30 | .classes(WrongPackageApplication.class) 31 | .spring(spring -> spring.boot( 32 | boot -> boot.applicationClassShouldResideInPackage("com.example.other"))) 33 | .build(); 34 | 35 | assertThrows(AssertionError.class, taikai::check); 36 | } 37 | } 38 | 39 | @SpringBootApplication 40 | static class CorrectPackageApplication { 41 | 42 | } 43 | 44 | @SpringBootApplication 45 | static class WrongPackageApplication { 46 | 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/configures/ConfigurerContextTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.configures; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertNull; 5 | import static org.junit.jupiter.api.Assertions.assertSame; 6 | 7 | import org.junit.jupiter.api.Test; 8 | 9 | class ConfigurerContextTest { 10 | 11 | private static final String VALID_NAMESPACE = "com.example"; 12 | private static final Configurers VALID_CONFIGURERS = new Configurers(); 13 | 14 | @Test 15 | void shouldReturnNamespace() { 16 | ConfigurerContext context = new ConfigurerContext(VALID_NAMESPACE, VALID_CONFIGURERS); 17 | 18 | assertEquals(VALID_NAMESPACE, context.namespace()); 19 | } 20 | 21 | @Test 22 | void shouldReturnConfigurers() { 23 | ConfigurerContext context = new ConfigurerContext(VALID_NAMESPACE, VALID_CONFIGURERS); 24 | 25 | assertSame(VALID_CONFIGURERS, context.configurers()); 26 | } 27 | 28 | @Test 29 | void shouldHandleNullNamespace() { 30 | ConfigurerContext context = new ConfigurerContext(null, VALID_CONFIGURERS); 31 | 32 | assertNull(context.namespace()); 33 | } 34 | 35 | @Test 36 | void shouldHandleNullConfigurers() { 37 | ConfigurerContext context = new ConfigurerContext(VALID_NAMESPACE, null); 38 | 39 | assertNull(context.configurers()); 40 | } 41 | 42 | @Test 43 | void shouldHandleNullParameters() { 44 | ConfigurerContext context = new ConfigurerContext(null, null); 45 | 46 | assertNull(context.namespace()); 47 | assertNull(context.configurers()); 48 | } 49 | } -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/java/ProtectedMembers.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static com.enofex.taikai.internal.Modifiers.isFieldProtected; 4 | import static com.enofex.taikai.internal.Modifiers.isMethodProtected; 5 | 6 | import com.tngtech.archunit.core.domain.JavaClass; 7 | import com.tngtech.archunit.core.domain.JavaField; 8 | import com.tngtech.archunit.core.domain.JavaMethod; 9 | import com.tngtech.archunit.lang.ArchCondition; 10 | import com.tngtech.archunit.lang.ConditionEvents; 11 | import com.tngtech.archunit.lang.SimpleConditionEvent; 12 | 13 | final class ProtectedMembers { 14 | 15 | private ProtectedMembers() { 16 | } 17 | 18 | static ArchCondition notHaveProtectedMembers() { 19 | return new ArchCondition<>("not have protected members") { 20 | @Override 21 | public void check(JavaClass javaClass, ConditionEvents events) { 22 | for (JavaField field : javaClass.getFields()) { 23 | if (isFieldProtected(field)) { 24 | events.add(SimpleConditionEvent.violated(field, 25 | "Field %s in final class %s is protected".formatted( 26 | field.getName(), 27 | javaClass.getName()))); 28 | } 29 | } 30 | 31 | for (JavaMethod method : javaClass.getMethods()) { 32 | if (isMethodProtected(method)) { 33 | events.add(SimpleConditionEvent.violated(method, 34 | "Method %s in final class %s is protected".formatted( 35 | method.getName(), 36 | javaClass.getName()))); 37 | } 38 | } 39 | } 40 | }; 41 | } 42 | } -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/java/SerialVersionUID.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static com.enofex.taikai.internal.Modifiers.isFieldFinal; 4 | import static com.enofex.taikai.internal.Modifiers.isFieldStatic; 5 | 6 | import com.tngtech.archunit.base.DescribedPredicate; 7 | import com.tngtech.archunit.core.domain.JavaField; 8 | import com.tngtech.archunit.lang.ArchCondition; 9 | import com.tngtech.archunit.lang.ConditionEvents; 10 | import com.tngtech.archunit.lang.SimpleConditionEvent; 11 | 12 | final class SerialVersionUID { 13 | 14 | private SerialVersionUID() { 15 | } 16 | 17 | static ArchCondition beStaticFinalLong() { 18 | return new ArchCondition<>("be static final long") { 19 | @Override 20 | public void check(JavaField javaField, ConditionEvents events) { 21 | if (!isFieldStatic(javaField) || !isFieldFinal(javaField) || !isLong(javaField)) { 22 | events.add(SimpleConditionEvent.violated(javaField, 23 | "Field %s in class %s is not static final long".formatted( 24 | javaField.getName(), 25 | javaField.getOwner().getName()))); 26 | } 27 | } 28 | 29 | private static boolean isLong(JavaField javaField) { 30 | return javaField.getRawType().isEquivalentTo(long.class); 31 | } 32 | }; 33 | } 34 | 35 | static DescribedPredicate namedSerialVersionUID() { 36 | return new DescribedPredicate<>("named serialVersionUID") { 37 | @Override 38 | public boolean test(JavaField javaField) { 39 | return "serialVersionUID".equals(javaField.getName()); 40 | } 41 | }; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/configures/AbstractConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.configures; 2 | 3 | import static java.util.Objects.requireNonNull; 4 | 5 | import com.enofex.taikai.TaikaiRule; 6 | import java.util.ArrayList; 7 | import java.util.Collection; 8 | import java.util.function.Supplier; 9 | 10 | public abstract class AbstractConfigurer implements Configurer { 11 | 12 | private final ConfigurerContext configurerContext; 13 | private final Collection rules; 14 | 15 | protected AbstractConfigurer(ConfigurerContext configurerContext) { 16 | this.configurerContext = requireNonNull(configurerContext); 17 | this.rules = new ArrayList<>(); 18 | } 19 | 20 | protected ConfigurerContext configurerContext() { 21 | return this.configurerContext; 22 | } 23 | 24 | protected T addRule(TaikaiRule rule) { 25 | requireNonNull(rule); 26 | 27 | this.rules.add(rule); 28 | return (T) this; 29 | } 30 | 31 | protected void disable(Class clazz) { 32 | requireNonNull(clazz); 33 | 34 | Configurer configurer = this.configurerContext.configurers().get(clazz); 35 | 36 | if (configurer != null) { 37 | configurer.clear(); 38 | } 39 | } 40 | 41 | protected C customizer(Customizer customizer, 42 | Supplier supplier) { 43 | requireNonNull(customizer); 44 | requireNonNull(supplier); 45 | 46 | customizer.customize(this.configurerContext 47 | .configurers() 48 | .getOrApply(supplier.get())); 49 | 50 | return (C) this; 51 | } 52 | 53 | @Override 54 | public Collection rules() { 55 | return this.rules; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/java/ProtectedMembersTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.enofex.taikai.Taikai; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class ProtectedMembersTest { 10 | 11 | @Test 12 | void shouldPassWhenFinalClassHasNoProtectedMembers() { 13 | Taikai taikai = Taikai.builder() 14 | .classes(ValidFinalClass.class) 15 | .java(JavaConfigurer::finalClassesShouldNotHaveProtectedMembers) 16 | .build(); 17 | 18 | assertDoesNotThrow(taikai::check); 19 | } 20 | 21 | @Test 22 | void shouldFailWhenFinalClassHasProtectedField() { 23 | Taikai taikai = Taikai.builder() 24 | .classes(FinalClassWithProtectedField.class) 25 | .java(JavaConfigurer::finalClassesShouldNotHaveProtectedMembers) 26 | .build(); 27 | 28 | assertThrows(AssertionError.class, taikai::check); 29 | } 30 | 31 | @Test 32 | void shouldFailWhenFinalClassHasProtectedMethod() { 33 | Taikai taikai = Taikai.builder() 34 | .classes(FinalClassWithProtectedMethod.class) 35 | .java(JavaConfigurer::finalClassesShouldNotHaveProtectedMembers) 36 | .build(); 37 | 38 | assertThrows(AssertionError.class, taikai::check); 39 | } 40 | 41 | 42 | static final class ValidFinalClass { 43 | 44 | private int privateField; 45 | 46 | public void publicMethod() { 47 | } 48 | } 49 | 50 | static final class FinalClassWithProtectedField { 51 | 52 | protected String protectedField; 53 | } 54 | 55 | static final class FinalClassWithProtectedMethod { 56 | 57 | protected void protectedMethod() { 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/java/SerialVersionUIDTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.enofex.taikai.Taikai; 7 | import java.io.Serializable; 8 | import org.junit.jupiter.api.Test; 9 | 10 | class SerialVersionUIDTest { 11 | 12 | @Test 13 | void shouldApplySerialVersionUIDNamingRule() { 14 | Taikai taikai = Taikai.builder() 15 | .classes(ValidSerialVersionUID.class) 16 | .java(JavaConfigurer::serialVersionUIDFieldsShouldBeStaticFinalLong) 17 | .build(); 18 | 19 | assertDoesNotThrow(taikai::check); 20 | } 21 | 22 | @Test 23 | void shouldThrowWhenSerialVersionUIDIsNotStaticFinalLong() { 24 | Taikai taikai = Taikai.builder() 25 | .classes(InvalidSerialVersionUID.class) 26 | .java(JavaConfigurer::serialVersionUIDFieldsShouldBeStaticFinalLong) 27 | .build(); 28 | 29 | assertThrows(AssertionError.class, taikai::check); 30 | } 31 | 32 | @Test 33 | void shouldIgnoreFieldsNotNamedSerialVersionUID() { 34 | Taikai taikai = Taikai.builder() 35 | .classes(ClassWithOtherConstant.class) 36 | .java(JavaConfigurer::serialVersionUIDFieldsShouldBeStaticFinalLong) 37 | .build(); 38 | 39 | assertDoesNotThrow(taikai::check); 40 | } 41 | 42 | 43 | static class ValidSerialVersionUID implements Serializable { 44 | 45 | private static final long serialVersionUID = 1L; 46 | } 47 | 48 | static class InvalidSerialVersionUID implements Serializable { 49 | 50 | private static long serialVersionUID = 2L; 51 | } 52 | 53 | static class ClassWithOtherConstant { 54 | 55 | private static final long SOME_CONSTANT = 42L; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/logging/LoggerConventions.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.logging; 2 | 3 | import com.tngtech.archunit.core.domain.JavaClass; 4 | import com.tngtech.archunit.core.domain.JavaField; 5 | import com.tngtech.archunit.core.domain.JavaModifier; 6 | import com.tngtech.archunit.lang.ArchCondition; 7 | import com.tngtech.archunit.lang.ConditionEvents; 8 | import com.tngtech.archunit.lang.SimpleConditionEvent; 9 | import java.util.Collection; 10 | 11 | final class LoggerConventions { 12 | 13 | private LoggerConventions() { 14 | } 15 | 16 | static ArchCondition followLoggerConventions(String typeName, String regex, 17 | Collection requiredModifiers) { 18 | return new ArchCondition<>( 19 | "have a logger field of type %s with name pattern %s and modifiers %s".formatted( 20 | typeName, regex, requiredModifiers)) { 21 | @Override 22 | public void check(JavaClass javaClass, ConditionEvents events) { 23 | for (JavaField field : javaClass.getFields()) { 24 | if (field.getRawType().isAssignableTo(typeName)) { 25 | if (!field.getName().matches(regex)) { 26 | events.add(SimpleConditionEvent.violated(field, 27 | "Field '%s' in class %s does not match the naming pattern '%s'".formatted( 28 | field.getName(), 29 | javaClass.getName(), regex))); 30 | } 31 | 32 | if (!field.getModifiers().containsAll(requiredModifiers)) { 33 | events.add(SimpleConditionEvent.violated(field, 34 | "Field '%s' in class %s does not have the required modifiers %s".formatted( 35 | field.getName(), 36 | javaClass.getName(), 37 | requiredModifiers))); 38 | } 39 | } 40 | } 41 | } 42 | }; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/java/MaxMethodParametersTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.enofex.taikai.Taikai; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class MaxMethodParametersTest { 10 | 11 | @Test 12 | void shouldNotThrowWhenMethodsDoNotExceedMaxParameters() { 13 | Taikai taikai = Taikai.builder() 14 | .classes(ValidMethods.class) 15 | .java(java -> java.methodsShouldNotExceedMaxParameters(3)) 16 | .build(); 17 | 18 | assertDoesNotThrow(taikai::check); 19 | } 20 | 21 | @Test 22 | void shouldThrowWhenMethodExceedsMaxParameters() { 23 | Taikai taikai = Taikai.builder() 24 | .classes(InvalidMethods.class) 25 | .java(java -> java.methodsShouldNotExceedMaxParameters(2)) 26 | .build(); 27 | 28 | assertThrows(AssertionError.class, taikai::check); 29 | } 30 | 31 | @Test 32 | void shouldAllowExactlyMaxParameters() { 33 | Taikai taikai = Taikai.builder() 34 | .classes(ExactlyMaxMethods.class) 35 | .java(java -> java.methodsShouldNotExceedMaxParameters(3)) 36 | .build(); 37 | 38 | assertDoesNotThrow(taikai::check); 39 | } 40 | 41 | static class ValidMethods { 42 | 43 | void noParams() { 44 | } 45 | 46 | void oneParam(String name) { 47 | } 48 | 49 | void twoParams(String name, int age) { 50 | } 51 | } 52 | 53 | static class InvalidMethods { 54 | 55 | void tooManyParams(String a, String b, String c) { 56 | } 57 | } 58 | 59 | static class ExactlyMaxMethods { 60 | 61 | void exactlyThree(String a, String b, String c) { 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/java/HashCodeAndEquals.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import com.tngtech.archunit.core.domain.JavaClass; 4 | import com.tngtech.archunit.lang.ArchCondition; 5 | import com.tngtech.archunit.lang.ConditionEvents; 6 | import com.tngtech.archunit.lang.SimpleConditionEvent; 7 | 8 | final class HashCodeAndEquals { 9 | 10 | private HashCodeAndEquals() { 11 | } 12 | 13 | static ArchCondition implementHashCodeAndEquals() { 14 | return new ArchCondition<>("implement both equals() and hashCode()") { 15 | @Override 16 | public void check(JavaClass javaClass, ConditionEvents events) { 17 | boolean hasEquals = hasEquals(javaClass); 18 | boolean hasHashCode = hasHashCode(javaClass); 19 | 20 | if (hasEquals && !hasHashCode) { 21 | events.add(SimpleConditionEvent.violated(javaClass, 22 | "Class %s implements equals() but not hashCode()".formatted( 23 | javaClass.getName()))); 24 | } else if (!hasEquals && hasHashCode) { 25 | events.add(SimpleConditionEvent.violated(javaClass, 26 | "Class %s implements hashCode() but not equals()".formatted( 27 | javaClass.getName()))); 28 | } 29 | } 30 | 31 | private static boolean hasHashCode(JavaClass javaClass) { 32 | return javaClass.getMethods().stream() 33 | .anyMatch(method -> "hashCode".equals(method.getName()) && 34 | method.getRawParameterTypes().isEmpty()); 35 | } 36 | 37 | private static boolean hasEquals(JavaClass javaClass) { 38 | return javaClass.getMethods().stream() 39 | .anyMatch(method -> "equals".equals(method.getName()) && 40 | method.getRawParameterTypes().size() == 1 && 41 | method.getRawParameterTypes().get(0).getName().equals(Object.class.getName())); 42 | } 43 | }; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/NamespaceTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai; 2 | 3 | import com.tngtech.archunit.core.domain.JavaClasses; 4 | import com.tngtech.archunit.core.importer.ClassFileImporter; 5 | import com.tngtech.archunit.core.importer.ImportOption; 6 | import org.junit.jupiter.api.Test; 7 | 8 | import static org.junit.jupiter.api.Assertions.*; 9 | 10 | class NamespaceTest { 11 | 12 | private static final String VALID_NAMESPACE = "com.enofex.taikai"; 13 | 14 | @Test 15 | void shouldReturnJavaClassesWithoutTests() { 16 | JavaClasses result = Namespace.from(VALID_NAMESPACE, Namespace.IMPORT.WITHOUT_TESTS); 17 | 18 | assertNotNull(result); 19 | assertDoesNotThrow(() -> new ClassFileImporter() 20 | .withImportOption(new ImportOption.DoNotIncludeTests()) 21 | .withImportOption(new ImportOption.DoNotIncludeJars()) 22 | .importPackages(VALID_NAMESPACE)); 23 | } 24 | 25 | @Test 26 | void shouldReturnJavaClassesWithTests() { 27 | JavaClasses result = Namespace.from(VALID_NAMESPACE, Namespace.IMPORT.WITH_TESTS); 28 | 29 | assertNotNull(result); 30 | assertDoesNotThrow(() -> new ClassFileImporter() 31 | .withImportOption(new ImportOption.DoNotIncludeJars()) 32 | .importPackages(VALID_NAMESPACE)); 33 | } 34 | 35 | @Test 36 | void shouldReturnJavaClassesOnlyTests() { 37 | JavaClasses result = Namespace.from(VALID_NAMESPACE, Namespace.IMPORT.ONLY_TESTS); 38 | 39 | assertNotNull(result); 40 | assertDoesNotThrow(() -> new ClassFileImporter() 41 | .withImportOption(new ImportOption.OnlyIncludeTests()) 42 | .withImportOption(new ImportOption.DoNotIncludeJars()) 43 | .importPackages(VALID_NAMESPACE)); 44 | } 45 | 46 | @Test 47 | void shouldThrowExceptionForNullNamespace() { 48 | assertThrows(NullPointerException.class, 49 | () -> Namespace.from(null, Namespace.IMPORT.WITHOUT_TESTS)); 50 | } 51 | 52 | @Test 53 | void shouldThrowExceptionForNullImportOption() { 54 | assertThrows(NullPointerException.class, () -> Namespace.from(VALID_NAMESPACE, null)); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/spring/ConfigurationsConfigurerTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.spring; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.enofex.taikai.Taikai; 7 | import org.junit.jupiter.api.Nested; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.boot.autoconfigure.SpringBootApplication; 10 | import org.springframework.context.annotation.Configuration; 11 | 12 | class ConfigurationsConfigurerTest { 13 | 14 | @Nested 15 | class NamesShouldEndWithConfiguration { 16 | 17 | @Test 18 | void shouldNotThrowWhenConfigurationClassEndsWithConfiguration() { 19 | Taikai taikai = Taikai.builder() 20 | .classes(ValidAppConfiguration.class) 21 | .spring(spring -> spring.configurations( 22 | ConfigurationsConfigurer::namesShouldEndWithConfiguration)) 23 | .build(); 24 | 25 | assertDoesNotThrow(taikai::check); 26 | } 27 | 28 | @Test 29 | void shouldThrowWhenConfigurationClassDoesNotEndWithConfiguration() { 30 | Taikai taikai = Taikai.builder() 31 | .classes(InvalidAppConfig.class) 32 | .spring(spring -> spring.configurations( 33 | ConfigurationsConfigurer::namesShouldEndWithConfiguration)) 34 | .build(); 35 | 36 | assertThrows(AssertionError.class, taikai::check); 37 | } 38 | 39 | @Test 40 | void shouldIgnoreSpringBootApplicationClasses() { 41 | Taikai taikai = Taikai.builder() 42 | .classes(DemoApplication.class) 43 | .spring(spring -> spring.configurations( 44 | ConfigurationsConfigurer::namesShouldEndWithConfiguration)) 45 | .build(); 46 | 47 | assertDoesNotThrow(taikai::check); 48 | } 49 | } 50 | 51 | @Configuration 52 | static class ValidAppConfiguration { 53 | 54 | } 55 | 56 | @Configuration 57 | static class InvalidAppConfig { 58 | 59 | } 60 | 61 | @Configuration 62 | record RecordAppConfiguration() { 63 | 64 | } 65 | 66 | @SpringBootApplication 67 | static class DemoApplication { 68 | 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish package to the Maven Central Repository 2 | permissions: 3 | contents: write 4 | on: 5 | push: 6 | tags: 7 | - v* 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v6 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Set up JDK 17 20 | uses: actions/setup-java@v5 21 | with: 22 | java-version: '17' 23 | distribution: 'temurin' 24 | cache: maven 25 | 26 | - name: Write release version 27 | run: VERSION=${GITHUB_REF_NAME#v}; echo "VERSION=$VERSION" >> $GITHUB_ENV 28 | 29 | - name: Set release version 30 | run: ./mvnw --no-transfer-progress --batch-mode versions:set -DnewVersion=${VERSION} 31 | 32 | - name: Commit & Push changes 33 | uses: actions-js/push@master 34 | with: 35 | github_token: ${{ secrets.JRELEASER_GITHUB_TOKEN }} 36 | message: Perform release ${{ github.event.inputs.version }} 37 | 38 | - name: Publish package 39 | env: 40 | JRELEASER_GPG_PASSPHRASE: ${{ secrets.JRELEASER_GPG_PASSPHRASE }} 41 | JRELEASER_GPG_SECRET_KEY: ${{ secrets.JRELEASER_GPG_SECRET_KEY }} 42 | JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.JRELEASER_GPG_PUBLIC_KEY }} 43 | JRELEASER_GITHUB_TOKEN: ${{ secrets.JRELEASER_GITHUB_TOKEN }} 44 | JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.JRELEASER_MAVENCENTRAL_USERNAME }} 45 | JRELEASER_MAVENCENTRAL_TOKEN: ${{ secrets.JRELEASER_MAVENCENTRAL_TOKEN }} 46 | 47 | run: ./mvnw --no-transfer-progress --batch-mode -Prelease deploy jreleaser:deploy 48 | 49 | - name: Set next version 50 | run: ./mvnw --no-transfer-progress --batch-mode build-helper:parse-version versions:set -DnewVersion=\${parsedVersion.majorVersion}.\${parsedVersion.minorVersion}.\${parsedVersion.nextIncrementalVersion}-SNAPSHOT versions:commit 51 | 52 | - name: Commit & Push changes 53 | uses: actions-js/push@master 54 | with: 55 | github_token: ${{ secrets.JRELEASER_GITHUB_TOKEN }} 56 | message: Prepare for next release 57 | tags: true 58 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/java/HashCodeAndEqualsTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.enofex.taikai.Taikai; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class HashCodeAndEqualsTest { 10 | 11 | @Test 12 | void shouldPassWhenClassImplementsBothEqualsAndHashCode() { 13 | Taikai taikai = Taikai.builder() 14 | .classes(ValidClass.class) 15 | .java(JavaConfigurer::classesShouldImplementHashCodeAndEquals) 16 | .build(); 17 | 18 | assertDoesNotThrow(taikai::check); 19 | } 20 | 21 | @Test 22 | void shouldFailWhenClassImplementsEqualsButNotHashCode() { 23 | Taikai taikai = Taikai.builder() 24 | .classes(EqualsOnlyClass.class) 25 | .java(JavaConfigurer::classesShouldImplementHashCodeAndEquals) 26 | .build(); 27 | 28 | assertThrows(AssertionError.class, taikai::check); 29 | } 30 | 31 | @Test 32 | void shouldFailWhenClassImplementsHashCodeButNotEquals() { 33 | Taikai taikai = Taikai.builder() 34 | .classes(HashCodeOnlyClass.class) 35 | .java(JavaConfigurer::classesShouldImplementHashCodeAndEquals) 36 | .build(); 37 | 38 | assertThrows(AssertionError.class, taikai::check); 39 | } 40 | 41 | @Test 42 | void shouldPassWhenClassImplementsNeitherEqualsNorHashCode() { 43 | Taikai taikai = Taikai.builder() 44 | .classes(PlainClass.class) 45 | .java(JavaConfigurer::classesShouldImplementHashCodeAndEquals) 46 | .build(); 47 | 48 | assertDoesNotThrow(taikai::check); 49 | } 50 | 51 | static class ValidClass { 52 | 53 | @Override 54 | public boolean equals(Object obj) { 55 | return this == obj; 56 | } 57 | 58 | @Override 59 | public int hashCode() { 60 | return 42; 61 | } 62 | } 63 | 64 | static class EqualsOnlyClass { 65 | 66 | @Override 67 | public boolean equals(Object obj) { 68 | return this == obj; 69 | } 70 | } 71 | 72 | static class HashCodeOnlyClass { 73 | 74 | @Override 75 | public int hashCode() { 76 | return 123; 77 | } 78 | } 79 | 80 | static class PlainClass { 81 | 82 | private String field; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/java/DeprecationsTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.enofex.taikai.Taikai; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class DeprecationsTest { 10 | 11 | @Test 12 | void shouldPassWhenNoDeprecatedApisAreUsed() { 13 | Taikai taikai = Taikai.builder() 14 | .classes(SafeClass.class) 15 | .java(JavaConfigurer::noUsageOfDeprecatedAPIs) 16 | .build(); 17 | 18 | assertDoesNotThrow(taikai::check); 19 | } 20 | 21 | @Test 22 | void shouldFailWhenCallingDeprecatedMethod() { 23 | Taikai taikai = Taikai.builder() 24 | .classes(UsesDeprecatedMethod.class) 25 | .java(JavaConfigurer::noUsageOfDeprecatedAPIs) 26 | .build(); 27 | 28 | assertThrows(AssertionError.class, taikai::check); 29 | } 30 | 31 | @Test 32 | void shouldFailWhenInstantiatingDeprecatedConstructor() { 33 | Taikai taikai = Taikai.builder() 34 | .classes(UsesDeprecatedConstructor.class) 35 | .java(JavaConfigurer::noUsageOfDeprecatedAPIs) 36 | .build(); 37 | 38 | assertThrows(AssertionError.class, taikai::check); 39 | } 40 | 41 | @Test 42 | void shouldFailWhenDependingOnDeprecatedClass() { 43 | Taikai taikai = Taikai.builder() 44 | .classes(DependsOnDeprecatedClass.class) 45 | .java(JavaConfigurer::noUsageOfDeprecatedAPIs) 46 | .build(); 47 | 48 | assertThrows(AssertionError.class, taikai::check); 49 | } 50 | 51 | static class SafeClass { 52 | 53 | String value; 54 | 55 | String method() { 56 | return "ok"; 57 | } 58 | } 59 | 60 | static class DeprecatedHolder { 61 | 62 | @Deprecated 63 | public static final String DEPRECATED_FIELD = "bad"; 64 | 65 | @Deprecated 66 | public void deprecatedMethod() { 67 | } 68 | 69 | @Deprecated 70 | public DeprecatedHolder() { 71 | } 72 | } 73 | 74 | @Deprecated 75 | static class DeprecatedClass { 76 | 77 | } 78 | 79 | static class UsesDeprecatedMethod { 80 | 81 | void call() { 82 | new DeprecatedHolder().deprecatedMethod(); 83 | } 84 | } 85 | 86 | static class UsesDeprecatedConstructor { 87 | 88 | void construct() { 89 | new DeprecatedHolder(); 90 | } 91 | } 92 | 93 | static class DependsOnDeprecatedClass { 94 | 95 | DeprecatedClass reference; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/ArchitectureTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai; 2 | 3 | import static com.enofex.taikai.java.ImportPatterns.*; 4 | import static com.tngtech.archunit.core.domain.JavaModifier.FINAL; 5 | import static com.tngtech.archunit.core.domain.JavaModifier.STATIC; 6 | 7 | import com.enofex.taikai.configures.AbstractConfigurer; 8 | import com.enofex.taikai.configures.Configurer; 9 | import java.text.SimpleDateFormat; 10 | import java.util.Calendar; 11 | import java.util.Date; 12 | import java.util.List; 13 | import org.junit.jupiter.api.Test; 14 | 15 | class ArchitectureTest { 16 | 17 | @Test 18 | void shouldFulfilConstrains() { 19 | Taikai.builder() 20 | .namespace("com.enofex.taikai") 21 | .java(java -> java 22 | .noUsageOfDeprecatedAPIs() 23 | .noUsageOfSystemOutOrErr() 24 | .noUsageOf(Date.class) 25 | .noUsageOf(Calendar.class) 26 | .noUsageOf(SimpleDateFormat.class) 27 | .fieldsShouldHaveModifiers("^[A-Z][A-Z0-9_]*$", List.of(STATIC, FINAL)) 28 | .classesShouldImplementHashCodeAndEquals() 29 | .finalClassesShouldNotHaveProtectedMembers() 30 | .utilityClassesShouldBeFinalAndHavePrivateConstructor() 31 | .methodsShouldNotDeclareGenericExceptions() 32 | .fieldsShouldNotBePublic() 33 | .serialVersionUIDFieldsShouldBeStaticFinalLong() 34 | .classesShouldResideInPackage("com.enofex.taikai..") 35 | .imports(imports -> imports 36 | .shouldHaveNoCycles() 37 | .shouldNotImport("org.springframework.core.annotation..") 38 | .shouldNotImport("jakarta.annotation..") 39 | .shouldNotImport("javax.annotation..") 40 | .shouldNotImport("org.jetbrains.annotations..") 41 | .shouldNotImport(shaded()) 42 | .shouldNotImport(lombok()) 43 | .shouldNotImport(junit4())) 44 | .naming(naming -> naming 45 | .packagesShouldMatchDefault() 46 | .fieldsShouldNotMatch(".*(List|Set|Map)$") 47 | .classesShouldNotMatch(".*Impl") 48 | .classesAssignableToShouldMatch(AbstractConfigurer.class, ".*Configurer") 49 | .classesImplementingShouldMatch(Configurer.class, ".*Configurer") 50 | .interfacesShouldNotHavePrefixI() 51 | .constantsShouldFollowConventions())) 52 | .build() 53 | .checkAll(); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/java/NoUsageOfSystemOutOrErrTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.enofex.taikai.Taikai; 7 | import org.junit.jupiter.api.Nested; 8 | import org.junit.jupiter.api.Test; 9 | 10 | class NoUsageOfSystemOutOrErrTest { 11 | 12 | @Nested 13 | class NoUsageOfSystemOutOrErr { 14 | 15 | @Test 16 | void shouldThrowWhenClassUsesSystemOut() { 17 | Taikai taikai = Taikai.builder() 18 | .classes(UsesSystemOut.class) 19 | .java(JavaConfigurer::noUsageOfSystemOutOrErr) 20 | .build(); 21 | 22 | assertThrows(AssertionError.class, taikai::check); 23 | } 24 | 25 | @Test 26 | void shouldThrowWhenClassUsesSystemErr() { 27 | Taikai taikai = Taikai.builder() 28 | .classes(UsesSystemErr.class) 29 | .java(JavaConfigurer::noUsageOfSystemOutOrErr) 30 | .build(); 31 | 32 | assertThrows(AssertionError.class, taikai::check); 33 | } 34 | 35 | @Test 36 | void shouldThrowWhenClassUsesBothSystemOutAndErr() { 37 | Taikai taikai = Taikai.builder() 38 | .classes(UsesSystemOutAndErr.class) 39 | .java(JavaConfigurer::noUsageOfSystemOutOrErr) 40 | .build(); 41 | 42 | assertThrows(AssertionError.class, taikai::check); 43 | } 44 | 45 | @Test 46 | void shouldNotThrowWhenClassDoesNotUseSystemOutOrErr() { 47 | Taikai taikai = Taikai.builder() 48 | .classes(DoesNotUseSystemStreams.class) 49 | .java(JavaConfigurer::noUsageOfSystemOutOrErr) 50 | .build(); 51 | 52 | assertDoesNotThrow(taikai::check); 53 | } 54 | } 55 | 56 | static class UsesSystemOut { 57 | void printSomething() { 58 | System.out.println("Hello, World!"); 59 | } 60 | } 61 | 62 | static class UsesSystemErr { 63 | void logError() { 64 | System.err.println("An error occurred!"); 65 | } 66 | } 67 | 68 | static class UsesSystemOutAndErr { 69 | void doSomething() { 70 | System.out.println("Working..."); 71 | System.err.println("Something went wrong!"); 72 | } 73 | } 74 | 75 | static class DoesNotUseSystemStreams { 76 | void logSafely() { 77 | // uses proper logging API instead of System.out/err 78 | String message = "safe logging"; 79 | if (message.isEmpty()) { 80 | throw new IllegalStateException("Unexpected state"); 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/java/UtilityClasses.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static com.enofex.taikai.internal.Modifiers.isMethodStatic; 4 | import static com.tngtech.archunit.base.DescribedPredicate.doNot; 5 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; 6 | 7 | import com.enofex.taikai.internal.Modifiers; 8 | import com.tngtech.archunit.base.DescribedPredicate; 9 | import com.tngtech.archunit.core.domain.JavaClass; 10 | import com.tngtech.archunit.lang.ArchCondition; 11 | import com.tngtech.archunit.lang.ConditionEvents; 12 | import com.tngtech.archunit.lang.SimpleConditionEvent; 13 | import com.tngtech.archunit.lang.syntax.elements.GivenClassesConjunction; 14 | 15 | final class UtilityClasses { 16 | 17 | private UtilityClasses() { 18 | } 19 | 20 | static GivenClassesConjunction utilityClasses() { 21 | return classes().that(haveOnlyStaticMethods()).and(doNot(extendAnyClass())); 22 | } 23 | 24 | private static DescribedPredicate haveOnlyStaticMethods() { 25 | return new DescribedPredicate<>("have only static methods") { 26 | @Override 27 | public boolean test(JavaClass javaClass) { 28 | return !javaClass.getMethods().isEmpty() && javaClass.getMethods().stream() 29 | .allMatch(method -> isMethodStatic(method) && !"main".equals(method.getName())); 30 | } 31 | }; 32 | } 33 | 34 | private static DescribedPredicate extendAnyClass() { 35 | return new DescribedPredicate<>("extend any class") { 36 | @Override 37 | public boolean test(JavaClass javaClass) { 38 | return javaClass.getSuperclass() 39 | // Ignore implicit Object super class. 40 | .filter(superClass -> !Object.class.getName().equals(superClass.getName())) 41 | .isPresent(); 42 | } 43 | }; 44 | } 45 | 46 | static ArchCondition havePrivateConstructor() { 47 | return new ArchCondition<>("have a private constructor") { 48 | @Override 49 | public void check(JavaClass javaClass, ConditionEvents events) { 50 | if (hasNoPrivateConstructor(javaClass)) { 51 | events.add(SimpleConditionEvent.violated(javaClass, 52 | "Class %s does not have a private constructor".formatted( 53 | javaClass.getName()))); 54 | } 55 | } 56 | 57 | private static boolean hasNoPrivateConstructor(JavaClass javaClass) { 58 | return javaClass.getConstructors().stream().noneMatch(Modifiers::isConstructorPrivate); 59 | } 60 | }; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Thank you for your interest in contributing to *Taikai*! We welcome contributions from the community 4 | to help make the project even better. Here are some ways you can get involved: 5 | 6 | * **Star the Project**: 7 | 8 | - Show your support by starring the project 9 | on [GitHub](https://github.com/enofex/taikai). 10 | - This helps increase visibility and encourages others to discover and contribute to the 11 | project. 12 | 13 | * **Review the Contribution Guide**: 14 | 15 | - Familiarize yourself with the guidelines and procedures outlined in our contribution guide. 16 | - The contribution guide provides detailed information on how to get started and the different 17 | ways 18 | you can contribute. 19 | 20 | * **Follow Contribution Guidelines**: 21 | 22 | - Ensure that you follow our 23 | contribution [guidelines](https://github.com/enofex/taikai/blob/main/CONTRIBUTING.md) 24 | when submitting your contributions. 25 | - These guidelines cover aspects such as code formatting, documentation standards, and other 26 | important considerations. 27 | 28 | * **Pull Requests**: 29 | 30 | - If you have improvements or fixes to propose, submit a Pull Request (PR) to the relevant 31 | module or repository. 32 | - Clearly describe the purpose and changes made in your PR, providing enough context for the 33 | reviewers to understand your contribution. 34 | - Be open to feedback and engage in discussions to refine your contribution. 35 | 36 | * **Bug Reports and Feature Requests**: 37 | 38 | - Help us improve *Taikai* by reporting any bugs or issues you encounter. 39 | - If you have ideas for new features or enhancements, submit a feature request. 40 | - Use the issue tracker in the respective repository to provide detailed information about the 41 | problem or request. 42 | 43 | * **Spread the Word**: 44 | 45 | - Share your positive experience with *Taikai* and encourage others to contribute. 46 | - Tweet about your contributions, write blog posts, or mention *Taikai* in relevant communities 47 | to increase awareness. 48 | 49 | * **Help with Documentation**: 50 | 51 | - Contribute to improving the documentation by identifying areas that need clarification or 52 | adding examples and tutorials. 53 | - Submit documentation PRs to enhance the usability and understanding of *Taikai*. 54 | 55 | Remember, contributions of all sizes are valuable and appreciated. We look forward to your 56 | involvement in the *Taikai* community. Thank you for considering contributing to the project! 57 | 58 | # Sponsor 59 | 60 | [Sponsor Taikai on GitHub :heart:](https://github.com/sponsors/mnhock){ .md-button } 61 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/java/ImportsConfigurerTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.enofex.taikai.Taikai; 7 | import com.enofex.taikai.TaikaiException; 8 | import org.junit.jupiter.api.Test; 9 | 10 | class ImportsConfigurerTest { 11 | 12 | @Test 13 | void shouldImportByRegex() { 14 | Taikai taikai = Taikai.builder() 15 | .classes(ClassUsingAllowedImport.class) 16 | .java(java -> java.imports( 17 | imports -> imports.shouldImport(".*ClassUsingAllowedImport", "java\\.lang\\..*"))) 18 | .build(); 19 | 20 | assertDoesNotThrow(taikai::check); 21 | } 22 | 23 | @Test 24 | void shouldThrowExceptionForImportByRegex() { 25 | Taikai taikai = Taikai.builder() 26 | .classes(ClassUsingAllowedImport.class) 27 | .java(java -> java.imports( 28 | imports -> imports.shouldImport(".*ClassUsingAllowedImport", "java\\.not\\.found..*"))) 29 | .build(); 30 | 31 | assertThrows(AssertionError.class, taikai::check); 32 | } 33 | 34 | @Test 35 | void shouldNotImportSpecificPackage() { 36 | Taikai taikai = Taikai.builder() 37 | .classes(ClassUsingDisallowedImport.class) 38 | .java(java -> java.imports(imports -> imports.shouldNotImport("java.util"))) 39 | .build(); 40 | 41 | assertThrows(AssertionError.class, taikai::check); 42 | } 43 | 44 | @Test 45 | void shouldNotImportByRegex() { 46 | Taikai taikai = Taikai.builder() 47 | .classes(ClassUsingDisallowedImport.class) 48 | .java(java -> java.imports( 49 | imports -> imports.shouldNotImport(".*ClassUsingDisallowedImport", "java\\.util\\..*"))) 50 | .build(); 51 | 52 | assertThrows(AssertionError.class, taikai::check); 53 | } 54 | 55 | @Test 56 | void shouldThrowWhenNamespaceNotSetForCycles() { 57 | assertThrows(TaikaiException.class, () -> Taikai.builder() 58 | .classes(ClassUsingAllowedImport.class) 59 | .java(java -> java.imports(ImportsConfigurer::shouldHaveNoCycles)) // no namespace configured 60 | .build()); 61 | } 62 | 63 | @Test 64 | void shouldAllowValidImports() { 65 | Taikai taikai = Taikai.builder() 66 | .classes(ClassUsingAllowedImport.class) 67 | .java(java -> java.imports(imports -> imports.shouldNotImport("java.sql"))) 68 | .build(); 69 | 70 | assertDoesNotThrow(taikai::check); 71 | } 72 | 73 | static class ClassUsingAllowedImport { 74 | 75 | private final java.lang.String value = "ok"; 76 | } 77 | 78 | static class ClassUsingDisallowedImport { 79 | 80 | private final java.util.List list = java.util.Collections.emptyList(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Taikai 2 | 3 | First off, thank you for taking the time to contribute! :+1: :tada: 4 | 5 | ### How to Contribute 6 | 7 | #### Ask questions 8 | 9 | If you believe there is an issue, search through existing issues for this project trying a 10 | few different ways to find discussions, past or current, that are related to the issue. 11 | Reading those discussions helps you to learn about the issue, and helps us to make a 12 | decision. 13 | 14 | #### Create an Issue 15 | 16 | Reporting an issue or making a feature request is a great way to contribute. Your feedback 17 | and the conversations that result from it provide a continuous flow of ideas. However, 18 | before creating a ticket, please take the time to [ask and research](#ask-questions) first. 19 | 20 | Once you're ready, create an issue on the module. 21 | 22 | #### Before submitting a Pull Request 23 | 24 | To contribute to this project, please fork the repository and submit a pull request with your changes. Please ensure that your changes adhere to the following guidelines: 25 | 26 | * Code should follow the conventions. 27 | * All code should be well-documented. 28 | * All new functionality should be covered by tests. 29 | * Changes should not break existing functionality. 30 | 31 | #### Submit a Pull Request 32 | 33 | 1. Always check out the `main` branch and submit pull requests against it. 34 | 35 | 2. Choose the granularity of your commits consciously and squash commits that represent 36 | multiple edits or corrections of the same logical change. See 37 | [Rewriting History section of Pro Git](https://git-scm.com/book/en/Git-Tools-Rewriting-History) 38 | for an overview of streamlining the commit history. 39 | 40 | 3. Format commit messages using 55 characters for the subject line, 72 characters per line 41 | for the description, followed by the issue fixed, e.g. `Closes gh-12279`. See the 42 | [Commit Guidelines section of Pro Git](https://git-scm.com/book/en/Distributed-Git-Contributing-to-a-Project#Commit-Guidelines) 43 | for best practices around commit messages, and use `git log` to see some examples. 44 | 45 | 4. If there is a prior issue, reference the GitHub issue number in the description of the 46 | pull request. 47 | 48 | 49 | #### Participate in Reviews 50 | 51 | Helping to review pull requests is another great way to contribute. Your feedback 52 | can help to shape the implementation of new features. When reviewing pull requests, 53 | however, please refrain from approving or rejecting a PR unless you are a core 54 | committer for this project. 55 | 56 | ### Code Conventions 57 | 58 | #### Java 59 | Please follow the Google Java Style Guide when writing Java code for this project. You can also import the Intellij IDEA code style configuration file for Java from [here](https://github.com/google/styleguide/blob/gh-pages/intellij-java-google-style.xml). 60 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/internal/DescribedPredicates.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.internal; 2 | 3 | import static com.enofex.taikai.internal.Modifiers.isClassFinal; 4 | 5 | import com.tngtech.archunit.base.DescribedPredicate; 6 | import com.tngtech.archunit.core.domain.JavaClass; 7 | import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; 8 | import java.util.Collection; 9 | 10 | /** 11 | * Internal utility class for defining general DescribedPredicate used in architectural rules. 12 | *

13 | * This class is intended for internal use only and is not part of the public API. Developers should 14 | * not rely on this class for any public API usage. 15 | */ 16 | public final class DescribedPredicates { 17 | 18 | private DescribedPredicates() { 19 | } 20 | 21 | /** 22 | * Creates a predicate that checks if an element is annotated with a specific annotation. 23 | * 24 | * @param annotation the annotation to check for 25 | * @param isMetaAnnotated true if the annotation should be meta-annotated, false otherwise 26 | * @return a described predicate for the annotation check 27 | */ 28 | public static DescribedPredicate annotatedWith(String annotation, 29 | boolean isMetaAnnotated) { 30 | return new DescribedPredicate<>("annotated with %s".formatted(annotation)) { 31 | @Override 32 | public boolean test(CanBeAnnotated canBeAnnotated) { 33 | return isMetaAnnotated ? canBeAnnotated.isMetaAnnotatedWith(annotation) 34 | : canBeAnnotated.isAnnotatedWith(annotation); 35 | } 36 | }; 37 | } 38 | 39 | /** 40 | * Creates a predicate that checks if an element is annotated with all the specified annotations. 41 | * 42 | * @param annotations the collection of annotations to check for 43 | * @param isMetaAnnotated true if the annotations should be meta-annotated, false otherwise 44 | * @return a described predicate for the annotation check 45 | */ 46 | public static DescribedPredicate annotatedWithAll(Collection annotations, 47 | boolean isMetaAnnotated) { 48 | return new DescribedPredicate<>("annotated with all of %s".formatted(annotations)) { 49 | @Override 50 | public boolean test(CanBeAnnotated canBeAnnotated) { 51 | return annotations.stream().allMatch(annotation -> 52 | isMetaAnnotated ? canBeAnnotated.isMetaAnnotatedWith(annotation) 53 | : canBeAnnotated.isAnnotatedWith(annotation)); 54 | } 55 | }; 56 | } 57 | 58 | /** 59 | * Creates a predicate that checks if a class is final. 60 | * 61 | * @return a described predicate for the final modifier check 62 | */ 63 | public static DescribedPredicate areFinal() { 64 | return new DescribedPredicate<>("are final") { 65 | @Override 66 | public boolean test(JavaClass javaClass) { 67 | return isClassFinal(javaClass); 68 | } 69 | }; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /mkdocs.yml: -------------------------------------------------------------------------------- 1 | # Project information 2 | site_name: Taikai 3 | site_url: https://enofex.github.io/taikai 4 | site_author: Martin Hock 5 | site_description: >- 6 | Taikai is an extension of the popular ArchUnit library, offering a comprehensive suite of predefined rules tailored for various technologies. 7 | 8 | # Repository 9 | repo_name: taikai 10 | repo_url: https://github.com/enofex/taikai 11 | 12 | # Copyright 13 | copyright: Copyright © 2025 Martin Hock / Enofex 14 | 15 | # Configuration 16 | theme: 17 | name: material 18 | features: 19 | - announce.dismiss 20 | - content.action.edit 21 | - content.action.view 22 | - content.code.annotate 23 | - content.code.copy 24 | - content.tooltips 25 | - navigation.footer 26 | - navigation.indexes 27 | - search.share 28 | - search.suggest 29 | - toc.follow 30 | palette: 31 | - scheme: default 32 | primary: indigo 33 | accent: indigo 34 | - scheme: slate 35 | primary: indigo 36 | accent: indigo 37 | font: 38 | text: Roboto 39 | code: Roboto Mono 40 | favicon: assets/favicon.ico 41 | logo: assets/images/taikai-logo-light.png 42 | 43 | # Plugins 44 | plugins: 45 | - search: 46 | separator: '[\s\-,:!=\[\]()"`/]+|\.(?!\d)|&[lg]t;|(?!\b)(?=[A-Z][a-z])' 47 | - minify: 48 | minify_html: true 49 | 50 | # Customization 51 | extra: 52 | social: 53 | - icon: fontawesome/brands/github 54 | link: https://github.com/enofex/taikai 55 | - icon: fontawesome/brands/docker 56 | link: https://hub.docker.com/u/enofex 57 | 58 | # Extensions 59 | markdown_extensions: 60 | - pymdownx.snippets 61 | - abbr 62 | - admonition 63 | - attr_list 64 | - def_list 65 | - footnotes 66 | - md_in_html 67 | - toc: 68 | permalink: true 69 | - pymdownx.arithmatex: 70 | generic: true 71 | - pymdownx.betterem: 72 | smart_enable: all 73 | - pymdownx.caret 74 | - pymdownx.details 75 | - pymdownx.emoji: 76 | emoji_generator: !!python/name:materialx.emoji.to_svg 77 | emoji_index: !!python/name:materialx.emoji.twemoji 78 | - pymdownx.highlight: 79 | anchor_linenums: true 80 | line_spans: __span 81 | pygments_lang_class: true 82 | - pymdownx.inlinehilite 83 | - pymdownx.keys 84 | - pymdownx.magiclink: 85 | repo_url_shorthand: true 86 | user: squidfunk 87 | repo: mkdocs-material 88 | - pymdownx.mark 89 | - pymdownx.smartsymbols 90 | - pymdownx.superfences: 91 | custom_fences: 92 | - name: mermaid 93 | class: mermaid 94 | format: !!python/name:pymdownx.superfences.fence_code_format 95 | - pymdownx.tabbed: 96 | alternate_style: true 97 | - pymdownx.tasklist: 98 | custom_checkbox: true 99 | - pymdownx.tilde 100 | 101 | # Page tree 102 | nav: 103 | - Home: index.md 104 | - Documentation: documentation.md 105 | - Contributing: contributing.md -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/internal/DescribedPredicatesTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.internal; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertFalse; 4 | import static org.junit.jupiter.api.Assertions.assertTrue; 5 | import static org.mockito.Mockito.when; 6 | 7 | import com.tngtech.archunit.core.domain.JavaClass; 8 | import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; 9 | import java.util.Collections; 10 | import java.util.Set; 11 | import org.junit.jupiter.api.Test; 12 | import org.junit.jupiter.api.extension.ExtendWith; 13 | import org.mockito.Mock; 14 | import org.mockito.junit.jupiter.MockitoExtension; 15 | import org.mockito.junit.jupiter.MockitoSettings; 16 | import org.mockito.quality.Strictness; 17 | 18 | @ExtendWith(MockitoExtension.class) 19 | @MockitoSettings(strictness = Strictness.LENIENT) 20 | class DescribedPredicatesTest { 21 | 22 | @Mock 23 | private CanBeAnnotated canBeAnnotated; 24 | @Mock 25 | private JavaClass javaClass; 26 | 27 | @Test 28 | void shouldReturnTrueWhenAnnotatedWithSpecificAnnotation() { 29 | String annotation = "MyAnnotation"; 30 | when(this.canBeAnnotated.isAnnotatedWith(annotation)).thenReturn(true); 31 | 32 | assertTrue(DescribedPredicates.annotatedWith(annotation, false).test(this.canBeAnnotated)); 33 | } 34 | 35 | @Test 36 | void shouldReturnFalseWhenNotAnnotatedWithSpecificAnnotation() { 37 | String annotation = "MyAnnotation"; 38 | when(this.canBeAnnotated.isAnnotatedWith(annotation)).thenReturn(false); 39 | 40 | assertFalse(DescribedPredicates.annotatedWith(annotation, false).test(this.canBeAnnotated)); 41 | } 42 | 43 | @Test 44 | void shouldReturnTrueWhenAnnotatedWithAllAnnotations() { 45 | Set annotations = Set.of("MyAnnotation1", "MyAnnotation2"); 46 | 47 | when(this.canBeAnnotated.isAnnotatedWith("MyAnnotation1")).thenReturn(true); 48 | when(this.canBeAnnotated.isAnnotatedWith("MyAnnotation2")).thenReturn(true); 49 | 50 | assertTrue(DescribedPredicates.annotatedWithAll(annotations, false).test(this.canBeAnnotated)); 51 | } 52 | 53 | @Test 54 | void shouldReturnFalseWhenNotAnnotatedWithAllAnnotations() { 55 | Set annotations = Set.of("MyAnnotation1", "MyAnnotation2"); 56 | 57 | when(this.canBeAnnotated.isAnnotatedWith("MyAnnotation1")).thenReturn(true); 58 | when(this.canBeAnnotated.isAnnotatedWith("MyAnnotation2")).thenReturn(false); 59 | 60 | assertFalse(DescribedPredicates.annotatedWithAll(annotations, false).test(this.canBeAnnotated)); 61 | } 62 | 63 | @Test 64 | void shouldReturnTrueWhenClassIsFinal() { 65 | when(this.javaClass.getModifiers()).thenReturn( 66 | Set.of(com.tngtech.archunit.core.domain.JavaModifier.FINAL)); 67 | 68 | assertTrue(DescribedPredicates.areFinal().test(this.javaClass)); 69 | } 70 | 71 | @Test 72 | void shouldReturnFalseWhenClassIsNotFinal() { 73 | when(this.javaClass.getModifiers()).thenReturn(Collections.emptySet()); 74 | 75 | assertFalse(DescribedPredicates.areFinal().test(this.javaClass)); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/test/JUnitDescribedPredicatesTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.test; 2 | 3 | import static com.enofex.taikai.test.JUnitDescribedPredicates.ANNOTATION_PARAMETRIZED_TEST; 4 | import static com.enofex.taikai.test.JUnitDescribedPredicates.ANNOTATION_TEST; 5 | import static com.enofex.taikai.test.JUnitDescribedPredicates.annotatedWithTestOrParameterizedTest; 6 | import static com.tngtech.archunit.lang.conditions.ArchConditions.beAnnotatedWith; 7 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.methods; 8 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 9 | import static org.junit.jupiter.api.Assertions.assertTrue; 10 | 11 | import com.tngtech.archunit.core.domain.JavaClasses; 12 | import com.tngtech.archunit.core.importer.ClassFileImporter; 13 | import com.tngtech.archunit.lang.ArchRule; 14 | import com.tngtech.archunit.lang.conditions.ArchConditions; 15 | import org.junit.jupiter.api.Test; 16 | import org.junit.jupiter.params.ParameterizedTest; 17 | import org.junit.jupiter.params.provider.EmptySource; 18 | 19 | class JUnitDescribedPredicatesTest { 20 | 21 | @Test 22 | void shouldIdentifyClassesAnnotatedWithTestOrParameterizedTest() { 23 | JavaClasses importedClasses = new ClassFileImporter().importClasses( 24 | TestExample.class, ParameterizedTestExample.class); 25 | 26 | ArchRule rule = methods().that(annotatedWithTestOrParameterizedTest(false)) 27 | .should(beAnnotatedWith(ANNOTATION_TEST) 28 | .or(beAnnotatedWith(ANNOTATION_PARAMETRIZED_TEST))); 29 | 30 | assertDoesNotThrow(() -> rule.check(importedClasses)); 31 | } 32 | 33 | @Test 34 | void shouldIdentifyClassesMetaAnnotatedWithTestOrParameterizedTest() { 35 | JavaClasses importedClasses = new ClassFileImporter().importClasses( 36 | MetaTestExample.class, MetaParameterizedTestExample.class); 37 | 38 | ArchRule rule = methods().that(annotatedWithTestOrParameterizedTest(true)) 39 | .should(ArchConditions.beMetaAnnotatedWith(ANNOTATION_TEST) 40 | .or(ArchConditions.beMetaAnnotatedWith(ANNOTATION_PARAMETRIZED_TEST))); 41 | 42 | assertDoesNotThrow(() -> rule.check(importedClasses)); 43 | } 44 | 45 | private static final class TestExample { 46 | 47 | @Test 48 | void should() { 49 | assertTrue(true); 50 | } 51 | } 52 | 53 | private static final class ParameterizedTestExample { 54 | 55 | @ParameterizedTest 56 | @EmptySource 57 | void should(String empty) { 58 | assertTrue(true); 59 | } 60 | } 61 | 62 | private static class MetaTestExample { 63 | 64 | @TestAnnotation 65 | void should() { 66 | assertTrue(true); 67 | } 68 | } 69 | 70 | private static final class MetaParameterizedTestExample { 71 | 72 | @ParameterizedTestAnnotation 73 | @EmptySource 74 | void should(String empty) { 75 | assertTrue(true); 76 | } 77 | } 78 | 79 | @Test 80 | private @interface TestAnnotation { 81 | 82 | } 83 | 84 | @ParameterizedTest 85 | private @interface ParameterizedTestAnnotation { 86 | 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/spring/SpringConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.spring; 2 | 3 | import static com.enofex.taikai.TaikaiRule.Configuration.defaultConfiguration; 4 | import static com.enofex.taikai.spring.SpringDescribedPredicates.ANNOTATION_AUTOWIRED; 5 | import static com.enofex.taikai.spring.SpringDescribedPredicates.annotatedWithAutowired; 6 | import static com.tngtech.archunit.lang.conditions.ArchConditions.be; 7 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noFields; 8 | 9 | import com.enofex.taikai.TaikaiRule; 10 | import com.enofex.taikai.TaikaiRule.Configuration; 11 | import com.enofex.taikai.configures.AbstractConfigurer; 12 | import com.enofex.taikai.configures.ConfigurerContext; 13 | import com.enofex.taikai.configures.Customizer; 14 | import com.enofex.taikai.configures.DisableableConfigurer; 15 | 16 | public final class SpringConfigurer extends AbstractConfigurer implements DisableableConfigurer { 17 | 18 | public SpringConfigurer(ConfigurerContext configurerContext) { 19 | super(configurerContext); 20 | } 21 | 22 | public SpringConfigurer properties(Customizer customizer) { 23 | return customizer(customizer, () -> new PropertiesConfigurer(configurerContext())); 24 | } 25 | 26 | public SpringConfigurer configurations( 27 | Customizer customizer) { 28 | return customizer(customizer, () -> new ConfigurationsConfigurer(configurerContext())); 29 | } 30 | 31 | public SpringConfigurer controllers( 32 | Customizer customizer) { 33 | return customizer(customizer, () -> new ControllersConfigurer(configurerContext())); 34 | } 35 | 36 | public SpringConfigurer services(Customizer customizer) { 37 | return customizer(customizer, () -> new ServicesConfigurer(configurerContext())); 38 | } 39 | 40 | public SpringConfigurer repositories( 41 | Customizer customizer) { 42 | return customizer(customizer, () -> new RepositoriesConfigurer(configurerContext())); 43 | } 44 | 45 | public SpringConfigurer boot(Customizer customizer) { 46 | return customizer(customizer, () -> new BootConfigurer(configurerContext())); 47 | } 48 | 49 | public SpringConfigurer noAutowiredFields() { 50 | return noAutowiredFields(defaultConfiguration()); 51 | } 52 | 53 | public SpringConfigurer noAutowiredFields(Configuration configuration) { 54 | return addRule(TaikaiRule.of(noFields() 55 | .should(be(annotatedWithAutowired(true))) 56 | .as("No fields should be annotated with %s, use constructor injection".formatted( 57 | ANNOTATION_AUTOWIRED)), configuration)); 58 | } 59 | 60 | @Override 61 | public SpringConfigurer disable() { 62 | disable(SpringConfigurer.class); 63 | disable(PropertiesConfigurer.class); 64 | disable(ConfigurationsConfigurer.class); 65 | disable(ControllersConfigurer.class); 66 | disable(ServicesConfigurer.class); 67 | disable(RepositoriesConfigurer.class); 68 | disable(BootConfigurer.class); 69 | 70 | return this; 71 | } 72 | } -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/java/ClassesShouldImplementTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.enofex.taikai.Taikai; 7 | import org.junit.jupiter.api.Nested; 8 | import org.junit.jupiter.api.Test; 9 | 10 | class ClassesShouldImplementTest { 11 | 12 | @Nested 13 | class ClassBasedAPI { 14 | 15 | @Test 16 | void shouldNotThrowWhenClassImplementsInterface() { 17 | Taikai taikai = Taikai.builder() 18 | .classes(ServiceImpl.class, MyService.class) 19 | .java(java -> java.classesShouldImplement("ServiceImpl", MyService.class)) 20 | .build(); 21 | 22 | assertDoesNotThrow(taikai::check); 23 | } 24 | 25 | @Test 26 | void shouldThrowWhenClassDoesNotImplementInterface() { 27 | Taikai taikai = Taikai.builder() 28 | .classes(ServiceWithoutInterface.class, MyService.class) 29 | .java(java -> java.classesShouldImplement("ServiceWithoutInterface", MyService.class)) 30 | .build(); 31 | 32 | assertThrows(AssertionError.class, taikai::check); 33 | } 34 | 35 | @Test 36 | void shouldNotThrowWhenRegexDoesNotMatchAnyClass() { 37 | Taikai taikai = Taikai.builder() 38 | .classes(ServiceImpl.class, MyService.class) 39 | .java(java -> java.classesShouldImplement("NonExistent", MyService.class)) 40 | .build(); 41 | 42 | assertDoesNotThrow(taikai::check); 43 | } 44 | } 45 | 46 | @Nested 47 | class StringBasedAPI { 48 | 49 | @Test 50 | void shouldNotThrowWhenClassImplementsInterface() { 51 | Taikai taikai = Taikai.builder() 52 | .classes(ServiceImpl.class, MyService.class) 53 | .java(java -> java.classesShouldImplement("ServiceImpl", MyService.class.getName())) 54 | .build(); 55 | 56 | assertDoesNotThrow(taikai::check); 57 | } 58 | 59 | @Test 60 | void shouldThrowWhenClassDoesNotImplementInterface() { 61 | Taikai taikai = Taikai.builder() 62 | .classes(ServiceWithoutInterface.class, MyService.class) 63 | .java(java -> java.classesShouldImplement("ServiceWithoutInterface", MyService.class.getName())) 64 | .build(); 65 | 66 | assertThrows(AssertionError.class, taikai::check); 67 | } 68 | 69 | @Test 70 | void shouldNotThrowWhenRegexDoesNotMatchAnyClass() { 71 | Taikai taikai = Taikai.builder() 72 | .classes(ServiceImpl.class, MyService.class) 73 | .java(java -> java.classesShouldImplement("NonExistent", MyService.class.getName())) 74 | .build(); 75 | 76 | assertDoesNotThrow(taikai::check); 77 | } 78 | } 79 | 80 | interface MyService { 81 | void execute(); 82 | } 83 | 84 | static class ServiceImpl implements MyService { 85 | @Override 86 | public void execute() { 87 | // Implementation here 88 | } 89 | } 90 | 91 | static class ServiceWithoutInterface { 92 | public void execute() { 93 | // No interface implemented 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/java/Deprecations.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import com.tngtech.archunit.core.domain.JavaClass; 4 | import com.tngtech.archunit.core.domain.JavaType; 5 | import com.tngtech.archunit.lang.ArchCondition; 6 | import com.tngtech.archunit.lang.ConditionEvents; 7 | import com.tngtech.archunit.lang.SimpleConditionEvent; 8 | 9 | final class Deprecations { 10 | 11 | private Deprecations() { 12 | } 13 | 14 | static ArchCondition notUseDeprecatedAPIs() { 15 | return new ArchCondition("not use deprecated APIs") { 16 | @Override 17 | public void check(JavaClass javaClass, ConditionEvents events) { 18 | javaClass.getFieldAccessesFromSelf().stream() 19 | .filter(access -> access.getTarget().isAnnotatedWith(Deprecated.class)) 20 | .forEach(access -> events.add(SimpleConditionEvent.violated(access.getTarget(), 21 | "Field %s in class %s is deprecated and is being accessed by %s".formatted( 22 | access.getTarget().getName(), 23 | access.getTarget().getOwner().getName(), 24 | javaClass.getName())))); 25 | 26 | javaClass.getMethodCallsFromSelf().stream() 27 | .filter(method -> !method.getTarget().getName().equals(Object.class.getName())) 28 | .filter(method -> !method.getTarget().getName().equals(Enum.class.getName())) 29 | .filter(method -> method.getTarget().isAnnotatedWith(Deprecated.class) || 30 | method.getTarget().getRawReturnType().isAnnotatedWith(Deprecated.class) || 31 | method.getTarget().getParameterTypes().stream() 32 | .anyMatch(Deprecations::isDeprecated)) 33 | .forEach(method -> events.add(SimpleConditionEvent.violated(method, 34 | "Method %s used in class %s is deprecated".formatted( 35 | method.getName(), 36 | javaClass.getName())))); 37 | 38 | javaClass.getConstructorCallsFromSelf().stream() 39 | .filter(constructor -> constructor.getTarget().isAnnotatedWith(Deprecated.class) || 40 | constructor.getTarget().getParameterTypes().stream() 41 | .anyMatch(Deprecations::isDeprecated)) 42 | .forEach(constructor -> events.add(SimpleConditionEvent.violated(constructor, 43 | "Constructor %s in class %s uses deprecated APIs".formatted( 44 | constructor.getTarget().getFullName(), 45 | javaClass.getName())))); 46 | 47 | javaClass.getDirectDependenciesFromSelf().stream() 48 | .filter(dependency -> dependency.getTargetClass().isAnnotatedWith(Deprecated.class)) 49 | .forEach(dependency -> events.add( 50 | SimpleConditionEvent.violated(dependency.getTargetClass(), 51 | "Class %s depends on deprecated class %s".formatted( 52 | javaClass.getName(), 53 | dependency.getTargetClass().getName())))); 54 | } 55 | }.as("no usage of deprecated APIs"); 56 | } 57 | 58 | private static boolean isDeprecated(JavaType javaType) { 59 | return javaType.toErasure().isAnnotatedWith(Deprecated.class); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/java/ImportPatterns.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | /** 4 | * Predefined import patterns for commonly used Java and framework packages. These patterns can be 5 | * used with Taikai import rules such as: 6 | * 7 | *

  8 |  *   .imports(imports -> imports
  9 |  *       .shouldNotImport(lombok()))
 10 |  * 
11 | */ 12 | public final class ImportPatterns { 13 | 14 | private ImportPatterns() { 15 | } 16 | 17 | /** 18 | * Matches imports under {@code org.apache.commons}. 19 | */ 20 | public static String apacheCommons() { 21 | return "org.apache.commons.."; 22 | } 23 | 24 | /** 25 | * Matches imports under {@code org.assertj}. 26 | */ 27 | public static String assertJ() { 28 | return "org.assertj.."; 29 | } 30 | 31 | /** 32 | * Matches imports under {@code org.hamcrest}. 33 | */ 34 | public static String hamcrest() { 35 | return "org.hamcrest.."; 36 | } 37 | 38 | /** 39 | * Matches imports under {@code org.hibernate}. 40 | */ 41 | public static String hibernate() { 42 | return "org.hibernate.."; 43 | } 44 | 45 | /** 46 | * Matches imports under {@code org.jspecify}. 47 | */ 48 | public static String jspecify() { 49 | return "org.jspecify.."; 50 | } 51 | 52 | /** 53 | * Matches imports under {@code org.junit} (JUnit 4). 54 | */ 55 | public static String junit4() { 56 | return "org.junit.."; 57 | } 58 | 59 | /** 60 | * Matches imports under {@code org.junit.jupiter} (JUnit 5 and higher). 61 | */ 62 | public static String junit() { 63 | return "org.junit.jupiter.."; 64 | } 65 | 66 | /** 67 | * Matches imports under {@code ch.qos.logback}. 68 | */ 69 | public static String logback() { 70 | return "ch.qos.logback.."; 71 | } 72 | 73 | /** 74 | * Matches imports under {@code lombok}. 75 | */ 76 | public static String lombok() { 77 | return "lombok.."; 78 | } 79 | 80 | /** 81 | * Matches imports under {@code org.mockito}. 82 | */ 83 | public static String mockito() { 84 | return "org.mockito.."; 85 | } 86 | 87 | /** 88 | * Matches shaded or relocated imports containing {@code .shaded.} in the package path. 89 | */ 90 | public static String shaded() { 91 | return "..shaded.."; 92 | } 93 | 94 | /** 95 | * Matches imports under {@code org.springframework.boot}. 96 | */ 97 | public static String springBoot() { 98 | return "org.springframework.boot.."; 99 | } 100 | 101 | /** 102 | * Matches imports under {@code org.springframework.data}. 103 | */ 104 | public static String springData() { 105 | return "org.springframework.data.."; 106 | } 107 | 108 | /** 109 | * Matches imports under {@code org.springframework}. 110 | */ 111 | public static String springFramework() { 112 | return "org.springframework.."; 113 | } 114 | 115 | /** 116 | * Matches imports under {@code org.springframework.security}. 117 | */ 118 | public static String springSecurity() { 119 | return "org.springframework.security.."; 120 | } 121 | 122 | /** 123 | * Matches imports under {@code org.testcontainers}. 124 | */ 125 | public static String testcontainers() { 126 | return "org.testcontainers.."; 127 | } 128 | } -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/spring/SpringDescribedPredicates.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.spring; 2 | 3 | import static com.enofex.taikai.internal.DescribedPredicates.annotatedWith; 4 | 5 | import com.tngtech.archunit.base.DescribedPredicate; 6 | import com.tngtech.archunit.core.domain.properties.CanBeAnnotated; 7 | 8 | final class SpringDescribedPredicates { 9 | 10 | static final String ANNOTATION_CONFIGURATION = "org.springframework.context.annotation.Configuration"; 11 | static final String ANNOTATION_CONFIGURATION_PROPERTIES = "org.springframework.boot.context.properties.ConfigurationProperties"; 12 | static final String ANNOTATION_CONTROLLER = "org.springframework.stereotype.Controller"; 13 | static final String ANNOTATION_REST_CONTROLLER = "org.springframework.web.bind.annotation.RestController"; 14 | static final String ANNOTATION_SERVICE = "org.springframework.stereotype.Service"; 15 | static final String ANNOTATION_REPOSITORY = "org.springframework.stereotype.Repository"; 16 | static final String ANNOTATION_SPRING_BOOT_APPLICATION = "org.springframework.boot.autoconfigure.SpringBootApplication"; 17 | static final String ANNOTATION_AUTOWIRED = "org.springframework.beans.factory.annotation.Autowired"; 18 | static final String ANNOTATION_VALIDATED = "org.springframework.validation.annotation.Validated"; 19 | 20 | private SpringDescribedPredicates() { 21 | } 22 | 23 | static DescribedPredicate annotatedWithControllerOrRestController( 24 | boolean isMetaAnnotated) { 25 | 26 | return annotatedWith(ANNOTATION_CONTROLLER, isMetaAnnotated) 27 | .or(annotatedWith(ANNOTATION_REST_CONTROLLER, isMetaAnnotated)); 28 | } 29 | 30 | static DescribedPredicate annotatedWithConfiguration( 31 | boolean isMetaAnnotated) { 32 | return annotatedWith(ANNOTATION_CONFIGURATION, isMetaAnnotated); 33 | } 34 | 35 | static DescribedPredicate annotatedWithConfigurationProperties( 36 | boolean isMetaAnnotated) { 37 | return annotatedWith(ANNOTATION_CONFIGURATION_PROPERTIES, isMetaAnnotated); 38 | } 39 | 40 | static DescribedPredicate annotatedWithRestController(boolean isMetaAnnotated) { 41 | return annotatedWith(ANNOTATION_REST_CONTROLLER, isMetaAnnotated); 42 | } 43 | 44 | static DescribedPredicate annotatedWithController(boolean isMetaAnnotated) { 45 | return annotatedWith(ANNOTATION_CONTROLLER, isMetaAnnotated); 46 | } 47 | 48 | static DescribedPredicate annotatedWithService(boolean isMetaAnnotated) { 49 | return annotatedWith(ANNOTATION_SERVICE, isMetaAnnotated); 50 | } 51 | 52 | static DescribedPredicate annotatedWithRepository(boolean isMetaAnnotated) { 53 | return annotatedWith(ANNOTATION_REPOSITORY, isMetaAnnotated); 54 | } 55 | 56 | static DescribedPredicate annotatedWithSpringBootApplication( 57 | boolean isMetaAnnotated) { 58 | return annotatedWith(ANNOTATION_SPRING_BOOT_APPLICATION, isMetaAnnotated); 59 | } 60 | 61 | static DescribedPredicate annotatedWithAutowired(boolean isMetaAnnotated) { 62 | return annotatedWith(ANNOTATION_AUTOWIRED, isMetaAnnotated); 63 | } 64 | 65 | static DescribedPredicate annotatedWithValidated(boolean isMetaAnnotated) { 66 | return annotatedWith(ANNOTATION_VALIDATED, isMetaAnnotated); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/java/ConstantNamingTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.enofex.taikai.Taikai; 7 | import java.io.Serializable; 8 | import java.util.Collections; 9 | import java.util.List; 10 | import org.junit.jupiter.api.Test; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | class ConstantNamingTest { 15 | 16 | @Test 17 | void shouldApplyConstantNamingConvention() { 18 | Taikai taikai = Taikai.builder() 19 | .classes(ValidConstants.class) 20 | .java(java -> java.naming(NamingConfigurer::constantsShouldFollowConventions)) 21 | .build(); 22 | 23 | assertDoesNotThrow(taikai::check); 24 | } 25 | 26 | @Test 27 | void shouldThrowWhenConstantDoesNotFollowConvention() { 28 | Taikai taikai = Taikai.builder() 29 | .classes(InvalidConstants.class) 30 | .java(java -> java.naming(NamingConfigurer::constantsShouldFollowConventions)) 31 | .build(); 32 | 33 | assertThrows(AssertionError.class, taikai::check); 34 | } 35 | 36 | @Test 37 | void shouldIgnoreSerialVersionUID() { 38 | Taikai taikai = Taikai.builder() 39 | .classes(SerialVersionUIDClass.class) 40 | .java(java -> java.naming(NamingConfigurer::constantsShouldFollowConventions)) 41 | .build(); 42 | 43 | assertDoesNotThrow(taikai::check); 44 | } 45 | 46 | @Test 47 | void shouldIgnoreExcludedFields() { 48 | Taikai taikai = Taikai.builder() 49 | .classes(LoggerField.class) 50 | .java( 51 | java -> java.naming(naming -> naming.constantsShouldFollowConventions(List.of("log")))) 52 | .build(); 53 | 54 | assertDoesNotThrow(taikai::check); 55 | } 56 | 57 | @Test 58 | void shouldIgnoreExcludedFieldsIfMultipleFieldNamesAreProvided() { 59 | Taikai taikai = Taikai.builder() 60 | .classes(LoggerField.class) 61 | .java( 62 | java -> java.naming( 63 | naming -> naming.constantsShouldFollowConventions(List.of("log", "logger")))) 64 | .build(); 65 | 66 | assertDoesNotThrow(taikai::check); 67 | } 68 | 69 | @Test 70 | void shouldThrowOnSerialVersionUIDWhenNoFieldsAreExcluded() { 71 | Taikai taikai = Taikai.builder() 72 | .classes(SerialVersionUIDClass.class) 73 | .java( 74 | java -> java.naming( 75 | naming -> naming.constantsShouldFollowConventions(Collections.emptyList()))) 76 | .build(); 77 | 78 | assertThrows(AssertionError.class, taikai::check); 79 | } 80 | 81 | static class ValidConstants { 82 | 83 | private static final String SOME_VALUE = "ABC"; 84 | private static final int MAX_COUNT = 42; 85 | } 86 | 87 | static class InvalidConstants { 88 | 89 | private static final String someValue = "bad"; // not all caps 90 | private static final int MaxCount = 5; // not fully uppercase 91 | } 92 | 93 | static class SerialVersionUIDClass implements Serializable { 94 | 95 | private static final long serialVersionUID = 1L; 96 | } 97 | 98 | static class LoggerField { 99 | 100 | private static final Logger log = LoggerFactory.getLogger(LoggerField.class); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/java/MethodsShouldBeAnnotatedWithTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.enofex.taikai.Taikai; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | import org.junit.jupiter.api.Nested; 10 | import org.junit.jupiter.api.Test; 11 | 12 | class MethodsShouldBeAnnotatedWithTest { 13 | 14 | @Nested 15 | class ClassBasedAPI { 16 | 17 | @Test 18 | void shouldNotThrowWhenMatchingMethodsAreAnnotated() { 19 | Taikai taikai = Taikai.builder() 20 | .classes(AnnotatedService.class) 21 | .java(java -> java.methodsShouldBeAnnotatedWith("process", TestAnnotation.class)) 22 | .build(); 23 | 24 | assertDoesNotThrow(taikai::check); 25 | } 26 | 27 | @Test 28 | void shouldThrowWhenMatchingMethodsAreNotAnnotated() { 29 | Taikai taikai = Taikai.builder() 30 | .classes(NonAnnotatedService.class) 31 | .java(java -> java.methodsShouldBeAnnotatedWith("process", TestAnnotation.class)) 32 | .build(); 33 | 34 | assertThrows(AssertionError.class, taikai::check); 35 | } 36 | 37 | @Test 38 | void shouldNotThrowWhenRegexDoesNotMatchAnyMethod() { 39 | Taikai taikai = Taikai.builder() 40 | .classes(AnnotatedService.class) 41 | .java(java -> java.methodsShouldBeAnnotatedWith("nonExistent", TestAnnotation.class)) 42 | .build(); 43 | 44 | assertDoesNotThrow(taikai::check); 45 | } 46 | } 47 | 48 | @Nested 49 | class StringBasedAPI { 50 | 51 | @Test 52 | void shouldNotThrowWhenMatchingMethodsAreAnnotated() { 53 | Taikai taikai = Taikai.builder() 54 | .classes(AnnotatedService.class) 55 | .java(java -> java.methodsShouldBeAnnotatedWith( 56 | "process", TestAnnotation.class.getName())) 57 | .build(); 58 | 59 | assertDoesNotThrow(taikai::check); 60 | } 61 | 62 | @Test 63 | void shouldThrowWhenMatchingMethodsAreNotAnnotated() { 64 | Taikai taikai = Taikai.builder() 65 | .classes(NonAnnotatedService.class) 66 | .java(java -> java.methodsShouldBeAnnotatedWith( 67 | "process", TestAnnotation.class.getName())) 68 | .build(); 69 | 70 | assertThrows(AssertionError.class, taikai::check); 71 | } 72 | 73 | @Test 74 | void shouldNotThrowWhenRegexDoesNotMatchAnyMethod() { 75 | Taikai taikai = Taikai.builder() 76 | .classes(AnnotatedService.class) 77 | .java(java -> java.methodsShouldBeAnnotatedWith( 78 | "nonExistent", TestAnnotation.class.getName())) 79 | .build(); 80 | 81 | assertDoesNotThrow(taikai::check); 82 | } 83 | } 84 | 85 | @Retention(RetentionPolicy.RUNTIME) 86 | @interface TestAnnotation { 87 | } 88 | 89 | static class AnnotatedService { 90 | @TestAnnotation 91 | void process() { 92 | // annotated correctly 93 | } 94 | 95 | void otherMethod() { 96 | // ignored, name does not match 97 | } 98 | } 99 | 100 | static class NonAnnotatedService { 101 | void process() { 102 | // missing required annotation 103 | } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/test/ContainAssertionsOrVerifications.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.test; 2 | 3 | import com.tngtech.archunit.core.domain.JavaMethod; 4 | import com.tngtech.archunit.core.domain.JavaMethodCall; 5 | import com.tngtech.archunit.lang.ArchCondition; 6 | import com.tngtech.archunit.lang.ConditionEvents; 7 | import com.tngtech.archunit.lang.SimpleConditionEvent; 8 | 9 | final class ContainAssertionsOrVerifications { 10 | 11 | private ContainAssertionsOrVerifications() { 12 | } 13 | 14 | static ArchCondition containAssertionsOrVerifications() { 15 | return new ArchCondition<>("a unit test should assert or verify something") { 16 | @Override 17 | public void check(JavaMethod item, ConditionEvents events) { 18 | for (JavaMethodCall call : item.getMethodCallsFromSelf()) { 19 | if (junit(call) || 20 | mockito(call) || 21 | hamcrest(call) || 22 | assertJ(call) || 23 | truth(call) || 24 | cucumber(call) || 25 | springMockMvc(call) || 26 | archRule(call) || 27 | taikai(call) 28 | ) { 29 | return; 30 | } 31 | } 32 | events.add(SimpleConditionEvent.violated( 33 | item, 34 | "%s does not assert or verify anything".formatted(item.getDescription())) 35 | ); 36 | } 37 | 38 | private boolean junit(JavaMethodCall call) { 39 | return "org.junit.jupiter.api.Assertions".equals(call.getTargetOwner().getName()); 40 | } 41 | 42 | private boolean mockito(JavaMethodCall call) { 43 | return "org.mockito.Mockito".equals(call.getTargetOwner().getName()) 44 | && (call.getName().startsWith("verify") 45 | || "inOrder".equals(call.getName()) 46 | || "capture".equals(call.getName())); 47 | } 48 | 49 | private boolean hamcrest(JavaMethodCall call) { 50 | return "org.hamcrest.MatcherAssert".equals(call.getTargetOwner().getName()); 51 | } 52 | 53 | private boolean assertJ(JavaMethodCall call) { 54 | return "org.assertj.core.api.Assertions".equals(call.getTargetOwner().getName()); 55 | } 56 | 57 | private boolean truth(JavaMethodCall call) { 58 | return "com.google.common.truth.Truth".equals(call.getTargetOwner().getName()); 59 | } 60 | 61 | private boolean cucumber(JavaMethodCall call) { 62 | return "io.cucumber.java.en.Then".equals(call.getTargetOwner().getName()) || 63 | "io.cucumber.java.en.Given".equals(call.getTargetOwner().getName()); 64 | } 65 | 66 | private boolean springMockMvc(JavaMethodCall call) { 67 | return 68 | "org.springframework.test.web.servlet.ResultActions".equals(call.getTargetOwner().getName()) 69 | && ("andExpect".equals(call.getName()) || "andExpectAll".equals(call.getName())); 70 | } 71 | 72 | private boolean archRule(JavaMethodCall call) { 73 | return "com.tngtech.archunit.lang.ArchRule".equals(call.getTargetOwner().getName()) 74 | && "check".equals(call.getName()); 75 | } 76 | 77 | private boolean taikai(JavaMethodCall call) { 78 | return "com.enofex.taikai.Taikai".equals(call.getTargetOwner().getName()) 79 | && ("check".equals(call.getName()) || "checkAll".equals(call.getName())) ; 80 | } 81 | }; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/java/ClassRecordTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.enofex.taikai.Taikai; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | import org.junit.jupiter.api.Nested; 10 | import org.junit.jupiter.api.Test; 11 | 12 | class ClassRecordTest { 13 | 14 | @Nested 15 | class ClassesAnnotatedWithShouldBeRecords { 16 | 17 | @Test 18 | void shouldNotThrowWhenAnnotatedClassesAreRecords_ClassVersion() { 19 | Taikai taikai = Taikai.builder() 20 | .classes(ValidRecord.class) 21 | .java(java -> java.classesAnnotatedWithShouldBeRecords(TestAnnotation.class)) 22 | .build(); 23 | 24 | assertDoesNotThrow(taikai::check); 25 | } 26 | 27 | @Test 28 | void shouldThrowWhenAnnotatedClassesAreNotRecords_ClassVersion() { 29 | Taikai taikai = Taikai.builder() 30 | .classes(InvalidNonRecord.class) 31 | .java(java -> java.classesAnnotatedWithShouldBeRecords(TestAnnotation.class)) 32 | .build(); 33 | 34 | assertThrows(AssertionError.class, taikai::check); 35 | } 36 | 37 | @Test 38 | void shouldNotThrowWhenAnnotatedClassesAreRecords_StringVersion() { 39 | Taikai taikai = Taikai.builder() 40 | .classes(ValidRecord.class) 41 | .java(java -> java.classesAnnotatedWithShouldBeRecords(TestAnnotation.class.getName())) 42 | .build(); 43 | 44 | assertDoesNotThrow(taikai::check); 45 | } 46 | 47 | @Test 48 | void shouldThrowWhenAnnotatedClassesAreNotRecords_StringVersion() { 49 | Taikai taikai = Taikai.builder() 50 | .classes(InvalidNonRecord.class) 51 | .java(java -> java.classesAnnotatedWithShouldBeRecords(TestAnnotation.class.getName())) 52 | .build(); 53 | 54 | assertThrows(AssertionError.class, taikai::check); 55 | } 56 | } 57 | 58 | @Nested 59 | class ClassesShouldBeRecords { 60 | 61 | @Test 62 | void shouldNotThrowWhenClassNameMatchesAndIsRecord() { 63 | Taikai taikai = Taikai.builder() 64 | .classes(ValidRecord.class) 65 | .java(java -> java.classesShouldBeRecords(".*ValidRecord")) 66 | .build(); 67 | 68 | assertDoesNotThrow(taikai::check); 69 | } 70 | 71 | @Test 72 | void shouldThrowWhenClassNameMatchesAndIsNotRecord() { 73 | Taikai taikai = Taikai.builder() 74 | .classes(InvalidNonRecord.class) 75 | .java(java -> java.classesShouldBeRecords(".*InvalidNonRecord")) 76 | .build(); 77 | 78 | assertThrows(AssertionError.class, taikai::check); 79 | } 80 | 81 | @Test 82 | void shouldNotThrowWhenNoClassNameMatches() { 83 | Taikai taikai = Taikai.builder() 84 | .classes(ValidRecord.class) 85 | .java(java -> java.classesShouldBeRecords(".*DoesNotExist")) 86 | .build(); 87 | 88 | assertDoesNotThrow(taikai::check); 89 | } 90 | } 91 | 92 | @Retention(RetentionPolicy.RUNTIME) 93 | @interface TestAnnotation { } 94 | 95 | @TestAnnotation 96 | record ValidRecord(String name, int id) { } 97 | 98 | @TestAnnotation 99 | static class InvalidNonRecord { 100 | private final String name = "test"; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # 2 | 3 | Taikai logo 4 | 5 | *Taikai* extends the capabilities of the popular ArchUnit library by offering a comprehensive suite of predefined rules tailored for various technologies. It simplifies the enforcement of architectural constraints and best practices in your codebase, ensuring consistency and quality across your projects. 6 | 7 | [Get Started](./documentation){ .md-button .md-button--primary } 8 | [View on GitHub :simple-github:](https://github.com/enofex/taikai){ .md-button } 9 | 10 | ## Example Usage 11 | 12 | ```java 13 | class ArchitectureTest { 14 | 15 | @Test 16 | void shouldFulfilConstrains() { 17 | Taikai.builder() 18 | .namespace("com.company.project") 19 | .java(java -> java 20 | .noUsageOfDeprecatedAPIs() 21 | .classesShouldImplementHashCodeAndEquals() 22 | .methodsShouldNotDeclareGenericExceptions() 23 | .utilityClassesShouldBeFinalAndHavePrivateConstructor() 24 | .imports(imports -> imports 25 | .shouldHaveNoCycles() 26 | .shouldNotImport("..internal..") 27 | .shouldNotImport(junit4())) 28 | .naming(naming -> naming 29 | .classesShouldNotMatch(".*Impl") 30 | .methodsShouldNotMatch("^(foo$|bar$).*") 31 | .fieldsShouldNotMatch(".*(List|Set|Map)$") 32 | .constantsShouldFollowConventions() 33 | .interfacesShouldNotHavePrefixI())) 34 | .test(test -> test 35 | .junit(junit -> junit 36 | .classesShouldNotBeAnnotatedWithDisabled() 37 | .methodsShouldNotBeAnnotatedWithDisabled())) 38 | .logging(logging -> logging 39 | .loggersShouldFollowConventions(Logger.class, "logger", List.of(PRIVATE, FINAL))) 40 | .spring(spring -> spring 41 | .noAutowiredFields() 42 | .boot(boot -> boot 43 | .applicationClassShouldResideInPackage()) 44 | .configurations(configuration -> configuration 45 | .namesShouldEndWithConfiguration() 46 | .namesShouldMatch("regex")) 47 | .controllers(controllers -> controllers 48 | .shouldBeAnnotatedWithRestController() 49 | .namesShouldEndWithController() 50 | .namesShouldMatch("regex") 51 | .shouldNotDependOnOtherControllers() 52 | .shouldBePackagePrivate())) 53 | .services(services -> services 54 | .namesShouldEndWithService() 55 | .shouldBeAnnotatedWithService()) 56 | .repositories(repositories -> repositories 57 | .namesShouldEndWithRepository() 58 | .shouldBeAnnotatedWithRepository()) 59 | .build() 60 | .check(); 61 | } 62 | } 63 | ``` 64 | 65 | ## Sponsors 66 | 67 | If *Taikai* has helped you save time and money, I invite you to support my work by becoming a 68 | sponsor. 69 | By becoming a [sponsor](https://github.com/sponsors/mnhock), you enable me to continue to improve 70 | Taikai's capabilities by fixing bugs immediately and continually adding new useful features. Your 71 | sponsorship plays an important role in making *Taikai* even better. 72 | 73 | ## Backers 74 | 75 | The Open Source Community and [Enofex](https://enofex.com) 76 | 77 | ## License 78 | 79 | See [LICENSE](https://github.com/enofex/taikai/blob/main/LICENSE). 80 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/logging/LoggingConfigurerTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.logging; 2 | 3 | import static com.tngtech.archunit.core.domain.JavaModifier.FINAL; 4 | import static com.tngtech.archunit.core.domain.JavaModifier.PRIVATE; 5 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 6 | import static org.junit.jupiter.api.Assertions.assertThrows; 7 | 8 | import com.enofex.taikai.Taikai; 9 | import java.util.List; 10 | import java.util.logging.Logger; 11 | import org.junit.jupiter.api.Test; 12 | 13 | class LoggingConfigurerTest { 14 | 15 | @Test 16 | void shouldApplyLoggerConventionsWithClass() { 17 | Taikai taikai = Taikai.builder() 18 | .classes(LoggerConventionsFollowed.class) 19 | .logging(logging -> logging.loggersShouldFollowConventions(Logger.class, "logger", 20 | List.of(PRIVATE, FINAL))) 21 | .build(); 22 | 23 | assertDoesNotThrow(taikai::check); 24 | } 25 | 26 | @Test 27 | void shouldApplyLoggerConventionsWithTypeName() { 28 | Taikai taikai = Taikai.builder() 29 | .classes(LoggerConventionsFollowed.class) 30 | .logging(logging -> logging 31 | .loggersShouldFollowConventions("java.util.logging.Logger", "logger", 32 | List.of(PRIVATE, FINAL))) 33 | .build(); 34 | 35 | assertDoesNotThrow(taikai::check); 36 | } 37 | 38 | /** 39 | * Violations in base classes should not be reported, as the base class may be part 40 | * of an external library, which is not under control of the developer. 41 | */ 42 | @Test 43 | void shouldApplyLoggerConventionsEvenIfLoggerInBaseClassDoesNotFollowConventions() { 44 | Taikai taikai = Taikai.builder() 45 | .classes(SubClassNotViolatingConventions.class) 46 | .logging(logging -> logging 47 | .loggersShouldFollowConventions("java.util.logging.Logger", "logger", 48 | List.of(PRIVATE, FINAL))) 49 | .build(); 50 | 51 | assertDoesNotThrow(taikai::check); 52 | } 53 | 54 | @Test 55 | void shouldThrowLoggerConventionsWithClassNaming() { 56 | Taikai taikai = Taikai.builder() 57 | .classes(LoggerConventionsNotFollowedNaming.class) 58 | .logging(logging -> logging.loggersShouldFollowConventions(Logger.class, "logger", 59 | List.of(PRIVATE, FINAL))) 60 | .build(); 61 | 62 | assertThrows(AssertionError.class, taikai::check); 63 | } 64 | 65 | @Test 66 | void shouldThrowLoggerConventionsWithClassModifier() { 67 | Taikai taikai = Taikai.builder() 68 | .classes(LoggerConventionsPartiallyModifier.class) 69 | .logging(logging -> logging.loggersShouldFollowConventions(Logger.class, "logger", 70 | List.of(PRIVATE, FINAL))) 71 | .build(); 72 | 73 | assertThrows(AssertionError.class, taikai::check); 74 | } 75 | 76 | private static class LoggerConventionsFollowed { 77 | private static final Logger logger = Logger.getLogger( 78 | LoggerConventionsFollowed.class.getName()); 79 | } 80 | 81 | private static class LoggerConventionsNotFollowedNaming { 82 | public static Logger LOGGER = Logger.getLogger( 83 | LoggerConventionsNotFollowedNaming.class.getName()); 84 | } 85 | 86 | private static class LoggerConventionsPartiallyModifier { 87 | private Logger logger = Logger.getLogger( 88 | LoggerConventionsPartiallyModifier.class.getName()); 89 | } 90 | 91 | private static class SubClassNotViolatingConventions extends LoggerConventionsNotFollowedNaming { 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/java/UtilityClassesTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.enofex.taikai.Taikai; 7 | import org.junit.jupiter.api.Test; 8 | 9 | class UtilityClassesTest { 10 | 11 | @Test 12 | void shouldApplyFinalUtilityClassWithPrivateConstructor() { 13 | Taikai taikai = Taikai.builder() 14 | .classes(ValidUtilityClass.class) 15 | .java(JavaConfigurer::utilityClassesShouldBeFinalAndHavePrivateConstructor) 16 | .build(); 17 | 18 | assertDoesNotThrow(taikai::check); 19 | } 20 | 21 | @Test 22 | void shouldApplyExceptionClassWithFactoryMethods() { 23 | Taikai taikai = Taikai.builder() 24 | .classes(ExceptionClassWithFactoryMethod.class) 25 | .java(JavaConfigurer::utilityClassesShouldBeFinalAndHavePrivateConstructor) 26 | .build(); 27 | 28 | assertDoesNotThrow(taikai::check); 29 | } 30 | 31 | @Test 32 | void shouldThrowUtilityClassThatIsNotFinal() { 33 | Taikai taikai = Taikai.builder() 34 | .classes(NonFinalUtilityClass.class) 35 | .java(JavaConfigurer::utilityClassesShouldBeFinalAndHavePrivateConstructor) 36 | .build(); 37 | 38 | assertThrows(AssertionError.class, taikai::check); 39 | } 40 | 41 | @Test 42 | void shouldThrowUtilityClassWithImplicitPublicConstructor() { 43 | Taikai taikai = Taikai.builder() 44 | .classes( 45 | FinalUtilityClassWithImplicitPublicConstructor.class) 46 | .java(JavaConfigurer::utilityClassesShouldBeFinalAndHavePrivateConstructor) 47 | .build(); 48 | 49 | assertThrows(AssertionError.class, taikai::check); 50 | } 51 | 52 | @Test 53 | void shouldThrowUtilityClassWithExplicitPublicConstructor() { 54 | Taikai taikai = Taikai.builder() 55 | .classes( 56 | FinalUtilityClassWithExplicitPublicConstructor.class) 57 | .java(JavaConfigurer::utilityClassesShouldBeFinalAndHavePrivateConstructor) 58 | .build(); 59 | 60 | assertThrows(AssertionError.class, taikai::check); 61 | } 62 | 63 | private static final class ValidUtilityClass { 64 | 65 | public static int number() { 66 | return 42; 67 | } 68 | 69 | private ValidUtilityClass() { 70 | } 71 | } 72 | 73 | private static class NonFinalUtilityClass { 74 | 75 | public static int number() { 76 | return 42; 77 | } 78 | 79 | private NonFinalUtilityClass() { 80 | } 81 | } 82 | 83 | /** 84 | * Class must be declared as public, otherwise the implicit constructor is private. 85 | */ 86 | public static final class FinalUtilityClassWithImplicitPublicConstructor { 87 | 88 | public static int number() { 89 | return 42; 90 | } 91 | } 92 | 93 | private static final class FinalUtilityClassWithExplicitPublicConstructor { 94 | 95 | public static int number() { 96 | return 42; 97 | } 98 | 99 | public FinalUtilityClassWithExplicitPublicConstructor() { 100 | } 101 | } 102 | 103 | private static class ExceptionClassWithFactoryMethod extends RuntimeException { 104 | 105 | public static ExceptionClassWithFactoryMethod failure() { 106 | return new ExceptionClassWithFactoryMethod("Something went wrong."); 107 | } 108 | 109 | public ExceptionClassWithFactoryMethod(String message) { 110 | super(message); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/TaikaiRuleTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertNotNull; 5 | import static org.junit.jupiter.api.Assertions.assertSame; 6 | import static org.junit.jupiter.api.Assertions.assertThrows; 7 | import static org.mockito.Mockito.mock; 8 | import static org.mockito.Mockito.verify; 9 | 10 | import com.enofex.taikai.TaikaiRule.Configuration; 11 | import com.tngtech.archunit.core.domain.JavaClasses; 12 | import com.tngtech.archunit.lang.ArchRule; 13 | import org.junit.jupiter.api.Test; 14 | 15 | class TaikaiRuleTest { 16 | 17 | @Test 18 | void shouldConstructWithArchRuleAndDefaultConfiguration() { 19 | ArchRule archRule = mock(ArchRule.class); 20 | TaikaiRule taikaiRule = TaikaiRule.of(archRule); 21 | 22 | assertNotNull(taikaiRule); 23 | assertEquals(archRule, taikaiRule.archRule()); 24 | assertNotNull(taikaiRule.configuration()); 25 | } 26 | 27 | @Test 28 | void shouldConstructWithArchRuleAndGivenConfiguration() { 29 | ArchRule archRule = mock(ArchRule.class); 30 | Configuration configuration = Configuration.of("com.example"); 31 | TaikaiRule taikaiRule = TaikaiRule.of(archRule, configuration); 32 | 33 | assertNotNull(taikaiRule); 34 | assertEquals(archRule, taikaiRule.archRule()); 35 | assertSame(configuration, taikaiRule.configuration()); 36 | } 37 | 38 | @Test 39 | void shouldThrowNullPointerExceptionWhenConstructedWithNullArchRule() { 40 | assertThrows(NullPointerException.class, () -> TaikaiRule.of(null)); 41 | } 42 | 43 | @Test 44 | void shouldReturnConfigurationNamespace() { 45 | Configuration configuration = Configuration.of("com.example"); 46 | 47 | assertEquals("com.example", configuration.namespace()); 48 | } 49 | 50 | @Test 51 | void shouldReturnConfigurationNamespaceImport() { 52 | Configuration configuration = Configuration.of(Namespace.IMPORT.ONLY_TESTS); 53 | 54 | assertEquals(Namespace.IMPORT.ONLY_TESTS, configuration.namespaceImport()); 55 | } 56 | 57 | @Test 58 | void shouldReturnConfigurationJavaClasses() { 59 | JavaClasses javaClasses = mock(JavaClasses.class); 60 | Configuration configuration = Configuration.of(javaClasses); 61 | 62 | assertEquals(javaClasses, configuration.javaClasses()); 63 | } 64 | 65 | @Test 66 | void shouldCheckUsingGivenJavaClasses() { 67 | JavaClasses javaClasses = mock(JavaClasses.class); 68 | ArchRule archRule = mock(ArchRule.class); 69 | TaikaiRule.of(archRule, Configuration.of(javaClasses)).check(null); 70 | 71 | verify(archRule).check(javaClasses); 72 | } 73 | 74 | @Test 75 | void shouldCheckUsingNamespaceFromConfiguration() { 76 | ArchRule archRule = mock(ArchRule.class); 77 | TaikaiRule.of(archRule, Configuration.of("com.example", Namespace.IMPORT.WITH_TESTS)) 78 | .check(null); 79 | 80 | verify(archRule).check(Namespace.from("com.example", Namespace.IMPORT.WITH_TESTS)); 81 | } 82 | 83 | @Test 84 | void shouldCheckUsingGlobalNamespace() { 85 | ArchRule archRule = mock(ArchRule.class); 86 | TaikaiRule.of(archRule).check("com.example"); 87 | 88 | verify(archRule).check(Namespace.from("com.example", Namespace.IMPORT.WITHOUT_TESTS)); 89 | } 90 | 91 | @Test 92 | void shouldThrowTaikaiExceptionWhenNoNamespaceProvided() { 93 | ArchRule archRule = mock(ArchRule.class); 94 | TaikaiRule taikaiRule = TaikaiRule.of(archRule, Configuration.defaultConfiguration()); 95 | 96 | assertThrows(TaikaiException.class, () -> taikaiRule.check(null)); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/configures/ConfigurersTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.configures; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertEquals; 4 | import static org.junit.jupiter.api.Assertions.assertFalse; 5 | import static org.junit.jupiter.api.Assertions.assertNull; 6 | import static org.junit.jupiter.api.Assertions.assertSame; 7 | import static org.junit.jupiter.api.Assertions.assertThrows; 8 | import static org.junit.jupiter.api.Assertions.assertTrue; 9 | 10 | import com.enofex.taikai.TaikaiRule; 11 | import java.util.Collection; 12 | import java.util.Iterator; 13 | import org.junit.jupiter.api.Test; 14 | 15 | class ConfigurersTest { 16 | 17 | private final Configurers configurers = new Configurers(); 18 | 19 | @Test 20 | void shouldThrowNullPointerExceptionWhenGetOrApplyWithNull() { 21 | assertThrows(NullPointerException.class, () -> this.configurers.getOrApply(null)); 22 | } 23 | 24 | @Test 25 | void shouldGetOrApplyReturnExistingConfigurer() { 26 | TestConfigurer testConfigurer = new TestConfigurer(); 27 | this.configurers.getOrApply(testConfigurer); 28 | TestConfigurer retrievedConfigurer = this.configurers.getOrApply(new TestConfigurer()); 29 | 30 | assertSame(testConfigurer, retrievedConfigurer); 31 | } 32 | 33 | @Test 34 | void shouldGetOrApplyApplyNewConfigurer() { 35 | TestConfigurer testConfigurer = new TestConfigurer(); 36 | TestConfigurer retrievedConfigurer = this.configurers.getOrApply(testConfigurer); 37 | 38 | assertSame(testConfigurer, retrievedConfigurer); 39 | assertEquals(1, this.configurers.all().size()); 40 | } 41 | 42 | @Test 43 | void shouldGetReturnConfigurerByClass() { 44 | TestConfigurer testConfigurer = new TestConfigurer(); 45 | this.configurers.getOrApply(testConfigurer); 46 | TestConfigurer retrievedConfigurer = this.configurers.get(TestConfigurer.class); 47 | 48 | assertSame(testConfigurer, retrievedConfigurer); 49 | } 50 | 51 | @Test 52 | void shouldGetReturnNullForUnknownClass() { 53 | assertNull(this.configurers.get(TestConfigurer.class)); 54 | } 55 | 56 | @Test 57 | void shouldAllReturnAllConfigurers() { 58 | TestConfigurer testConfigurer1 = new TestConfigurer(); 59 | AnotherTestConfigurer testConfigurer2 = new AnotherTestConfigurer(); 60 | this.configurers.getOrApply(testConfigurer1); 61 | this.configurers.getOrApply(testConfigurer2); 62 | 63 | Collection allConfigurers = this.configurers.all(); 64 | 65 | assertEquals(2, allConfigurers.size()); 66 | assertTrue(allConfigurers.contains(testConfigurer1)); 67 | assertTrue(allConfigurers.contains(testConfigurer2)); 68 | } 69 | 70 | @Test 71 | void shouldIteratorIterateOverAllConfigurers() { 72 | TestConfigurer testConfigurer1 = new TestConfigurer(); 73 | AnotherTestConfigurer testConfigurer2 = new AnotherTestConfigurer(); 74 | this.configurers.getOrApply(testConfigurer1); 75 | this.configurers.getOrApply(testConfigurer2); 76 | 77 | Iterator iterator = this.configurers.iterator(); 78 | assertTrue(iterator.hasNext()); 79 | assertSame(testConfigurer1, iterator.next()); 80 | assertSame(testConfigurer2, iterator.next()); 81 | assertFalse(iterator.hasNext()); 82 | } 83 | 84 | private static class TestConfigurer implements Configurer { 85 | 86 | @Override 87 | public Collection rules() { 88 | return null; 89 | } 90 | } 91 | 92 | private static class AnotherTestConfigurer implements Configurer { 93 | 94 | @Override 95 | public Collection rules() { 96 | return null; 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/internal/Modifiers.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.internal; 2 | 3 | import com.tngtech.archunit.core.domain.JavaClass; 4 | import com.tngtech.archunit.core.domain.JavaConstructor; 5 | import com.tngtech.archunit.core.domain.JavaField; 6 | import com.tngtech.archunit.core.domain.JavaMethod; 7 | import com.tngtech.archunit.core.domain.JavaModifier; 8 | 9 | /** 10 | * This class provides utility methods for checking Java modifiers. 11 | *

12 | * This class is intended for internal use only and is not part of the public API. Developers should 13 | * not rely on this class for any public API usage. 14 | */ 15 | public final class Modifiers { 16 | 17 | private Modifiers() { 18 | } 19 | 20 | /** 21 | * Checks if a class is final. 22 | * 23 | * @param javaClass the Java class to check 24 | * @return true if the class is final, false otherwise 25 | */ 26 | public static boolean isClassFinal(JavaClass javaClass) { 27 | return javaClass.getModifiers().contains(JavaModifier.FINAL); 28 | } 29 | 30 | /** 31 | * Checks if a constructor is private. 32 | * 33 | * @param constructor the Java constructor to check 34 | * @return true if the constructor is private, false otherwise 35 | */ 36 | public static boolean isConstructorPrivate(JavaConstructor constructor) { 37 | return constructor.getModifiers().contains(JavaModifier.PRIVATE); 38 | } 39 | 40 | /** 41 | * Checks if a method is protected. 42 | * 43 | * @param method the Java method to check 44 | * @return true if the method is protected, false otherwise 45 | */ 46 | public static boolean isMethodProtected(JavaMethod method) { 47 | return method.getModifiers().contains(JavaModifier.PROTECTED); 48 | } 49 | 50 | /** 51 | * Checks if a method is static. 52 | * 53 | * @param method the Java method to check 54 | * @return true if the method is static, false otherwise 55 | */ 56 | public static boolean isMethodStatic(JavaMethod method) { 57 | return method.getModifiers().contains(JavaModifier.STATIC); 58 | } 59 | 60 | /** 61 | * Checks if a field is static. 62 | * 63 | * @param field the Java field to check 64 | * @return true if the field is static, false otherwise 65 | */ 66 | public static boolean isFieldStatic(JavaField field) { 67 | return field.getModifiers().contains(JavaModifier.STATIC); 68 | } 69 | 70 | /** 71 | * Checks if a field is public. 72 | * 73 | * @param field the Java field to check 74 | * @return true if the field is public, false otherwise 75 | */ 76 | public static boolean isFieldPublic(JavaField field) { 77 | return field.getModifiers().contains(JavaModifier.PUBLIC); 78 | } 79 | 80 | /** 81 | * Checks if a field is protected. 82 | * 83 | * @param field the Java field to check 84 | * @return true if the field is protected, false otherwise 85 | */ 86 | public static boolean isFieldProtected(JavaField field) { 87 | return field.getModifiers().contains(JavaModifier.PROTECTED); 88 | } 89 | 90 | /** 91 | * Checks if a field is final. 92 | * 93 | * @param field the Java field to check 94 | * @return true if the field is final, false otherwise 95 | */ 96 | public static boolean isFieldFinal(JavaField field) { 97 | return field.getModifiers().contains(JavaModifier.FINAL); 98 | } 99 | 100 | /** 101 | * Checks if a field is synthetic. 102 | * 103 | * @param field the Java field to check 104 | * @return true if the field is synthetic, false otherwise 105 | */ 106 | public static boolean isFieldSynthetic(JavaField field) { 107 | return field.getModifiers().contains(JavaModifier.SYNTHETIC); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/java/MethodsAnnotatedWithShouldNotBeAnnotatedWithTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.enofex.taikai.Taikai; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | import org.junit.jupiter.api.Nested; 10 | import org.junit.jupiter.api.Test; 11 | 12 | class MethodsAnnotatedWithShouldNotBeAnnotatedWithTest { 13 | 14 | @Nested 15 | class ClassBasedAPI { 16 | 17 | @Test 18 | void shouldThrowWhenMethodHasBothConflictingAnnotations() { 19 | Taikai taikai = Taikai.builder() 20 | .classes(ConflictingAnnotationsService.class) 21 | .java(java -> java.methodsAnnotatedWithShouldNotBeAnnotatedWith( 22 | PrimaryAnnotation.class, ConflictingAnnotation.class)) 23 | .build(); 24 | 25 | assertThrows(AssertionError.class, taikai::check); 26 | } 27 | 28 | @Test 29 | void shouldNotThrowWhenMethodHasOnlyPrimaryAnnotation() { 30 | Taikai taikai = Taikai.builder() 31 | .classes(SafeAnnotatedService.class) 32 | .java(java -> java.methodsAnnotatedWithShouldNotBeAnnotatedWith( 33 | PrimaryAnnotation.class, ConflictingAnnotation.class)) 34 | .build(); 35 | 36 | assertDoesNotThrow(taikai::check); 37 | } 38 | 39 | @Test 40 | void shouldNotThrowWhenMethodHasOnlyConflictingAnnotation() { 41 | Taikai taikai = Taikai.builder() 42 | .classes(OnlyConflictingService.class) 43 | .java(java -> java.methodsAnnotatedWithShouldNotBeAnnotatedWith( 44 | PrimaryAnnotation.class, ConflictingAnnotation.class)) 45 | .build(); 46 | 47 | assertDoesNotThrow(taikai::check); 48 | } 49 | } 50 | 51 | @Nested 52 | class StringBasedAPI { 53 | 54 | @Test 55 | void shouldThrowWhenMethodHasBothConflictingAnnotations() { 56 | Taikai taikai = Taikai.builder() 57 | .classes(ConflictingAnnotationsService.class) 58 | .java(java -> java.methodsAnnotatedWithShouldNotBeAnnotatedWith( 59 | PrimaryAnnotation.class.getName(), ConflictingAnnotation.class.getName())) 60 | .build(); 61 | 62 | assertThrows(AssertionError.class, taikai::check); 63 | } 64 | 65 | @Test 66 | void shouldNotThrowWhenMethodHasOnlyPrimaryAnnotation() { 67 | Taikai taikai = Taikai.builder() 68 | .classes(SafeAnnotatedService.class) 69 | .java(java -> java.methodsAnnotatedWithShouldNotBeAnnotatedWith( 70 | PrimaryAnnotation.class.getName(), ConflictingAnnotation.class.getName())) 71 | .build(); 72 | 73 | assertDoesNotThrow(taikai::check); 74 | } 75 | 76 | @Test 77 | void shouldNotThrowWhenMethodHasOnlyConflictingAnnotation() { 78 | Taikai taikai = Taikai.builder() 79 | .classes(OnlyConflictingService.class) 80 | .java(java -> java.methodsAnnotatedWithShouldNotBeAnnotatedWith( 81 | PrimaryAnnotation.class.getName(), ConflictingAnnotation.class.getName())) 82 | .build(); 83 | 84 | assertDoesNotThrow(taikai::check); 85 | } 86 | } 87 | 88 | @Retention(RetentionPolicy.RUNTIME) 89 | @interface PrimaryAnnotation { 90 | } 91 | 92 | @Retention(RetentionPolicy.RUNTIME) 93 | @interface ConflictingAnnotation { 94 | } 95 | 96 | static class ConflictingAnnotationsService { 97 | 98 | @PrimaryAnnotation 99 | @ConflictingAnnotation 100 | void doSomething() { 101 | } 102 | } 103 | 104 | static class SafeAnnotatedService { 105 | 106 | @PrimaryAnnotation 107 | void doSomething() { 108 | } 109 | } 110 | 111 | static class OnlyConflictingService { 112 | 113 | @ConflictingAnnotation 114 | void doSomething() { 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/java/NoUsageOfRuleTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.enofex.taikai.Taikai; 7 | import com.tngtech.archunit.core.importer.ClassFileImporter; 8 | import java.util.ArrayList; 9 | import java.util.List; 10 | import org.junit.jupiter.api.Nested; 11 | import org.junit.jupiter.api.Test; 12 | 13 | class NoUsageOfRuleTest { 14 | 15 | @Nested 16 | class NoUsageOfClassBased { 17 | 18 | @Test 19 | void shouldThrowWhenClassUsesForbiddenType() { 20 | Taikai taikai = Taikai.builder() 21 | .classes(UsesArrayList.class) 22 | .java(java -> java.noUsageOf(ArrayList.class)) 23 | .build(); 24 | 25 | assertThrows(AssertionError.class, taikai::check); 26 | } 27 | 28 | @Test 29 | void shouldNotThrowWhenClassDoesNotUseForbiddenType() { 30 | Taikai taikai = Taikai.builder() 31 | .classes(DoesNotUseArrayList.class) 32 | .java(java -> java.noUsageOf(ArrayList.class)) 33 | .build(); 34 | 35 | assertDoesNotThrow(taikai::check); 36 | } 37 | 38 | @Test 39 | void shouldThrowWhenForbiddenTypeUsedWithinPackage() { 40 | Taikai taikai = Taikai.builder() 41 | .classes(new ClassFileImporter().importPackages("com.enofex.taikai.java")) 42 | .java(java -> java.noUsageOf(ArrayList.class, "com.enofex.taikai.java")) 43 | .build(); 44 | 45 | assertThrows(AssertionError.class, taikai::check); 46 | } 47 | 48 | @Test 49 | void shouldNotThrowWhenPackageRestrictionDoesNotMatch() { 50 | Taikai taikai = Taikai.builder() 51 | .classes(new ClassFileImporter().importPackages("com.enofex.taikai.java")) 52 | .java(java -> java.noUsageOf(ArrayList.class, "com.company.project")) 53 | .build(); 54 | 55 | assertDoesNotThrow(taikai::check); 56 | } 57 | } 58 | 59 | @Nested 60 | class NoUsageOfStringBased { 61 | 62 | @Test 63 | void shouldThrowWhenClassUsesForbiddenTypeByName() { 64 | Taikai taikai = Taikai.builder() 65 | .classes(UsesArrayList.class) 66 | .java(java -> java.noUsageOf("java.util.ArrayList")) 67 | .build(); 68 | 69 | assertThrows(AssertionError.class, taikai::check); 70 | } 71 | 72 | @Test 73 | void shouldNotThrowWhenClassDoesNotUseForbiddenTypeByName() { 74 | Taikai taikai = Taikai.builder() 75 | .classes(DoesNotUseArrayList.class) 76 | .java(java -> java.noUsageOf("java.util.ArrayList")) 77 | .build(); 78 | 79 | assertDoesNotThrow(taikai::check); 80 | } 81 | 82 | @Test 83 | void shouldThrowWhenForbiddenTypeUsedWithinPackageByName() { 84 | Taikai taikai = Taikai.builder() 85 | .classes(new ClassFileImporter().importPackages("com.enofex.taikai.java")) 86 | .java(java -> java.noUsageOf("java.util.ArrayList", "com.enofex.taikai.java")) 87 | .build(); 88 | 89 | assertThrows(AssertionError.class, taikai::check); 90 | } 91 | 92 | @Test 93 | void shouldNotThrowWhenPackageRestrictionDoesNotMatchByName() { 94 | Taikai taikai = Taikai.builder() 95 | .classes(new ClassFileImporter().importPackages("com.enofex.taikai.java")) 96 | .java(java -> java.noUsageOf("java.util.ArrayList", "com.company.project")) 97 | .build(); 98 | 99 | assertDoesNotThrow(taikai::check); 100 | } 101 | } 102 | 103 | static class UsesArrayList { 104 | private final List list = new ArrayList<>(); 105 | 106 | void add(String value) { 107 | this.list.add(value); 108 | } 109 | } 110 | 111 | static class DoesNotUseArrayList { 112 | private final String name = "safe"; 113 | 114 | void print() { 115 | System.out.println(this.name); 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/java/MethodsShouldBeAnnotatedWithAllTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.enofex.taikai.Taikai; 7 | import java.lang.annotation.Retention; 8 | import java.lang.annotation.RetentionPolicy; 9 | import java.util.List; 10 | import org.junit.jupiter.api.Nested; 11 | import org.junit.jupiter.api.Test; 12 | 13 | class MethodsShouldBeAnnotatedWithAllTest { 14 | 15 | @Nested 16 | class ClassBasedAPI { 17 | 18 | @Test 19 | void shouldNotThrowWhenMethodHasAllRequiredAnnotations() { 20 | Taikai taikai = Taikai.builder() 21 | .classes(FullyAnnotatedService.class) 22 | .java(java -> java.methodsShouldBeAnnotatedWithAll( 23 | BaseAnnotation.class, List.of(RequiredA.class, RequiredB.class))) 24 | .build(); 25 | 26 | assertDoesNotThrow(taikai::check); 27 | } 28 | 29 | @Test 30 | void shouldThrowWhenMethodIsMissingOneRequiredAnnotation() { 31 | Taikai taikai = Taikai.builder() 32 | .classes(PartiallyAnnotatedService.class) 33 | .java(java -> java.methodsShouldBeAnnotatedWithAll( 34 | BaseAnnotation.class, List.of(RequiredA.class, RequiredB.class))) 35 | .build(); 36 | 37 | assertThrows(AssertionError.class, taikai::check); 38 | } 39 | 40 | @Test 41 | void shouldNotThrowWhenMethodIsNotAnnotatedWithBaseAnnotation() { 42 | Taikai taikai = Taikai.builder() 43 | .classes(NonBaseAnnotatedService.class) 44 | .java(java -> java.methodsShouldBeAnnotatedWithAll( 45 | BaseAnnotation.class, List.of(RequiredA.class, RequiredB.class))) 46 | .build(); 47 | 48 | assertDoesNotThrow(taikai::check); 49 | } 50 | } 51 | 52 | @Nested 53 | class StringBasedAPI { 54 | 55 | @Test 56 | void shouldNotThrowWhenMethodHasAllRequiredAnnotations() { 57 | Taikai taikai = Taikai.builder() 58 | .classes(FullyAnnotatedService.class) 59 | .java(java -> java.methodsShouldBeAnnotatedWithAll( 60 | BaseAnnotation.class.getName(), 61 | List.of(RequiredA.class.getName(), RequiredB.class.getName()))) 62 | .build(); 63 | 64 | assertDoesNotThrow(taikai::check); 65 | } 66 | 67 | @Test 68 | void shouldThrowWhenMethodIsMissingOneRequiredAnnotation() { 69 | Taikai taikai = Taikai.builder() 70 | .classes(PartiallyAnnotatedService.class) 71 | .java(java -> java.methodsShouldBeAnnotatedWithAll( 72 | BaseAnnotation.class.getName(), 73 | List.of(RequiredA.class.getName(), RequiredB.class.getName()))) 74 | .build(); 75 | 76 | assertThrows(AssertionError.class, taikai::check); 77 | } 78 | 79 | @Test 80 | void shouldNotThrowWhenMethodIsNotAnnotatedWithBaseAnnotation() { 81 | Taikai taikai = Taikai.builder() 82 | .classes(NonBaseAnnotatedService.class) 83 | .java(java -> java.methodsShouldBeAnnotatedWithAll( 84 | BaseAnnotation.class.getName(), 85 | List.of(RequiredA.class.getName(), RequiredB.class.getName()))) 86 | .build(); 87 | 88 | assertDoesNotThrow(taikai::check); 89 | } 90 | } 91 | 92 | @Retention(RetentionPolicy.RUNTIME) 93 | @interface BaseAnnotation { 94 | } 95 | 96 | @Retention(RetentionPolicy.RUNTIME) 97 | @interface RequiredA { 98 | } 99 | 100 | @Retention(RetentionPolicy.RUNTIME) 101 | @interface RequiredB { 102 | } 103 | 104 | static class FullyAnnotatedService { 105 | 106 | @BaseAnnotation 107 | @RequiredA 108 | @RequiredB 109 | void process() { 110 | } 111 | } 112 | 113 | static class PartiallyAnnotatedService { 114 | 115 | @BaseAnnotation 116 | @RequiredA 117 | void process() { 118 | } 119 | } 120 | 121 | static class NonBaseAnnotatedService { 122 | 123 | void process() { 124 | // No base annotation, should be ignored 125 | } 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/java/MethodExceptionRuleTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.enofex.taikai.Taikai; 7 | import java.io.IOException; 8 | import org.junit.jupiter.api.Nested; 9 | import org.junit.jupiter.api.Test; 10 | 11 | class MethodExceptionRuleTest { 12 | 13 | @Nested 14 | class MethodsShouldNotDeclareGenericExceptions { 15 | 16 | @Test 17 | void shouldNotThrowWhenNoMethodsDeclareGenericExceptions() { 18 | Taikai taikai = Taikai.builder() 19 | .classes(NoGenericExceptions.class) 20 | .java(JavaConfigurer::methodsShouldNotDeclareGenericExceptions) 21 | .build(); 22 | 23 | assertDoesNotThrow(taikai::check); 24 | } 25 | 26 | @Test 27 | void shouldThrowWhenMethodsDeclareExceptionOrRuntimeException() { 28 | Taikai taikai = Taikai.builder() 29 | .classes(DeclaresGenericExceptions.class) 30 | .java(JavaConfigurer::methodsShouldNotDeclareGenericExceptions) 31 | .build(); 32 | 33 | assertThrows(AssertionError.class, taikai::check); 34 | } 35 | } 36 | 37 | @Nested 38 | class MethodsShouldNotDeclareSpecificException { 39 | 40 | @Test 41 | void shouldNotThrowWhenMethodsDoNotDeclareGivenException_ClassVersion() { 42 | Taikai taikai = Taikai.builder() 43 | .classes(NoIOExceptionDeclared.class) 44 | .java(java -> java.methodsShouldNotDeclareException(".*", IOException.class)) 45 | .build(); 46 | 47 | assertDoesNotThrow(taikai::check); 48 | } 49 | 50 | @Test 51 | void shouldThrowWhenMethodsDeclareGivenException_ClassVersion() { 52 | Taikai taikai = Taikai.builder() 53 | .classes(DeclaresIOException.class) 54 | .java(java -> java.methodsShouldNotDeclareException(".*", IOException.class)) 55 | .build(); 56 | 57 | assertThrows(AssertionError.class, taikai::check); 58 | } 59 | 60 | @Test 61 | void shouldNotThrowWhenMethodsDoNotDeclareGivenException_StringVersion() { 62 | Taikai taikai = Taikai.builder() 63 | .classes(NoIOExceptionDeclared.class) 64 | .java(java -> java.methodsShouldNotDeclareException(".*", IOException.class.getName())) 65 | .build(); 66 | 67 | assertDoesNotThrow(taikai::check); 68 | } 69 | 70 | @Test 71 | void shouldThrowWhenMethodsDeclareGivenException_StringVersion() { 72 | Taikai taikai = Taikai.builder() 73 | .classes(DeclaresIOException.class) 74 | .java(java -> java.methodsShouldNotDeclareException(".*", IOException.class.getName())) 75 | .build(); 76 | 77 | assertThrows(AssertionError.class, taikai::check); 78 | } 79 | 80 | @Test 81 | void shouldNotThrowWhenRegexDoesNotMatchAnyMethod() { 82 | Taikai taikai = Taikai.builder() 83 | .classes(DeclaresIOException.class) 84 | .java(java -> java.methodsShouldNotDeclareException("nonExistingMethod", IOException.class)) 85 | .build(); 86 | 87 | assertDoesNotThrow(taikai::check); 88 | } 89 | } 90 | 91 | static class NoGenericExceptions { 92 | 93 | void safeMethod() { 94 | } 95 | 96 | void anotherSafeMethod() { 97 | } 98 | } 99 | 100 | static class DeclaresGenericExceptions { 101 | 102 | void methodThrowsException() throws Exception { 103 | throw new Exception("generic exception"); 104 | } 105 | 106 | void methodThrowsRuntimeException() throws RuntimeException { 107 | throw new RuntimeException("runtime"); 108 | } 109 | } 110 | 111 | static class DeclaresIOException { 112 | 113 | void riskyOperation() throws IOException { 114 | throw new IOException("I/O error"); 115 | } 116 | 117 | void anotherRiskyOperation() throws IOException { 118 | throw new IOException("Another I/O error"); 119 | } 120 | } 121 | 122 | static class NoIOExceptionDeclared { 123 | 124 | void safeOperation() { 125 | } 126 | 127 | void anotherSafeOperation() { 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/spring/ValidatedController.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.spring; 2 | 3 | import static com.enofex.taikai.spring.SpringDescribedPredicates.ANNOTATION_VALIDATED; 4 | 5 | import com.tngtech.archunit.core.domain.JavaClass; 6 | import com.tngtech.archunit.core.domain.JavaMethod; 7 | import com.tngtech.archunit.core.domain.JavaParameter; 8 | import com.tngtech.archunit.lang.ArchCondition; 9 | import com.tngtech.archunit.lang.ConditionEvents; 10 | import com.tngtech.archunit.lang.SimpleConditionEvent; 11 | 12 | final class ValidatedController { 13 | 14 | private static final String REQUEST_PARAM = "org.springframework.web.bind.annotation.RequestParam"; 15 | private static final String PATH_VARIABLE = "org.springframework.web.bind.annotation.PathVariable"; 16 | 17 | private static final String JAVAX_VALIDATION_NOT_NULL = "javax.validation.constraints.NotNull"; 18 | private static final String JAVAX_VALIDATION_MIN = "javax.validation.constraints.Min"; 19 | private static final String JAVAX_VALIDATION_MAX = "javax.validation.constraints.Max"; 20 | private static final String JAVAX_VALIDATION_SIZE = "javax.validation.constraints.Size"; 21 | private static final String JAVAX_VALIDATION_NOT_BLANK = "javax.validation.constraints.NotBlank"; 22 | private static final String JAVAX_VALIDATION_PATTERN = "javax.validation.constraints.Pattern"; 23 | 24 | private static final String JAKARTA_VALIDATION_NOT_NULL = "jakarta.validation.constraints.NotNull"; 25 | private static final String JAKARTA_VALIDATION_MIN = "jakarta.validation.constraints.Min"; 26 | private static final String JAKARTA_VALIDATION_MAX = "jakarta.validation.constraints.Max"; 27 | private static final String JAKARTA_VALIDATION_SIZE = "jakarta.validation.constraints.Size"; 28 | private static final String JAKARTA_VALIDATION_NOT_BLANK = "jakarta.validation.constraints.NotBlank"; 29 | private static final String JAKARTA_VALIDATION_PATTERN = "jakarta.validation.constraints.Pattern"; 30 | 31 | private ValidatedController() { 32 | } 33 | 34 | static ArchCondition beAnnotatedWithValidated() { 35 | return new ArchCondition<>( 36 | "be annotated with @Validated if @RequestParam or @PathVariable has validation annotations") { 37 | @Override 38 | public void check(JavaClass controllerClass, ConditionEvents events) { 39 | boolean hasValidatedAnnotation = controllerClass.isMetaAnnotatedWith(ANNOTATION_VALIDATED); 40 | 41 | for (JavaMethod method : controllerClass.getMethods()) { 42 | for (JavaParameter parameter : method.getParameters()) { 43 | if ((parameter.isMetaAnnotatedWith(REQUEST_PARAM) 44 | || parameter.isMetaAnnotatedWith(PATH_VARIABLE)) 45 | && (hasJavaXValidationAnnotations(parameter) 46 | || hasJakartaValidationAnnotations(parameter)) 47 | ) { 48 | 49 | if (!hasValidatedAnnotation) { 50 | events.add(SimpleConditionEvent.violated(controllerClass, 51 | "Controller %s is missing @Validated but has a method parameter in %s annotated with @PathVariable or @RequestParam and a validation annotation.".formatted( 52 | controllerClass.getName(), 53 | method.getFullName()))); 54 | } 55 | } 56 | } 57 | } 58 | } 59 | 60 | private boolean hasJavaXValidationAnnotations(JavaParameter parameter) { 61 | return parameter.isMetaAnnotatedWith(JAVAX_VALIDATION_NOT_NULL) 62 | || parameter.isMetaAnnotatedWith(JAVAX_VALIDATION_MIN) 63 | || parameter.isMetaAnnotatedWith(JAVAX_VALIDATION_MAX) 64 | || parameter.isMetaAnnotatedWith(JAVAX_VALIDATION_SIZE) 65 | || parameter.isMetaAnnotatedWith(JAVAX_VALIDATION_NOT_BLANK) 66 | || parameter.isMetaAnnotatedWith(JAVAX_VALIDATION_PATTERN); 67 | } 68 | 69 | private boolean hasJakartaValidationAnnotations(JavaParameter parameter) { 70 | return parameter.isMetaAnnotatedWith(JAKARTA_VALIDATION_NOT_NULL) 71 | || parameter.isMetaAnnotatedWith(JAKARTA_VALIDATION_MIN) 72 | || parameter.isMetaAnnotatedWith(JAKARTA_VALIDATION_MAX) 73 | || parameter.isMetaAnnotatedWith(JAKARTA_VALIDATION_SIZE) 74 | || parameter.isMetaAnnotatedWith(JAKARTA_VALIDATION_NOT_BLANK) 75 | || parameter.isMetaAnnotatedWith(JAKARTA_VALIDATION_PATTERN); 76 | } 77 | }; 78 | } 79 | } 80 | 81 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/spring/ConfigurationsConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.spring; 2 | 3 | import static com.enofex.taikai.TaikaiRule.Configuration.defaultConfiguration; 4 | import static com.enofex.taikai.spring.SpringDescribedPredicates.annotatedWithConfiguration; 5 | import static com.enofex.taikai.spring.SpringDescribedPredicates.annotatedWithSpringBootApplication; 6 | import static com.tngtech.archunit.base.DescribedPredicate.not; 7 | import static com.tngtech.archunit.lang.conditions.ArchPredicates.are; 8 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; 9 | 10 | import com.enofex.taikai.TaikaiRule; 11 | import com.enofex.taikai.TaikaiRule.Configuration; 12 | import com.enofex.taikai.configures.AbstractConfigurer; 13 | import com.enofex.taikai.configures.ConfigurerContext; 14 | import com.enofex.taikai.configures.DisableableConfigurer; 15 | 16 | /** 17 | * Configures and enforces conventions for Spring {@code @Configuration} classes using 18 | * {@link com.tngtech.archunit ArchUnit} through the Taikai framework. 19 | * 20 | *

This configurer ensures that Spring configuration classes have consistent naming 21 | * and do not conflict with {@code @SpringBootApplication} classes.

22 | * 23 | *

Example Usage

24 | *
{@code
 25 |  * Taikai.builder()
 26 |  *     .namespace("com.example.project")
 27 |  *     .spring(spring -> spring
 28 |  *         .configurations(config -> config
 29 |  *             .namesShouldEndWithConfiguration()
 30 |  *         )
 31 |  *     );
 32 |  * }
33 | * 34 | *

By default, this enforces that all classes annotated with {@code @Configuration} 35 | * (but not {@code @SpringBootApplication}) end with the suffix {@code Configuration}.

36 | */ 37 | public final class ConfigurationsConfigurer extends AbstractConfigurer implements 38 | DisableableConfigurer { 39 | 40 | private static final String DEFAULT_CONFIGURATION_NAME_MATCHING = ".+Configuration"; 41 | 42 | public ConfigurationsConfigurer(ConfigurerContext configurerContext) { 43 | super(configurerContext); 44 | } 45 | 46 | /** 47 | * Adds a rule that ensures all Spring {@code @Configuration}-annotated classes (excluding 48 | * {@code @SpringBootApplication} classes) have names ending with {@code Configuration}. 49 | * 50 | * @return this configurer instance for fluent chaining 51 | */ 52 | public ConfigurationsConfigurer namesShouldEndWithConfiguration() { 53 | return namesShouldMatch(DEFAULT_CONFIGURATION_NAME_MATCHING, defaultConfiguration()); 54 | } 55 | 56 | /** 57 | * See {@link #namesShouldEndWithConfiguration()}, but with {@link Configuration} for 58 | * customization. 59 | * 60 | * @param configuration the configuration for rule customization 61 | * @return this configurer instance for fluent chaining 62 | */ 63 | public ConfigurationsConfigurer namesShouldEndWithConfiguration(Configuration configuration) { 64 | return namesShouldMatch(DEFAULT_CONFIGURATION_NAME_MATCHING, configuration); 65 | } 66 | 67 | /** 68 | * Adds a rule that ensures all Spring {@code @Configuration}-annotated classes (excluding 69 | * {@code @SpringBootApplication} classes) have names matching the given regex. 70 | * 71 | * @param regex the regex pattern for valid configuration class names 72 | * @return this configurer instance for fluent chaining 73 | */ 74 | public ConfigurationsConfigurer namesShouldMatch(String regex) { 75 | return namesShouldMatch(regex, defaultConfiguration()); 76 | } 77 | 78 | /** 79 | * See {@link #namesShouldMatch(String)}, but with {@link Configuration} for customization. 80 | * 81 | * @param regex the regex pattern for valid configuration class names 82 | * @param configuration the configuration for rule customization 83 | * @return this configurer instance for fluent chaining 84 | */ 85 | public ConfigurationsConfigurer namesShouldMatch(String regex, Configuration configuration) { 86 | return addRule(TaikaiRule.of(classes() 87 | .that(are(annotatedWithConfiguration(true) 88 | .and(not(annotatedWithSpringBootApplication(true)))) 89 | ) 90 | .should().haveNameMatching(regex) 91 | .as("Configurations should have name ending %s".formatted(regex)), configuration)); 92 | } 93 | 94 | @Override 95 | public ConfigurationsConfigurer disable() { 96 | disable(ConfigurationsConfigurer.class); 97 | 98 | return this; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/Namespace.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai; 2 | 3 | import static java.util.Objects.requireNonNull; 4 | 5 | import com.tngtech.archunit.core.domain.JavaClasses; 6 | import com.tngtech.archunit.core.importer.ClassFileImporter; 7 | import com.tngtech.archunit.core.importer.ImportOption; 8 | import java.util.Map; 9 | import java.util.concurrent.ConcurrentHashMap; 10 | 11 | /** 12 | * Utility class for importing {@link JavaClasses} from a given package namespace 13 | * with different import options. It provides cached access to imported classes 14 | * to improve performance when repeatedly analyzing the same namespace. 15 | * 16 | *

Supported import modes are: 17 | *

    18 | *
  • {@link IMPORT#WITHOUT_TESTS} – imports production classes only
  • 19 | *
  • {@link IMPORT#WITH_TESTS} – imports both production and test classes
  • 20 | *
  • {@link IMPORT#ONLY_TESTS} – imports test classes only
  • 21 | *
22 | */ 23 | public final class Namespace { 24 | 25 | private static final Map JAVA_CLASSES = new ConcurrentHashMap<>(); 26 | 27 | public enum IMPORT { 28 | WITHOUT_TESTS, 29 | WITH_TESTS, 30 | ONLY_TESTS 31 | } 32 | 33 | private Namespace() { 34 | } 35 | 36 | /** 37 | * Imports {@link JavaClasses} from the specified namespace using the given import option. 38 | * 39 | * @param namespace the base package name to import (e.g. {@code "com.example"}) 40 | * @param importOption the import mode defining which classes to include 41 | * @return the imported {@link JavaClasses} 42 | * @throws NullPointerException if {@code namespace} or {@code importOption} is {@code null} 43 | */ 44 | public static JavaClasses from(String namespace, IMPORT importOption) { 45 | requireNonNull(namespace); 46 | requireNonNull(importOption); 47 | 48 | return switch (importOption) { 49 | case WITH_TESTS -> withTests(namespace); 50 | case ONLY_TESTS -> onlyTests(namespace); 51 | default -> withoutTests(namespace); 52 | }; 53 | } 54 | 55 | /** 56 | * Imports all classes from the given namespace, excluding test and JAR classes. 57 | * Results are cached to avoid redundant imports. 58 | * 59 | * @param namespace the base package name to import 60 | * @return the imported {@link JavaClasses} excluding tests 61 | * @throws NullPointerException if {@code namespace} is {@code null} 62 | */ 63 | public static JavaClasses withoutTests(String namespace) { 64 | requireNonNull(namespace); 65 | 66 | return JAVA_CLASSES.computeIfAbsent( 67 | new Key(namespace, IMPORT.WITHOUT_TESTS), 68 | key -> new ClassFileImporter() 69 | .withImportOption(new ImportOption.DoNotIncludeTests()) 70 | .withImportOption(new ImportOption.DoNotIncludeJars()) 71 | .importPackages(namespace) 72 | ); 73 | } 74 | 75 | /** 76 | * Imports all classes from the given namespace, including test classes but excluding JAR classes. 77 | * Results are cached to avoid redundant imports. 78 | * 79 | * @param namespace the base package name to import 80 | * @return the imported {@link JavaClasses} including tests 81 | * @throws NullPointerException if {@code namespace} is {@code null} 82 | */ 83 | public static JavaClasses withTests(String namespace) { 84 | requireNonNull(namespace); 85 | 86 | return JAVA_CLASSES.computeIfAbsent( 87 | new Key(namespace, IMPORT.WITH_TESTS), 88 | key -> new ClassFileImporter() 89 | .withImportOption(new ImportOption.DoNotIncludeJars()) 90 | .importPackages(namespace)); 91 | } 92 | 93 | /** 94 | * Imports only test classes from the given namespace, excluding JAR classes. 95 | * Results are cached to avoid redundant imports. 96 | * 97 | * @param namespace the base package name to import 98 | * @return the imported test {@link JavaClasses} 99 | * @throws NullPointerException if {@code namespace} is {@code null} 100 | */ 101 | public static JavaClasses onlyTests(String namespace) { 102 | requireNonNull(namespace); 103 | 104 | return JAVA_CLASSES.computeIfAbsent( 105 | new Key(namespace, IMPORT.ONLY_TESTS), 106 | key -> new ClassFileImporter() 107 | .withImportOption(new ImportOption.OnlyIncludeTests()) 108 | .withImportOption(new ImportOption.DoNotIncludeJars()) 109 | .importPackages(namespace)); 110 | } 111 | 112 | private record Key(String namespace, IMPORT importOption) { 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/spring/RepositoriesConfigurerTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.spring; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.enofex.taikai.Taikai; 7 | import org.junit.jupiter.api.Nested; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.stereotype.Repository; 10 | import org.springframework.stereotype.Service; 11 | 12 | class RepositoriesConfigurerTest { 13 | 14 | @Nested 15 | class NamesShouldEndWithRepository { 16 | 17 | @Test 18 | void shouldNotThrowWhenRepositoryEndsWithRepository() { 19 | Taikai taikai = Taikai.builder() 20 | .classes(UserRepository.class) 21 | .spring( 22 | spring -> spring.repositories(RepositoriesConfigurer::namesShouldEndWithRepository)) 23 | .build(); 24 | 25 | assertDoesNotThrow(taikai::check); 26 | } 27 | 28 | @Test 29 | void shouldThrowWhenAnnotatedRepositoryDoesNotEndWithRepository() { 30 | Taikai taikai = Taikai.builder() 31 | .classes(InvalidRepoName.class) 32 | .spring( 33 | spring -> spring.repositories(RepositoriesConfigurer::namesShouldEndWithRepository)) 34 | .build(); 35 | 36 | assertThrows(AssertionError.class, taikai::check); 37 | } 38 | 39 | @Test 40 | void shouldNotThrowWhenNotAnnotatedAsRepository() { 41 | Taikai taikai = Taikai.builder() 42 | .classes(UtilityHelper.class) 43 | .spring( 44 | spring -> spring.repositories(RepositoriesConfigurer::namesShouldEndWithRepository)) 45 | .build(); 46 | 47 | assertDoesNotThrow(taikai::check); 48 | } 49 | } 50 | 51 | @Nested 52 | class ShouldBeAnnotatedWithRepository { 53 | 54 | @Test 55 | void shouldNotThrowWhenRepositoryAnnotated() { 56 | Taikai taikai = Taikai.builder() 57 | .classes(UserRepository.class) 58 | .spring(spring -> spring.repositories( 59 | RepositoriesConfigurer::shouldBeAnnotatedWithRepository)) 60 | .build(); 61 | 62 | assertDoesNotThrow(taikai::check); 63 | } 64 | 65 | @Test 66 | void shouldThrowWhenRepositoryMissingAnnotation() { 67 | Taikai taikai = Taikai.builder() 68 | .classes(MissingAnnotationRepository.class) 69 | .spring(spring -> spring.repositories( 70 | RepositoriesConfigurer::shouldBeAnnotatedWithRepository)) 71 | .build(); 72 | 73 | assertThrows(AssertionError.class, taikai::check); 74 | } 75 | 76 | @Test 77 | void shouldNotThrowWhenClassDoesNotMatchRepositoryPattern() { 78 | Taikai taikai = Taikai.builder() 79 | .classes(UtilityHelper.class) 80 | .spring(spring -> spring.repositories( 81 | RepositoriesConfigurer::shouldBeAnnotatedWithRepository)) 82 | .build(); 83 | 84 | assertDoesNotThrow(taikai::check); 85 | } 86 | } 87 | 88 | @Nested 89 | class ShouldNotDependOnServices { 90 | 91 | @Test 92 | void shouldNotThrowWhenRepositoryDoesNotDependOnService() { 93 | Taikai taikai = Taikai.builder() 94 | .classes(UserRepository.class) 95 | .spring(spring -> spring.repositories(RepositoriesConfigurer::shouldNotDependOnServices)) 96 | .build(); 97 | 98 | assertDoesNotThrow(taikai::check); 99 | } 100 | 101 | @Test 102 | void shouldThrowWhenRepositoryDependsOnService() { 103 | Taikai taikai = Taikai.builder() 104 | .classes(RepositoryDependingOnService.class, 105 | UserService.class) 106 | .spring(spring -> spring.repositories(RepositoriesConfigurer::shouldNotDependOnServices)) 107 | .build(); 108 | 109 | assertThrows(AssertionError.class, taikai::check); 110 | } 111 | } 112 | 113 | @Repository 114 | static class UserRepository { 115 | // valid repository 116 | } 117 | 118 | @Repository 119 | static class InvalidRepoName { 120 | 121 | } 122 | 123 | static class MissingAnnotationRepository { 124 | 125 | } 126 | 127 | static class UtilityHelper { 128 | 129 | } 130 | 131 | @Service 132 | static class UserService { 133 | 134 | } 135 | 136 | @Repository 137 | static class RepositoryDependingOnService { 138 | 139 | private final UserService userService; 140 | 141 | RepositoryDependingOnService(UserService userService) { 142 | this.userService = userService; 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/main/java/com/enofex/taikai/spring/BootConfigurer.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.spring; 2 | 3 | import static com.enofex.taikai.TaikaiRule.Configuration.defaultConfiguration; 4 | import static com.enofex.taikai.spring.SpringDescribedPredicates.ANNOTATION_SPRING_BOOT_APPLICATION; 5 | import static com.enofex.taikai.spring.SpringDescribedPredicates.annotatedWithSpringBootApplication; 6 | import static com.tngtech.archunit.lang.conditions.ArchPredicates.are; 7 | import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.classes; 8 | import static java.util.Objects.requireNonNull; 9 | 10 | import com.enofex.taikai.TaikaiRule; 11 | import com.enofex.taikai.TaikaiRule.Configuration; 12 | import com.enofex.taikai.configures.AbstractConfigurer; 13 | import com.enofex.taikai.configures.ConfigurerContext; 14 | import com.enofex.taikai.configures.DisableableConfigurer; 15 | 16 | /** 17 | * Configures and enforces conventions for Spring Boot application classes using 18 | * {@link com.tngtech.archunit ArchUnit} through the Taikai framework. 19 | * 20 | *

This configurer ensures that the class annotated with {@code @SpringBootApplication} 21 | * resides in the correct package, maintaining a clear and predictable project structure.

22 | * 23 | *

Example Usage

24 | *
{@code
 25 |  * Taikai.builder()
 26 |  *     .namespace("com.example.project")
 27 |  *     .spring(spring -> spring
 28 |  *         .boot(boot -> boot
 29 |  *             .applicationClassShouldResideInPackage("com.example.project")
 30 |  *         )
 31 |  *     );
 32 |  * }
33 | * 34 | *

By default, this rule ensures that your {@code @SpringBootApplication}-annotated class 35 | * is located in the defined root package or a designated boot package.

36 | */ 37 | public final class BootConfigurer extends AbstractConfigurer implements DisableableConfigurer { 38 | 39 | public BootConfigurer(ConfigurerContext configurerContext) { 40 | super(configurerContext); 41 | } 42 | 43 | /** 44 | * Adds a rule that classes annotated with {@code @SpringBootApplication} must reside in the 45 | * project's root package (i.e., the namespace configured via 46 | * {@link com.enofex.taikai.Taikai.Builder#namespace(String)}). 47 | * 48 | *

This variant uses the namespace provided in the Taikai configuration 49 | * and applies the rule without requiring an explicit package argument.

50 | * 51 | * @return this configurer instance for fluent chaining 52 | * @throws NullPointerException if no namespace is configured 53 | */ 54 | public BootConfigurer applicationClassShouldResideInPackage() { 55 | String namespace = configurerContext().namespace(); 56 | requireNonNull(namespace, "Namespace must not be null"); 57 | 58 | return applicationClassShouldResideInPackage(namespace, defaultConfiguration()); 59 | } 60 | 61 | /** 62 | * @deprecated Use {@link #applicationClassShouldResideInPackage(String)} instead. 63 | */ 64 | @Deprecated(forRemoval = true) 65 | public BootConfigurer springBootApplicationShouldBeIn(String packageIdentifier) { 66 | return applicationClassShouldResideInPackage(packageIdentifier); 67 | } 68 | 69 | /** 70 | * @deprecated Use {@link #applicationClassShouldResideInPackage(String, Configuration)} 71 | * instead. 72 | */ 73 | @Deprecated(forRemoval = true) 74 | public BootConfigurer springBootApplicationShouldBeIn(String packageIdentifier, 75 | Configuration configuration) { 76 | return applicationClassShouldResideInPackage(packageIdentifier, configuration); 77 | } 78 | 79 | /** 80 | * Adds a rule that classes annotated with {@code @SpringBootApplication} should reside in the 81 | * specified package. 82 | * 83 | * @param packageIdentifier the target package identifier (e.g., {@code "com.example.project"}) 84 | * @return this configurer instance for fluent chaining 85 | */ 86 | public BootConfigurer applicationClassShouldResideInPackage(String packageIdentifier) { 87 | requireNonNull(packageIdentifier); 88 | return applicationClassShouldResideInPackage(packageIdentifier, defaultConfiguration()); 89 | } 90 | 91 | /** 92 | * See {@link #applicationClassShouldResideInPackage(String)}, but with {@link Configuration} 93 | * for customization. 94 | */ 95 | public BootConfigurer applicationClassShouldResideInPackage(String packageIdentifier, 96 | Configuration configuration) { 97 | return addRule(TaikaiRule.of(classes() 98 | .that(are(annotatedWithSpringBootApplication(true))) 99 | .should().resideInAPackage(packageIdentifier) 100 | .allowEmptyShould(false) 101 | .as("Classes annotated with %s should reside in package %s".formatted( 102 | ANNOTATION_SPRING_BOOT_APPLICATION, packageIdentifier)), configuration)); 103 | } 104 | 105 | 106 | @Override 107 | public BootConfigurer disable() { 108 | disable(BootConfigurer.class); 109 | 110 | return this; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/spring/ServicesConfigurerTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.spring; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.enofex.taikai.Taikai; 7 | import org.junit.jupiter.api.Nested; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.stereotype.Controller; 10 | import org.springframework.stereotype.Service; 11 | import org.springframework.web.bind.annotation.RestController; 12 | 13 | class ServicesConfigurerTest { 14 | 15 | @Nested 16 | class NamesShouldEndWithService { 17 | 18 | @Test 19 | void shouldNotThrowWhenServiceEndsWithService() { 20 | Taikai taikai = Taikai.builder() 21 | .classes(UserService.class) 22 | .spring(spring -> spring.services(ServicesConfigurer::namesShouldEndWithService)) 23 | .build(); 24 | 25 | assertDoesNotThrow(taikai::check); 26 | } 27 | 28 | @Test 29 | void shouldThrowWhenAnnotatedServiceDoesNotEndWithService() { 30 | Taikai taikai = Taikai.builder() 31 | .classes(InvalidNaming.class) 32 | .spring(spring -> spring.services(ServicesConfigurer::namesShouldEndWithService)) 33 | .build(); 34 | 35 | assertThrows(AssertionError.class, taikai::check); 36 | } 37 | 38 | @Test 39 | void shouldNotThrowWhenClassIsNotAnnotatedWithService() { 40 | Taikai taikai = Taikai.builder() 41 | .classes(UtilityHelper.class) 42 | .spring(spring -> spring.services(ServicesConfigurer::namesShouldEndWithService)) 43 | .build(); 44 | 45 | assertDoesNotThrow(taikai::check); 46 | } 47 | } 48 | 49 | @Nested 50 | class ShouldBeAnnotatedWithService { 51 | 52 | @Test 53 | void shouldNotThrowWhenAnnotatedWithService() { 54 | Taikai taikai = Taikai.builder() 55 | .classes(UserService.class) 56 | .spring(spring -> spring.services(ServicesConfigurer::shouldBeAnnotatedWithService)) 57 | .build(); 58 | 59 | assertDoesNotThrow(taikai::check); 60 | } 61 | 62 | @Test 63 | void shouldThrowWhenServiceMissingAnnotation() { 64 | Taikai taikai = Taikai.builder() 65 | .classes(MissingAnnotationService.class) 66 | .spring(spring -> spring.services(ServicesConfigurer::shouldBeAnnotatedWithService)) 67 | .build(); 68 | 69 | assertThrows(AssertionError.class, taikai::check); 70 | } 71 | 72 | @Test 73 | void shouldNotThrowWhenClassDoesNotMatchServicePattern() { 74 | Taikai taikai = Taikai.builder() 75 | .classes(UtilityHelper.class) 76 | .spring(spring -> spring.services(ServicesConfigurer::shouldBeAnnotatedWithService)) 77 | .build(); 78 | 79 | assertDoesNotThrow(taikai::check); 80 | } 81 | } 82 | 83 | @Nested 84 | class ShouldNotDependOnControllers { 85 | 86 | @Test 87 | void shouldNotThrowWhenServiceDoesNotDependOnController() { 88 | Taikai taikai = Taikai.builder() 89 | .classes(UserService.class) 90 | .spring(spring -> spring.services(ServicesConfigurer::shouldNotDependOnControllers)) 91 | .build(); 92 | 93 | assertDoesNotThrow(taikai::check); 94 | } 95 | 96 | @Test 97 | void shouldThrowWhenServiceDependsOnController() { 98 | Taikai taikai = Taikai.builder() 99 | .classes(ServiceDependingOnController.class, 100 | UserController.class) 101 | .spring(spring -> spring.services(ServicesConfigurer::shouldNotDependOnControllers)) 102 | .build(); 103 | 104 | assertThrows(AssertionError.class, taikai::check); 105 | } 106 | 107 | @Test 108 | void shouldThrowWhenServiceDependsOnRestController() { 109 | Taikai taikai = Taikai.builder() 110 | .classes(ServiceDependingOnRestController.class, 111 | ApiController.class) 112 | .spring(spring -> spring.services(ServicesConfigurer::shouldNotDependOnControllers)) 113 | .build(); 114 | 115 | assertThrows(AssertionError.class, taikai::check); 116 | } 117 | } 118 | 119 | @Service 120 | static class UserService { 121 | // valid service 122 | } 123 | 124 | @Service 125 | static class InvalidNaming { 126 | 127 | } 128 | 129 | static class MissingAnnotationService { 130 | 131 | } 132 | 133 | static class UtilityHelper { 134 | 135 | } 136 | 137 | @Controller 138 | static class UserController { 139 | 140 | } 141 | 142 | @RestController 143 | static class ApiController { 144 | 145 | } 146 | 147 | @Service 148 | static class ServiceDependingOnController { 149 | 150 | private final UserController controller; 151 | 152 | ServiceDependingOnController(UserController controller) { 153 | this.controller = controller; 154 | } 155 | } 156 | 157 | @Service 158 | static class ServiceDependingOnRestController { 159 | 160 | private final ApiController apiController; 161 | 162 | ServiceDependingOnRestController(ApiController apiController) { 163 | this.apiController = apiController; 164 | } 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/internal/ModifiersTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.internal; 2 | 3 | 4 | import static org.junit.jupiter.api.Assertions.assertFalse; 5 | import static org.junit.jupiter.api.Assertions.assertTrue; 6 | import static org.mockito.Mockito.when; 7 | 8 | import com.tngtech.archunit.core.domain.JavaClass; 9 | import com.tngtech.archunit.core.domain.JavaConstructor; 10 | import com.tngtech.archunit.core.domain.JavaField; 11 | import com.tngtech.archunit.core.domain.JavaMethod; 12 | import com.tngtech.archunit.core.domain.JavaModifier; 13 | import java.util.Collections; 14 | import java.util.Set; 15 | import org.junit.jupiter.api.Test; 16 | import org.junit.jupiter.api.extension.ExtendWith; 17 | import org.mockito.Mock; 18 | import org.mockito.junit.jupiter.MockitoExtension; 19 | 20 | @ExtendWith(MockitoExtension.class) 21 | class ModifiersTest { 22 | 23 | @Mock 24 | private JavaClass javaClass; 25 | @Mock 26 | private JavaConstructor constructor; 27 | @Mock 28 | private JavaField field; 29 | @Mock 30 | private JavaMethod method; 31 | 32 | 33 | @Test 34 | void shouldReturnTrueWhenClassIsFinal() { 35 | when(this.javaClass.getModifiers()).thenReturn(Set.of(JavaModifier.FINAL)); 36 | 37 | assertTrue(Modifiers.isClassFinal(this.javaClass)); 38 | } 39 | 40 | @Test 41 | void shouldReturnFalseWhenClassIsNotFinal() { 42 | when(this.javaClass.getModifiers()).thenReturn(Collections.emptySet()); 43 | 44 | assertFalse(Modifiers.isClassFinal(this.javaClass)); 45 | } 46 | 47 | @Test 48 | void shouldReturnTrueWhenConstructorIsPrivate() { 49 | when(this.constructor.getModifiers()).thenReturn(Set.of(JavaModifier.PRIVATE)); 50 | 51 | assertTrue(Modifiers.isConstructorPrivate(this.constructor)); 52 | } 53 | 54 | @Test 55 | void shouldReturnFalseWhenConstructorIsNotPrivate() { 56 | when(this.constructor.getModifiers()).thenReturn(Collections.emptySet()); 57 | 58 | assertFalse(Modifiers.isConstructorPrivate(this.constructor)); 59 | } 60 | 61 | @Test 62 | void shouldReturnTrueWhenMethodIsProtected() { 63 | when(this.method.getModifiers()).thenReturn(Set.of(JavaModifier.PROTECTED)); 64 | 65 | assertTrue(Modifiers.isMethodProtected(this.method)); 66 | } 67 | 68 | @Test 69 | void shouldReturnFalseWhenMethodIsNotProtected() { 70 | when(this.method.getModifiers()).thenReturn(Collections.emptySet()); 71 | 72 | assertFalse(Modifiers.isMethodProtected(this.method)); 73 | } 74 | 75 | @Test 76 | void shouldReturnTrueWhenMethodIsStatic() { 77 | when(this.method.getModifiers()).thenReturn(Set.of(JavaModifier.STATIC)); 78 | 79 | assertTrue(Modifiers.isMethodStatic(this.method)); 80 | } 81 | 82 | @Test 83 | void shouldReturnFalseWhenMethodIsNotStatic() { 84 | when(this.method.getModifiers()).thenReturn(Collections.emptySet()); 85 | 86 | assertFalse(Modifiers.isMethodStatic(this.method)); 87 | } 88 | 89 | @Test 90 | void shouldReturnTrueWhenFieldIsStatic() { 91 | when(this.field.getModifiers()).thenReturn(Set.of(JavaModifier.STATIC)); 92 | 93 | assertTrue(Modifiers.isFieldStatic(this.field)); 94 | } 95 | 96 | @Test 97 | void shouldReturnFalseWhenFieldIsNotStatic() { 98 | when(this.field.getModifiers()).thenReturn(Collections.emptySet()); 99 | 100 | assertFalse(Modifiers.isFieldStatic(this.field)); 101 | } 102 | 103 | @Test 104 | void shouldReturnTrueWhenFieldIsPublic() { 105 | when(this.field.getModifiers()).thenReturn(Set.of(JavaModifier.PUBLIC)); 106 | 107 | assertTrue(Modifiers.isFieldPublic(this.field)); 108 | } 109 | 110 | @Test 111 | void shouldReturnFalseWhenFieldIsNotPublic() { 112 | when(this.field.getModifiers()).thenReturn(Collections.emptySet()); 113 | 114 | assertFalse(Modifiers.isFieldPublic(this.field)); 115 | } 116 | 117 | @Test 118 | void shouldReturnTrueWhenFieldIsProtected() { 119 | when(this.field.getModifiers()).thenReturn(Set.of(JavaModifier.PROTECTED)); 120 | 121 | assertTrue(Modifiers.isFieldProtected(this.field)); 122 | } 123 | 124 | @Test 125 | void shouldReturnFalseWhenFieldIsNotProtected() { 126 | when(this.field.getModifiers()).thenReturn(Collections.emptySet()); 127 | 128 | assertFalse(Modifiers.isFieldProtected(this.field)); 129 | } 130 | 131 | @Test 132 | void shouldReturnTrueWhenFieldIsFinal() { 133 | when(this.field.getModifiers()).thenReturn(Set.of(JavaModifier.FINAL)); 134 | 135 | assertTrue(Modifiers.isFieldFinal(this.field)); 136 | } 137 | 138 | @Test 139 | void shouldReturnFalseWhenFieldIsNotFinal() { 140 | when(this.field.getModifiers()).thenReturn(Collections.emptySet()); 141 | 142 | assertFalse(Modifiers.isFieldFinal(this.field)); 143 | } 144 | 145 | @Test 146 | void shouldReturnTrueWhenFieldIsSynthetic() { 147 | when(this.field.getModifiers()).thenReturn(Set.of(JavaModifier.SYNTHETIC)); 148 | 149 | assertTrue(Modifiers.isFieldSynthetic(this.field)); 150 | } 151 | 152 | @Test 153 | void shouldReturnFalseWhenFieldIsNotSynthetic() { 154 | when(this.field.getModifiers()).thenReturn(Collections.emptySet()); 155 | 156 | assertFalse(Modifiers.isFieldSynthetic(this.field)); 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 4 |

5 | 6 |

7 | 8 | 9 | 10 |

11 | 12 | # Taikai 13 | 14 | Taikai extends the capabilities of the popular ArchUnit library by offering a comprehensive suite of predefined rules tailored for various technologies. It simplifies the enforcement of architectural constraints and best practices in your codebase, ensuring consistency and quality across your projects. 15 | 16 | ## Maven Usage 17 | 18 | Add Taikai as a dependency in your `pom.xml`: 19 | 20 | ```xml 21 | 22 | com.enofex 23 | taikai 24 | ${taikai.version} 25 | test 26 | 27 | ``` 28 | 29 | Replace `${taikai.version}` with the appropriate version defined in your project. Ensure that the required dependencies like ArchUnit are already declared. 30 | 31 | ## Gradle Usage 32 | 33 | Add Taikai as a dependency in your `build.gradle` file: 34 | 35 | ```groovy 36 | testImplementation "com.enofex:taikai:${taikaiVersion}" 37 | ``` 38 | 39 | Replace `${taikaiVersion}` with the appropriate version defined in your project. Ensure that the required dependencies like ArchUnit are already declared. 40 | 41 | ## JUnit Example Test 42 | 43 | Here's an example demonstrating the usage of some Taikai rules with JUnit. Customize rules as needed using `TaikaiRule.of()`. 44 | 45 | ```java 46 | @Test 47 | void shouldFulfillConstraints() { 48 | Taikai.builder() 49 | .namespace("com.enofex.taikai") 50 | .java(java -> java 51 | .noUsageOfDeprecatedAPIs() 52 | .methodsShouldNotDeclareGenericExceptions() 53 | .utilityClassesShouldBeFinalAndHavePrivateConstructor() 54 | .imports(imports -> imports 55 | .shouldHaveNoCycles() 56 | .shouldNotImport("..internal..") 57 | .shouldNotImport(junit4())) 58 | .naming(naming -> naming 59 | .classesShouldNotMatch(".*Impl") 60 | .methodsShouldNotMatch("^(foo$|bar$).*") 61 | .fieldsShouldNotMatch(".*(List|Set|Map)$") 62 | .fieldsShouldMatch("com.enofex.taikai.Matcher", "matcher") 63 | .constantsShouldFollowConventions() 64 | .interfacesShouldNotHavePrefixI())) 65 | .logging(logging -> logging 66 | .loggersShouldFollowConventions(Logger.class, "logger", List.of(PRIVATE, FINAL))) 67 | .test(test -> test 68 | .junit(junit -> junit 69 | .classesShouldNotBeAnnotatedWithDisabled() 70 | .methodsShouldNotBeAnnotatedWithDisabled())) 71 | .spring(spring -> spring 72 | .noAutowiredFields() 73 | .boot(boot -> boot 74 | .applicationClassShouldResideInPackage("com.enofex.taikai")) 75 | .configurations(configuration -> configuration 76 | .namesShouldEndWithConfiguration()) 77 | .controllers(controllers -> controllers 78 | .shouldBeAnnotatedWithRestController() 79 | .namesShouldEndWithController() 80 | .shouldNotDependOnOtherControllers() 81 | .shouldBePackagePrivate()) 82 | .services(services -> services 83 | .shouldBeAnnotatedWithService() 84 | .shouldNotDependOnControllers() 85 | .namesShouldEndWithService()) 86 | .repositories(repositories -> repositories 87 | .shouldBeAnnotatedWithRepository() 88 | .shouldNotDependOnServices() 89 | .namesShouldEndWithRepository())) 90 | .addRule(TaikaiRule.of(...)) // Add custom ArchUnit rule here 91 | .build() 92 | .checkAll(); 93 | } 94 | ``` 95 | 96 | ## User Guide 97 | 98 | Explore the complete [documentation](https://enofex.github.io/taikai) for comprehensive information on all available rules. 99 | 100 | ## Contributing 101 | 102 | Interested in contributing? Check out our [Contribution Guidelines](https://github.com/enofex/taikai/blob/main/CONTRIBUTING.md) for details on how to get involved. Note, that we expect everyone to follow the [Code of Conduct](https://github.com/enofex/taikai/blob/main/CODE_OF_CONDUCT.md). 103 | 104 | ### What you will need 105 | 106 | * Git 107 | * Java 17 or higher 108 | 109 | ### Get the Source Code 110 | 111 | Clone the repository 112 | 113 | ```shell 114 | git clone git@github.com:enofex/taikai.git 115 | cd taikai 116 | ``` 117 | 118 | ### Build the code 119 | 120 | To compile, test, and build 121 | 122 | ```shell 123 | ./mvnw clean package -B 124 | ``` 125 | 126 | ## Backers 127 | 128 | The Open Source Community 129 | 130 |
131 | 132 | 133 | 134 |
135 | 136 | ## Website 137 | 138 | Visit the [Taikai](https://enofex.github.io/taikai/) Website for general information and documentation. 139 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/java/FieldModifierTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.java; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.enofex.taikai.Taikai; 7 | import com.tngtech.archunit.core.domain.JavaModifier; 8 | import java.lang.annotation.Retention; 9 | import java.lang.annotation.RetentionPolicy; 10 | import java.util.EnumSet; 11 | import java.util.List; 12 | import java.util.Set; 13 | import org.junit.jupiter.api.Nested; 14 | import org.junit.jupiter.api.Test; 15 | 16 | class FieldModifierTest { 17 | 18 | @Nested 19 | class FieldsAnnotatedWithShouldHaveModifiers { 20 | 21 | @Test 22 | void shouldNotThrowWhenAllAnnotatedFieldsHaveRequiredModifiers_ClassVersion() { 23 | Taikai taikai = Taikai.builder() 24 | .classes(PublicFinalFields.class) 25 | .java(java -> java.fieldsAnnotatedWithShouldHaveModifiers( 26 | TestAnnotation.class, List.of(JavaModifier.PUBLIC, JavaModifier.FINAL))) 27 | .build(); 28 | 29 | assertDoesNotThrow(taikai::check); 30 | } 31 | 32 | @Test 33 | void shouldThrowWhenAnnotatedFieldsLackRequiredModifiers_ClassVersion() { 34 | Taikai taikai = Taikai.builder() 35 | .classes(NonPublicFields.class) 36 | .java(java -> java.fieldsAnnotatedWithShouldHaveModifiers( 37 | TestAnnotation.class, List.of(JavaModifier.PUBLIC))) 38 | .build(); 39 | 40 | assertThrows(AssertionError.class, taikai::check); 41 | } 42 | 43 | @Test 44 | void shouldNotThrowWhenAllAnnotatedFieldsHaveRequiredModifiers_StringVersion() { 45 | Taikai taikai = Taikai.builder() 46 | .classes(PublicFinalFields.class) 47 | .java(java -> java.fieldsAnnotatedWithShouldHaveModifiers( 48 | TestAnnotation.class.getName(), List.of(JavaModifier.PUBLIC, JavaModifier.FINAL))) 49 | .build(); 50 | 51 | assertDoesNotThrow(taikai::check); 52 | } 53 | 54 | @Test 55 | void shouldThrowWhenAnnotatedFieldsLackRequiredModifiers_StringVersion() { 56 | Taikai taikai = Taikai.builder() 57 | .classes(NonPublicFields.class) 58 | .java(java -> java.fieldsAnnotatedWithShouldHaveModifiers( 59 | TestAnnotation.class.getName(), List.of(JavaModifier.PUBLIC))) 60 | .build(); 61 | 62 | assertThrows(AssertionError.class, taikai::check); 63 | } 64 | } 65 | 66 | @Nested 67 | class FieldsAnnotatedWithShouldNotHaveModifiers { 68 | 69 | @Test 70 | void shouldNotThrowWhenAnnotatedFieldsDoNotHaveForbiddenModifiers_ClassVersion() { 71 | Taikai taikai = Taikai.builder() 72 | .classes(NonStaticFields.class) 73 | .java(java -> java.fieldsAnnotatedWithShouldNotHaveModifiers( 74 | TestAnnotation.class, List.of(JavaModifier.STATIC))) 75 | .build(); 76 | 77 | assertDoesNotThrow(taikai::check); 78 | } 79 | 80 | @Test 81 | void shouldThrowWhenAnnotatedFieldsHaveForbiddenModifiers_ClassVersion() { 82 | Taikai taikai = Taikai.builder() 83 | .classes(StaticFields.class) 84 | .java(java -> java.fieldsAnnotatedWithShouldNotHaveModifiers( 85 | TestAnnotation.class, List.of(JavaModifier.STATIC))) 86 | .build(); 87 | 88 | assertThrows(AssertionError.class, taikai::check); 89 | } 90 | 91 | @Test 92 | void shouldNotThrowWhenAnnotatedFieldsDoNotHaveForbiddenModifiers_StringVersion() { 93 | Taikai taikai = Taikai.builder() 94 | .classes(NonStaticFields.class) 95 | .java(java -> java.fieldsAnnotatedWithShouldNotHaveModifiers( 96 | TestAnnotation.class.getName(), List.of(JavaModifier.STATIC))) 97 | .build(); 98 | 99 | assertDoesNotThrow(taikai::check); 100 | } 101 | 102 | @Test 103 | void shouldThrowWhenAnnotatedFieldsHaveForbiddenModifiers_StringVersion() { 104 | Taikai taikai = Taikai.builder() 105 | .classes(StaticFields.class) 106 | .java(java -> java.fieldsAnnotatedWithShouldNotHaveModifiers( 107 | TestAnnotation.class.getName(), List.of(JavaModifier.STATIC))) 108 | .build(); 109 | 110 | assertThrows(AssertionError.class, taikai::check); 111 | } 112 | } 113 | 114 | @Test 115 | void shouldSupportEmptyModifierCollections() { 116 | Taikai taikai = Taikai.builder() 117 | .classes(PublicFinalFields.class) 118 | .java(java -> java.fieldsAnnotatedWithShouldHaveModifiers( 119 | TestAnnotation.class, Set.of())) 120 | .build(); 121 | 122 | assertDoesNotThrow(taikai::check); 123 | } 124 | 125 | @Retention(RetentionPolicy.RUNTIME) 126 | @interface TestAnnotation { } 127 | 128 | static class PublicFinalFields { 129 | @TestAnnotation 130 | public final int id = 1; 131 | } 132 | 133 | static class NonPublicFields { 134 | @TestAnnotation 135 | int value = 0; // missing PUBLIC, FINAL 136 | } 137 | 138 | static class StaticFields { 139 | @TestAnnotation 140 | public static int counter = 0; 141 | } 142 | 143 | static class NonStaticFields { 144 | @TestAnnotation 145 | public int counter = 0; 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | info@enofex.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/TaikaiTest.java: -------------------------------------------------------------------------------- 1 | 2 | package com.enofex.taikai; 3 | 4 | import static java.util.Collections.emptyList; 5 | import static org.junit.jupiter.api.Assertions.assertEquals; 6 | import static org.junit.jupiter.api.Assertions.assertFalse; 7 | import static org.junit.jupiter.api.Assertions.assertNotNull; 8 | import static org.junit.jupiter.api.Assertions.assertNull; 9 | import static org.junit.jupiter.api.Assertions.assertThrows; 10 | import static org.junit.jupiter.api.Assertions.assertTrue; 11 | import static org.mockito.Mockito.any; 12 | import static org.mockito.Mockito.mock; 13 | import static org.mockito.Mockito.times; 14 | import static org.mockito.Mockito.verify; 15 | 16 | import com.enofex.taikai.configures.Customizer; 17 | import com.enofex.taikai.java.JavaConfigurer; 18 | import com.enofex.taikai.spring.SpringConfigurer; 19 | import com.enofex.taikai.test.TestConfigurer; 20 | import com.tngtech.archunit.ArchConfiguration; 21 | import java.util.Collection; 22 | import java.util.Collections; 23 | import org.junit.jupiter.api.BeforeEach; 24 | import org.junit.jupiter.api.Test; 25 | 26 | class TaikaiTest { 27 | 28 | private static final String VALID_NAMESPACE = "com.enofex.taikai"; 29 | 30 | @BeforeEach 31 | void setUp() { 32 | ArchConfiguration.get().reset(); 33 | } 34 | 35 | @Test 36 | void shouldBuildTaikaiWithDefaultValues() { 37 | Taikai taikai = Taikai.builder() 38 | .namespace(VALID_NAMESPACE) 39 | .build(); 40 | 41 | assertFalse(taikai.failOnEmpty()); 42 | assertEquals(VALID_NAMESPACE, taikai.namespace()); 43 | assertNull(taikai.classes()); 44 | assertTrue(taikai.rules().isEmpty()); 45 | assertTrue(taikai.excludedClasses().isEmpty()); 46 | } 47 | 48 | @Test 49 | void shouldBuildTaikaiWithCustomValues() { 50 | TaikaiRule mockRule = mock(TaikaiRule.class); 51 | Collection rules = Collections.singletonList(mockRule); 52 | 53 | Taikai taikai = Taikai.builder() 54 | .classes(TaikaiTest.class) 55 | .excludeClasses("com.enofex.taikai.foo.ClassToExclude", "com.enofex.taikai.bar.ClassToExclude") 56 | .failOnEmpty(true) 57 | .addRules(rules) 58 | .build(); 59 | 60 | assertTrue(taikai.failOnEmpty()); 61 | assertNull(taikai.namespace()); 62 | assertNotNull(taikai.classes()); 63 | assertEquals(1, taikai.rules().size()); 64 | assertTrue(taikai.rules().contains(mockRule)); 65 | assertEquals(2, taikai.excludedClasses().size()); 66 | } 67 | 68 | @Test 69 | void shouldAddSingleRule() { 70 | TaikaiRule mockRule = mock(TaikaiRule.class); 71 | 72 | Taikai taikai = Taikai.builder() 73 | .namespace(VALID_NAMESPACE) 74 | .addRule(mockRule) 75 | .build(); 76 | 77 | assertEquals(1, taikai.rules().size()); 78 | assertTrue(taikai.rules().contains(mockRule)); 79 | } 80 | 81 | @Test 82 | void shouldConfigureJavaCustomizer() { 83 | Customizer customizer = mock(Customizer.class); 84 | 85 | Taikai.builder() 86 | .namespace(VALID_NAMESPACE) 87 | .java(customizer) 88 | .build(); 89 | 90 | verify(customizer, times(1)).customize(any(JavaConfigurer.class)); 91 | } 92 | 93 | @Test 94 | void shouldConfigureSpringCustomizer() { 95 | Customizer customizer = mock(Customizer.class); 96 | 97 | Taikai.builder() 98 | .namespace(VALID_NAMESPACE) 99 | .spring(customizer) 100 | .build(); 101 | 102 | verify(customizer, times(1)).customize(any(SpringConfigurer.class)); 103 | } 104 | 105 | @Test 106 | void shouldConfigureTestCustomizer() { 107 | Customizer customizer = mock(Customizer.class); 108 | 109 | Taikai.builder() 110 | .namespace(VALID_NAMESPACE) 111 | .test(customizer) 112 | .build(); 113 | 114 | verify(customizer, times(1)).customize(any(TestConfigurer.class)); 115 | } 116 | 117 | @Test 118 | void shouldThrowExceptionForNullCustomizer() { 119 | assertThrows(NullPointerException.class, () -> Taikai.builder().java(null)); 120 | assertThrows(NullPointerException.class, () -> Taikai.builder().spring(null)); 121 | assertThrows(NullPointerException.class, () -> Taikai.builder().test(null)); 122 | } 123 | 124 | @Test 125 | void shouldCheckRules() { 126 | TaikaiRule mockRule = mock(TaikaiRule.class); 127 | 128 | Taikai.builder() 129 | .namespace(VALID_NAMESPACE) 130 | .addRule(mockRule) 131 | .build() 132 | .check(); 133 | 134 | verify(mockRule, times(1)).check(VALID_NAMESPACE, null, emptyList()); 135 | } 136 | 137 | @Test 138 | void shouldRebuildTaikaiWithNewValues() { 139 | Taikai taikai = Taikai.builder() 140 | .namespace(VALID_NAMESPACE) 141 | .excludeClasses("com.enofex.taikai.ClassToExclude") 142 | .failOnEmpty(true) 143 | .java(java -> java 144 | .fieldsShouldNotBePublic()) 145 | .build(); 146 | 147 | Taikai modifiedTaikai = taikai.toBuilder() 148 | .namespace("com.enofex.newnamespace") 149 | .excludeClasses("com.enofex.taikai.AnotherClassToExclude") 150 | .failOnEmpty(false) 151 | .java(java -> java 152 | .classesShouldImplementHashCodeAndEquals() 153 | .finalClassesShouldNotHaveProtectedMembers()) 154 | .build(); 155 | 156 | assertFalse(modifiedTaikai.failOnEmpty()); 157 | assertEquals("com.enofex.newnamespace", modifiedTaikai.namespace()); 158 | assertEquals(2, modifiedTaikai.excludedClasses().size()); 159 | assertEquals(3, modifiedTaikai.rules().size()); 160 | assertTrue(modifiedTaikai.excludedClasses().contains("com.enofex.taikai.ClassToExclude")); 161 | assertTrue( 162 | modifiedTaikai.excludedClasses().contains("com.enofex.taikai.AnotherClassToExclude")); 163 | } 164 | 165 | @Test 166 | void shouldThrowExceptionIfNamespaceAndClasses() { 167 | assertThrows(IllegalArgumentException.class, () -> Taikai.builder() 168 | .namespace(VALID_NAMESPACE) 169 | .classes(TaikaiTest.class) 170 | .build()); 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/spring/PropertiesConfigurerTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.spring; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.enofex.taikai.Taikai; 7 | import org.junit.jupiter.api.Nested; 8 | import org.junit.jupiter.api.Test; 9 | import org.springframework.boot.context.properties.ConfigurationProperties; 10 | import org.springframework.validation.annotation.Validated; 11 | 12 | class PropertiesConfigurerTest { 13 | 14 | @Nested 15 | class NamesShouldEndWithProperties { 16 | 17 | @Test 18 | void shouldNotThrowWhenClassEndsWithProperties() { 19 | Taikai taikai = Taikai.builder() 20 | .classes(ApplicationProperties.class) 21 | .spring(spring -> spring.properties(PropertiesConfigurer::namesShouldEndWithProperties)) 22 | .build(); 23 | 24 | assertDoesNotThrow(taikai::check); 25 | } 26 | 27 | @Test 28 | void shouldNotThrowWhenNonPropertiesClassIsNotAnnotated() { 29 | Taikai taikai = Taikai.builder() 30 | .classes(RandomUtility.class) 31 | .spring(spring -> spring.properties(PropertiesConfigurer::namesShouldEndWithProperties)) 32 | .build(); 33 | 34 | assertDoesNotThrow(taikai::check); 35 | } 36 | 37 | @Test 38 | void shouldThrowWhenAnnotatedClassDoesNotEndWithProperties() { 39 | Taikai taikai = Taikai.builder() 40 | .classes(InvalidConfig.class) 41 | .spring(spring -> spring.properties(PropertiesConfigurer::namesShouldEndWithProperties)) 42 | .build(); 43 | 44 | assertThrows(AssertionError.class, taikai::check); 45 | } 46 | } 47 | 48 | @Nested 49 | class ShouldBeAnnotatedWithValidated { 50 | 51 | @Test 52 | void shouldNotThrowWhenConfigurationPropertiesAnnotatedWithValidated() { 53 | Taikai taikai = Taikai.builder() 54 | .classes(ValidatedProperties.class) 55 | .spring(spring -> spring.properties(PropertiesConfigurer::shouldBeAnnotatedWithValidated)) 56 | .build(); 57 | 58 | assertDoesNotThrow(taikai::check); 59 | } 60 | 61 | @Test 62 | void shouldThrowWhenConfigurationPropertiesMissingValidatedAnnotation() { 63 | Taikai taikai = Taikai.builder() 64 | .classes(ApplicationProperties.class) 65 | .spring(spring -> spring.properties(PropertiesConfigurer::shouldBeAnnotatedWithValidated)) 66 | .build(); 67 | 68 | assertThrows(AssertionError.class, taikai::check); 69 | } 70 | 71 | @Test 72 | void shouldNotThrowWhenNotAnnotatedWithConfigurationProperties() { 73 | Taikai taikai = Taikai.builder() 74 | .classes(RandomUtility.class) 75 | .spring(spring -> spring.properties(PropertiesConfigurer::shouldBeAnnotatedWithValidated)) 76 | .build(); 77 | 78 | assertDoesNotThrow(taikai::check); 79 | } 80 | } 81 | 82 | @Nested 83 | class ShouldBeAnnotatedWithConfigurationProperties { 84 | 85 | @Test 86 | void shouldNotThrowWhenAnnotatedWithConfigurationProperties() { 87 | Taikai taikai = Taikai.builder() 88 | .classes(ApplicationProperties.class) 89 | .spring(spring -> spring.properties( 90 | PropertiesConfigurer::shouldBeAnnotatedWithConfigurationProperties)) 91 | .build(); 92 | 93 | assertDoesNotThrow(taikai::check); 94 | } 95 | 96 | @Test 97 | void shouldThrowWhenPropertiesClassIsMissingAnnotation() { 98 | Taikai taikai = Taikai.builder() 99 | .classes(MissingAnnotationProperties.class) 100 | .spring(spring -> spring.properties( 101 | PropertiesConfigurer::shouldBeAnnotatedWithConfigurationProperties)) 102 | .build(); 103 | 104 | assertThrows(AssertionError.class, taikai::check); 105 | } 106 | 107 | @Test 108 | void shouldNotThrowWhenClassNameDoesNotMatchPattern() { 109 | Taikai taikai = Taikai.builder() 110 | .classes(RandomUtility.class) 111 | .spring(spring -> spring.properties( 112 | PropertiesConfigurer::shouldBeAnnotatedWithConfigurationProperties)) 113 | .build(); 114 | 115 | assertDoesNotThrow(taikai::check); 116 | } 117 | } 118 | 119 | @Nested 120 | class ShouldBeRecords { 121 | 122 | @Test 123 | void shouldNotThrowWhenConfigurationPropertiesIsRecord() { 124 | Taikai taikai = Taikai.builder() 125 | .classes(RecordApplicationProperties.class) 126 | .spring(spring -> spring.properties(PropertiesConfigurer::shouldBeRecords)) 127 | .build(); 128 | 129 | assertDoesNotThrow(taikai::check); 130 | } 131 | 132 | @Test 133 | void shouldThrowWhenConfigurationPropertiesIsNotRecord() { 134 | Taikai taikai = Taikai.builder() 135 | .classes(ApplicationProperties.class) 136 | .spring(spring -> spring.properties(PropertiesConfigurer::shouldBeRecords)) 137 | .build(); 138 | 139 | assertThrows(AssertionError.class, taikai::check); 140 | } 141 | 142 | @Test 143 | void shouldNotThrowWhenNonConfigurationPropertiesClassIsNotRecord() { 144 | Taikai taikai = Taikai.builder() 145 | .classes(RandomUtility.class) 146 | .spring(spring -> spring.properties(PropertiesConfigurer::shouldBeRecords)) 147 | .build(); 148 | 149 | assertDoesNotThrow(taikai::check); 150 | } 151 | } 152 | 153 | @ConfigurationProperties(prefix = "app") 154 | static class ApplicationProperties { 155 | 156 | private String name; 157 | } 158 | 159 | @ConfigurationProperties(prefix = "app") 160 | @Validated 161 | static class ValidatedProperties { 162 | 163 | private String host; 164 | } 165 | 166 | @ConfigurationProperties(prefix = "invalid") 167 | static class InvalidConfig { 168 | 169 | private int port; 170 | } 171 | 172 | static class MissingAnnotationProperties { 173 | 174 | private boolean enabled; 175 | } 176 | 177 | static class RandomUtility { 178 | 179 | static void doSomething() { 180 | } 181 | } 182 | 183 | @ConfigurationProperties(prefix = "record") 184 | record RecordApplicationProperties(String name, int port) { 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/test/java/com/enofex/taikai/spring/ControllersConfigurerTest.java: -------------------------------------------------------------------------------- 1 | package com.enofex.taikai.spring; 2 | 3 | import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; 4 | import static org.junit.jupiter.api.Assertions.assertThrows; 5 | 6 | import com.enofex.taikai.Taikai; 7 | import jakarta.validation.constraints.NotNull; 8 | import org.junit.jupiter.api.Nested; 9 | import org.junit.jupiter.api.Test; 10 | import org.springframework.stereotype.Controller; 11 | import org.springframework.validation.annotation.Validated; 12 | import org.springframework.web.bind.annotation.PathVariable; 13 | import org.springframework.web.bind.annotation.RequestParam; 14 | import org.springframework.web.bind.annotation.RestController; 15 | 16 | class ControllersConfigurerTest { 17 | 18 | @Nested 19 | class NamesShouldEndWithController { 20 | 21 | @Test 22 | void shouldNotThrowWhenControllerEndsWithController() { 23 | Taikai taikai = Taikai.builder() 24 | .classes(ValidUserController.class) 25 | .spring(spring -> spring.controllers(ControllersConfigurer::namesShouldEndWithController)) 26 | .build(); 27 | 28 | assertDoesNotThrow(taikai::check); 29 | } 30 | 31 | @Test 32 | void shouldNotThrowWhenClassIsNotControllerOrRestController() { 33 | Taikai taikai = Taikai.builder() 34 | .classes(InvalidUserHandler.class) 35 | .spring(spring -> spring.controllers(ControllersConfigurer::namesShouldEndWithController)) 36 | .build(); 37 | 38 | assertDoesNotThrow(taikai::check); 39 | } 40 | 41 | } 42 | 43 | @Nested 44 | class ShouldBeAnnotatedWithRestController { 45 | 46 | @Test 47 | void shouldNotThrowWhenRestControllerAnnotated() { 48 | Taikai taikai = Taikai.builder() 49 | .classes(ValidUserController.class) 50 | .spring(spring -> spring.controllers( 51 | ControllersConfigurer::shouldBeAnnotatedWithRestController)) 52 | .build(); 53 | 54 | assertDoesNotThrow(taikai::check); 55 | } 56 | 57 | @Test 58 | void shouldThrowWhenControllerMissingRestControllerAnnotation() { 59 | Taikai taikai = Taikai.builder() 60 | .classes(MissingRestController.class) 61 | .spring(spring -> spring.controllers( 62 | ControllersConfigurer::shouldBeAnnotatedWithRestController)) 63 | .build(); 64 | 65 | assertThrows(AssertionError.class, taikai::check); 66 | } 67 | } 68 | 69 | @Nested 70 | class ShouldBeAnnotatedWithController { 71 | 72 | @Test 73 | void shouldNotThrowWhenControllerAnnotated() { 74 | Taikai taikai = Taikai.builder() 75 | .classes(BasicController.class) 76 | .spring(spring -> spring.controllers( 77 | ControllersConfigurer::shouldBeAnnotatedWithController)) 78 | .build(); 79 | 80 | assertDoesNotThrow(taikai::check); 81 | } 82 | 83 | @Test 84 | void shouldThrowWhenMissingControllerAnnotation() { 85 | Taikai taikai = Taikai.builder() 86 | .classes(UnannotatedController.class) 87 | .spring(spring -> spring.controllers( 88 | ControllersConfigurer::shouldBeAnnotatedWithController)) 89 | .build(); 90 | 91 | assertThrows(AssertionError.class, taikai::check); 92 | } 93 | } 94 | 95 | @Nested 96 | class ShouldBePackagePrivate { 97 | 98 | @Test 99 | void shouldThrowWhenControllerIsPublic() { 100 | Taikai taikai = Taikai.builder() 101 | .classes(PublicController.class) 102 | .spring(spring -> spring.controllers(ControllersConfigurer::shouldBePackagePrivate)) 103 | .build(); 104 | 105 | assertThrows(AssertionError.class, taikai::check); 106 | } 107 | 108 | @Test 109 | void shouldNotThrowWhenControllerIsPackagePrivate() { 110 | Taikai taikai = Taikai.builder() 111 | .classes(PackagePrivateController.class) 112 | .spring(spring -> spring.controllers(ControllersConfigurer::shouldBePackagePrivate)) 113 | .build(); 114 | 115 | assertDoesNotThrow(taikai::check); 116 | } 117 | } 118 | 119 | @Nested 120 | class ShouldBeAnnotatedWithValidated { 121 | 122 | @Test 123 | void shouldNotThrowWhenControllerIsValidated() { 124 | Taikai taikai = Taikai.builder() 125 | .classes(ValidatedUserController.class) 126 | .spring(spring -> spring.controllers( 127 | ControllersConfigurer::shouldBeAnnotatedWithValidated)) 128 | .build(); 129 | 130 | assertDoesNotThrow(taikai::check); 131 | } 132 | 133 | @Test 134 | void shouldNotThrowWhenControllerHasNoValidationAnnotations() { 135 | Taikai taikai = Taikai.builder() 136 | .classes(UnvalidatedUserController.class) 137 | .spring(spring -> spring.controllers( 138 | ControllersConfigurer::shouldBeAnnotatedWithValidated)) 139 | .build(); 140 | 141 | assertDoesNotThrow(taikai::check); 142 | } 143 | 144 | @Test 145 | void shouldThrowWhenControllerHasValidationAnnotationsButMissingValidatedAnnotation() { 146 | Taikai taikai = Taikai.builder() 147 | .classes( 148 | UnvalidatedControllerWithValidationAnnotations.class) 149 | .spring(spring -> spring.controllers( 150 | ControllersConfigurer::shouldBeAnnotatedWithValidated)) 151 | .build(); 152 | 153 | assertThrows(AssertionError.class, taikai::check); 154 | } 155 | } 156 | 157 | @RestController 158 | static class ValidUserController { 159 | 160 | } 161 | 162 | @Controller 163 | static class BasicController { 164 | 165 | } 166 | 167 | static class InvalidUserHandler { 168 | 169 | } 170 | 171 | @Controller 172 | static class MissingRestController { 173 | 174 | } 175 | 176 | @Controller 177 | public static class PublicController { 178 | 179 | } 180 | 181 | @Controller 182 | static class PackagePrivateController { 183 | 184 | } 185 | 186 | @RestController 187 | @Validated 188 | static class ValidatedUserController { 189 | 190 | } 191 | 192 | @RestController 193 | static class UnvalidatedUserController { 194 | 195 | } 196 | 197 | @RestController 198 | static class UnvalidatedControllerWithValidationAnnotations { 199 | 200 | public String getUser(@RequestParam @NotNull String id) { 201 | return id; 202 | } 203 | 204 | public String getByPath(@PathVariable @NotNull String userId) { 205 | return userId; 206 | } 207 | } 208 | 209 | static class UnannotatedController { 210 | 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | <# : batch portion 2 | @REM ---------------------------------------------------------------------------- 3 | @REM Licensed to the Apache Software Foundation (ASF) under one 4 | @REM or more contributor license agreements. See the NOTICE file 5 | @REM distributed with this work for additional information 6 | @REM regarding copyright ownership. The ASF licenses this file 7 | @REM to you under the Apache License, Version 2.0 (the 8 | @REM "License"); you may not use this file except in compliance 9 | @REM with the License. You may obtain a copy of the License at 10 | @REM 11 | @REM https://www.apache.org/licenses/LICENSE-2.0 12 | @REM 13 | @REM Unless required by applicable law or agreed to in writing, 14 | @REM software distributed under the License is distributed on an 15 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | @REM KIND, either express or implied. See the License for the 17 | @REM specific language governing permissions and limitations 18 | @REM under the License. 19 | @REM ---------------------------------------------------------------------------- 20 | 21 | @REM ---------------------------------------------------------------------------- 22 | @REM Apache Maven Wrapper startup batch script, version 3.3.1 23 | @REM 24 | @REM Optional ENV vars 25 | @REM MVNW_REPOURL - repo url base for downloading maven distribution 26 | @REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven 27 | @REM MVNW_VERBOSE - true: enable verbose log; others: silence the output 28 | @REM ---------------------------------------------------------------------------- 29 | 30 | @IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) 31 | @SET __MVNW_CMD__= 32 | @SET __MVNW_ERROR__= 33 | @SET __MVNW_PSMODULEP_SAVE=%PSModulePath% 34 | @SET PSModulePath= 35 | @FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( 36 | IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) 37 | ) 38 | @SET PSModulePath=%__MVNW_PSMODULEP_SAVE% 39 | @SET __MVNW_PSMODULEP_SAVE= 40 | @SET __MVNW_ARG0_NAME__= 41 | @SET MVNW_USERNAME= 42 | @SET MVNW_PASSWORD= 43 | @IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) 44 | @echo Cannot start maven from wrapper >&2 && exit /b 1 45 | @GOTO :EOF 46 | : end batch / begin powershell #> 47 | 48 | $ErrorActionPreference = "Stop" 49 | if ($env:MVNW_VERBOSE -eq "true") { 50 | $VerbosePreference = "Continue" 51 | } 52 | 53 | # calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties 54 | $distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl 55 | if (!$distributionUrl) { 56 | Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" 57 | } 58 | 59 | switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { 60 | "maven-mvnd-*" { 61 | $USE_MVND = $true 62 | $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" 63 | $MVN_CMD = "mvnd.cmd" 64 | break 65 | } 66 | default { 67 | $USE_MVND = $false 68 | $MVN_CMD = $script -replace '^mvnw','mvn' 69 | break 70 | } 71 | } 72 | 73 | # apply MVNW_REPOURL and calculate MAVEN_HOME 74 | # maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ 75 | if ($env:MVNW_REPOURL) { 76 | $MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" } 77 | $distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')" 78 | } 79 | $distributionUrlName = $distributionUrl -replace '^.*/','' 80 | $distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$','' 81 | $MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" 82 | $MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join '' 83 | $MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME" 84 | 85 | if (Test-Path -Path "$MAVEN_HOME" -PathType Container) { 86 | Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME" 87 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 88 | exit $? 89 | } 90 | 91 | if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) { 92 | Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl" 93 | } 94 | 95 | # prepare tmp dir 96 | $TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile 97 | $TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir" 98 | $TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null 99 | trap { 100 | if ($TMP_DOWNLOAD_DIR.Exists) { 101 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 102 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 103 | } 104 | } 105 | 106 | New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null 107 | 108 | # Download and Install Apache Maven 109 | Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." 110 | Write-Verbose "Downloading from: $distributionUrl" 111 | Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" 112 | 113 | $webclient = New-Object System.Net.WebClient 114 | if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) { 115 | $webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD) 116 | } 117 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 118 | $webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null 119 | 120 | # If specified, validate the SHA-256 sum of the Maven distribution zip file 121 | $distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum 122 | if ($distributionSha256Sum) { 123 | if ($USE_MVND) { 124 | Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." 125 | } 126 | Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash 127 | if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) { 128 | Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property." 129 | } 130 | } 131 | 132 | # unzip and move 133 | Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null 134 | Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null 135 | try { 136 | Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null 137 | } catch { 138 | if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { 139 | Write-Error "fail to move MAVEN_HOME" 140 | } 141 | } finally { 142 | try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } 143 | catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } 144 | } 145 | 146 | Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" 147 | --------------------------------------------------------------------------------