diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..07b230b --- /dev/null +++ b/.editorconfig @@ -0,0 +1,23 @@ +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +insert_final_newline = true +max_line_length = 120 +ij_continuation_indent_size = 8 +tab_width = 4 +trim_trailing_whitespace = true + +[{*.kt,*.kts}] +ij_kotlin_allow_trailing_comma = false +ij_kotlin_allow_trailing_comma_on_call_site = false +ij_kotlin_name_count_to_use_star_import_for_members = 20 +ij_kotlin_name_count_to_use_star_import = 20 +ij_kotlin_continuation_indent_size = 4 +ktlint_code_style=intellij_idea + +[*.java] +ij_java_names_count_to_use_import_on_demand = 20 +ij_java_class_count_to_use_import_on_demand = 20 \ No newline at end of file diff --git a/archrules-deprecation/build.gradle.kts b/archrules-deprecation/build.gradle.kts index de14a6d..372da28 100644 --- a/archrules-deprecation/build.gradle.kts +++ b/archrules-deprecation/build.gradle.kts @@ -3,9 +3,7 @@ plugins { id("com.netflix.nebula.archrules.library") } description = "Arch Rules for detecting usage of deprecated code" -repositories { - mavenCentral() -} + dependencies { archRulesImplementation(libs.jspecify) diff --git a/archrules-deprecation/src/archRules/java/com/netflix/nebula/archrules/deprecation/DeprecationRule.java b/archrules-deprecation/src/archRules/java/com/netflix/nebula/archrules/deprecation/DeprecationRule.java index 546a9d2..7307714 100644 --- a/archrules-deprecation/src/archRules/java/com/netflix/nebula/archrules/deprecation/DeprecationRule.java +++ b/archrules-deprecation/src/archRules/java/com/netflix/nebula/archrules/deprecation/DeprecationRule.java @@ -26,7 +26,7 @@ public class DeprecationRule implements ArchRulesService { * - Kotlin @Deprecated annotations * - Kotlin @DeprecatedSinceKotlin annotations */ - public static ArchRule deprecationRule = ArchRuleDefinition.priority(Priority.LOW) + public static final ArchRule deprecationRule = ArchRuleDefinition.priority(Priority.LOW) .noClasses() // Java deprecated .should().dependOnClassesThat().areAnnotatedWith(Deprecated.class) @@ -58,7 +58,7 @@ public boolean test(JavaAnnotation annotation) { } }; - public static ArchRule deprecationForRemovalRule = ArchRuleDefinition.priority(Priority.MEDIUM) + public static final ArchRule deprecationForRemovalRule = ArchRuleDefinition.priority(Priority.MEDIUM) .noClasses() .should().dependOnClassesThat().areAnnotatedWith(deprecatedForRemoval) .orShould().accessTargetWhere(targetOwner(annotatedWith(deprecatedForRemoval))) diff --git a/archrules-gradle-plugin-development/build.gradle.kts b/archrules-gradle-plugin-development/build.gradle.kts new file mode 100644 index 0000000..e844f58 --- /dev/null +++ b/archrules-gradle-plugin-development/build.gradle.kts @@ -0,0 +1,23 @@ +plugins { + id("com.netflix.nebula.library") + id("com.netflix.nebula.archrules.library") +} + +description = "Arch Rules for detecting bad practices when developing Gradle plugins" + +dependencies { + archRulesImplementation(libs.jspecify) + + archRulesTestImplementation(libs.assertj) + archRulesTestImplementation(libs.logback) + archRulesTestImplementation(gradleApi()) +} +java { + toolchain { + languageVersion = JavaLanguageVersion.of(8) + } +} + +dependencyLocking { + lockAllConfigurations() +} diff --git a/archrules-gradle-plugin-development/gradle.lockfile b/archrules-gradle-plugin-development/gradle.lockfile new file mode 100644 index 0000000..53f13ad --- /dev/null +++ b/archrules-gradle-plugin-development/gradle.lockfile @@ -0,0 +1,27 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +ch.qos.logback:logback-classic:1.5.20=archRulesTestArchRulesRuntime,archRulesTestCompileClasspath,archRulesTestRuntimeClasspath +ch.qos.logback:logback-core:1.5.20=archRulesTestArchRulesRuntime,archRulesTestCompileClasspath,archRulesTestRuntimeClasspath +com.netflix.nebula:archrules-deprecation:0.4.0=archRules +com.netflix.nebula:archrules-joda:0.4.0=archRules +com.netflix.nebula:archrules-nullability:0.4.0=archRules +com.netflix.nebula:archrules-testing-frameworks:0.4.0=archRules +com.netflix.nebula:nebula-archrules-core:0.3.0=archRules +com.netflix.nebula:nebula-archrules-core:0.5.2=archRulesArchRulesRuntime,archRulesCompileClasspath,archRulesRuntimeClasspath,archRulesTestArchRulesRuntime,archRulesTestCompileClasspath,archRulesTestRuntimeClasspath +com.tngtech.archunit:archunit:1.4.1=archRules,archRulesArchRulesRuntime,archRulesCompileClasspath,archRulesRuntimeClasspath,archRulesTestArchRulesRuntime,archRulesTestCompileClasspath,archRulesTestRuntimeClasspath +net.bytebuddy:byte-buddy:1.17.7=archRulesTestArchRulesRuntime,archRulesTestCompileClasspath,archRulesTestRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=archRulesTestCompileClasspath +org.assertj:assertj-core:3.27.6=archRulesTestArchRulesRuntime,archRulesTestCompileClasspath,archRulesTestRuntimeClasspath +org.jspecify:jspecify:1.0.0=archRules,archRulesArchRulesRuntime,archRulesCompileClasspath,archRulesRuntimeClasspath,archRulesTestArchRulesRuntime,archRulesTestCompileClasspath,archRulesTestRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.12.2=archRulesTestArchRulesRuntime,archRulesTestCompileClasspath,archRulesTestRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.12.2=archRulesTestArchRulesRuntime,archRulesTestRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.12.2=archRulesTestArchRulesRuntime,archRulesTestCompileClasspath,archRulesTestRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.12.2=archRulesTestArchRulesRuntime,archRulesTestCompileClasspath,archRulesTestRuntimeClasspath +org.junit.platform:junit-platform-commons:1.12.2=archRulesTestArchRulesRuntime,archRulesTestCompileClasspath,archRulesTestRuntimeClasspath +org.junit.platform:junit-platform-engine:1.12.2=archRulesTestArchRulesRuntime,archRulesTestRuntimeClasspath +org.junit.platform:junit-platform-launcher:1.12.2=archRulesTestArchRulesRuntime,archRulesTestRuntimeClasspath +org.junit:junit-bom:5.12.2=archRulesTestArchRulesRuntime,archRulesTestCompileClasspath,archRulesTestRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=archRulesTestArchRulesRuntime,archRulesTestCompileClasspath,archRulesTestRuntimeClasspath +org.slf4j:slf4j-api:2.0.17=archRules,archRulesArchRulesRuntime,archRulesCompileClasspath,archRulesRuntimeClasspath,archRulesTestArchRulesRuntime,archRulesTestCompileClasspath,archRulesTestRuntimeClasspath +empty=annotationProcessor,archRulesAnnotationProcessor,archRulesTestAnnotationProcessor,compileClasspath,mainArchRulesRuntime,runtimeClasspath,testAnnotationProcessor,testArchRulesRuntime,testCompileClasspath,testRuntimeClasspath diff --git a/archrules-gradle-plugin-development/src/archRules/java/com/netflix/nebula/archrules/gradleplugins/GradleTaskActionRule.java b/archrules-gradle-plugin-development/src/archRules/java/com/netflix/nebula/archrules/gradleplugins/GradleTaskActionRule.java new file mode 100644 index 0000000..a13f079 --- /dev/null +++ b/archrules-gradle-plugin-development/src/archRules/java/com/netflix/nebula/archrules/gradleplugins/GradleTaskActionRule.java @@ -0,0 +1,123 @@ +package com.netflix.nebula.archrules.gradleplugins; + +import com.netflix.nebula.archrules.core.ArchRulesService; +import com.tngtech.archunit.base.DescribedPredicate; +import com.tngtech.archunit.core.domain.JavaAccess; +import com.tngtech.archunit.core.domain.JavaFieldAccess; +import com.tngtech.archunit.core.domain.JavaMethod; +import com.tngtech.archunit.lang.ArchCondition; +import com.tngtech.archunit.lang.ArchRule; +import com.tngtech.archunit.lang.ConditionEvents; +import com.tngtech.archunit.lang.Priority; +import com.tngtech.archunit.lang.SimpleConditionEvent; +import com.tngtech.archunit.lang.syntax.ArchRuleDefinition; +import org.jspecify.annotations.NullMarked; + +import java.util.HashMap; +import java.util.Map; + +/** + * Rules for Gradle task action methods to ensure Gradle 10 compatibility. + */ +@NullMarked +public class GradleTaskActionRule implements ArchRulesService { + + /** + * Prevents {@code @TaskAction} methods from accessing {@code Project}. + *

+ * Accessing {@code Project} in task actions breaks configuration cache and will + * cause runtime errors in Gradle 10+. Move Project access to configuration time + * (constructor/initializer) and use task properties instead. + */ + public static final ArchRule taskActionShouldNotAccessProject = ArchRuleDefinition.priority(Priority.MEDIUM) + .methods() + .that(areAnnotatedWithTaskAction()) + .should(notAccessProject()) + .allowEmptyShould(true) + .because( + "Accessing Project in @TaskAction methods breaks configuration cache and will be removed in Gradle 10. " + + "Move Project access to task configuration time and use task inputs/properties instead. " + + "See https://docs.gradle.org/9.2.0/userguide/upgrading_version_7.html#task_project" + ); + + /** + * Prevents {@code @TaskAction} methods from calling {@code getTaskDependencies()}. + *

+ * Calling {@code getTaskDependencies()} in task actions breaks configuration cache and will + * cause runtime errors in Gradle 10+. Task dependencies should be declared at configuration time. + */ + public static final ArchRule taskActionShouldNotCallGetTaskDependencies = ArchRuleDefinition.priority(Priority.MEDIUM) + .methods() + .that(areAnnotatedWithTaskAction()) + .should(notCallGetTaskDependencies()) + .allowEmptyShould(true) + .because( + "Calling getTaskDependencies() in @TaskAction methods breaks configuration cache and will be removed in Gradle 10. " + + "Declare task dependencies at configuration time instead. " + + "See https://docs.gradle.org/9.2.0/userguide/upgrading_version_7.html#task_dependencies" + ); + + private static DescribedPredicate areAnnotatedWithTaskAction() { + return new DescribedPredicate("are annotated with @TaskAction") { + @Override + public boolean test(JavaMethod method) { + return method.isAnnotatedWith("org.gradle.api.tasks.TaskAction"); + } + }; + } + + private static ArchCondition notAccessProject() { + return notAccessType("Project", "org.gradle.api.Project", "getProject"); + } + + private static ArchCondition notCallGetTaskDependencies() { + return notAccessType("TaskDependency", "org.gradle.api.tasks.TaskDependency", "getTaskDependencies"); + } + + private static ArchCondition notAccessType(String displayName, String fullyQualifiedClassName, String getterMethodName) { + return new ArchCondition("not access " + displayName) { + @Override + public void check(JavaMethod method, ConditionEvents events) { + for (JavaAccess access : method.getAccessesFromSelf()) { + if (isTargetTypeAccess(access) || isGetterCall(access) || isTargetTypeFieldAccess(access)) { + String message = String.format( + "Method %s.%s() accesses %s at %s", + method.getOwner().getSimpleName(), + method.getName(), + displayName, + access.getDescription() + ); + events.add(SimpleConditionEvent.violated(access, message)); + } + } + } + + private boolean isTargetTypeAccess(JavaAccess access) { + return fullyQualifiedClassName.equals(access.getTargetOwner().getFullName()); + } + + private boolean isGetterCall(JavaAccess access) { + if (!getterMethodName.equals(access.getName())) { + return false; + } + return access.getTargetOwner().isAssignableTo("org.gradle.api.Task"); + } + + private boolean isTargetTypeFieldAccess(JavaAccess access) { + if (access instanceof JavaFieldAccess) { + JavaFieldAccess fieldAccess = (JavaFieldAccess) access; + return fieldAccess.getTarget().getRawType().isAssignableTo(fullyQualifiedClassName); + } + return false; + } + }; + } + + @Override + public Map getRules() { + Map rules = new HashMap<>(); + rules.put("gradle-task-action-project-access", taskActionShouldNotAccessProject); + rules.put("gradle-task-action-task-dependencies", taskActionShouldNotCallGetTaskDependencies); + return rules; + } +} diff --git a/archrules-gradle-plugin-development/src/archRulesTest/java/com/netflix/nebula/archrules/gradleplugins/GradleTaskActionRuleTest.java b/archrules-gradle-plugin-development/src/archRulesTest/java/com/netflix/nebula/archrules/gradleplugins/GradleTaskActionRuleTest.java new file mode 100644 index 0000000..19a6be3 --- /dev/null +++ b/archrules-gradle-plugin-development/src/archRulesTest/java/com/netflix/nebula/archrules/gradleplugins/GradleTaskActionRuleTest.java @@ -0,0 +1,142 @@ +package com.netflix.nebula.archrules.gradleplugins; + +import com.netflix.nebula.archrules.core.Runner; +import com.tngtech.archunit.lang.EvaluationResult; +import org.gradle.api.DefaultTask; +import org.gradle.api.Project; +import org.gradle.api.provider.Property; +import org.gradle.api.tasks.Input; +import org.gradle.api.tasks.TaskAction; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static org.assertj.core.api.Assertions.assertThat; + +public class GradleTaskActionRuleTest { + private static final Logger LOG = LoggerFactory.getLogger(GradleTaskActionRuleTest.class); + + @Test + public void taskActionAccessingProject_should_fail() { + final EvaluationResult result = Runner.check( + GradleTaskActionRule.taskActionShouldNotAccessProject, + TaskAccessingProject.class + ); + LOG.info(result.getFailureReport().toString()); + assertThat(result.hasViolation()).isTrue(); + } + + @Test + public void taskActionCallingGetProject_should_fail() { + final EvaluationResult result = Runner.check( + GradleTaskActionRule.taskActionShouldNotAccessProject, + TaskCallingGetProject.class + ); + LOG.info(result.getFailureReport().toString()); + assertThat(result.hasViolation()).isTrue(); + } + + @Test + public void taskActionNotAccessingProject_should_pass() { + final EvaluationResult result = Runner.check( + GradleTaskActionRule.taskActionShouldNotAccessProject, + TaskNotAccessingProject.class + ); + LOG.info(result.getFailureReport().toString()); + assertThat(result.hasViolation()).isFalse(); + } + + @Test + public void taskActionAccessingProjectInConstructor_should_pass() { + final EvaluationResult result = Runner.check( + GradleTaskActionRule.taskActionShouldNotAccessProject, + TaskAccessingProjectInConstructor.class + ); + LOG.info(result.getFailureReport().toString()); + assertThat(result.hasViolation()).isFalse(); + } + + @Test + public void taskActionCallingGetTaskDependencies_should_fail() { + final EvaluationResult result = Runner.check( + GradleTaskActionRule.taskActionShouldNotCallGetTaskDependencies, + TaskCallingGetTaskDependencies.class + ); + LOG.info(result.getFailureReport().toString()); + assertThat(result.hasViolation()).isTrue(); + } + + @Test + public void taskActionNotCallingGetTaskDependencies_should_pass() { + final EvaluationResult result = Runner.check( + GradleTaskActionRule.taskActionShouldNotCallGetTaskDependencies, + TaskNotCallingGetTaskDependencies.class + ); + LOG.info(result.getFailureReport().toString()); + assertThat(result.hasViolation()).isFalse(); + } + + @SuppressWarnings("unused") + public static abstract class TaskAccessingProject extends DefaultTask { + @TaskAction + public void execute() { + Project project = getProject(); + String version = project.getVersion().toString(); + System.out.println("Version: " + version); + } + } + + @SuppressWarnings("unused") + public static abstract class TaskCallingGetProject extends DefaultTask { + @TaskAction + public void execute() { + getProject(); + System.out.println("Task executed"); + } + } + + @SuppressWarnings("unused") + public static abstract class TaskNotAccessingProject extends DefaultTask { + @Input + public abstract Property getVersion(); + + @TaskAction + public void execute() { + String version = getVersion().get(); + System.out.println("Version: " + version); + } + } + + @SuppressWarnings("unused") + public static abstract class TaskAccessingProjectInConstructor extends DefaultTask { + @Input + public abstract Property getVersion(); + + public TaskAccessingProjectInConstructor() { + getVersion().set(getProject().getVersion().toString()); + } + + @TaskAction + public void execute() { + String version = getVersion().get(); + System.out.println("Version: " + version); + } + } + + @SuppressWarnings("unused") + public static abstract class TaskCallingGetTaskDependencies extends DefaultTask { + @TaskAction + public void execute() { + getTaskDependencies(); + System.out.println("Task executed"); + } + } + + @SuppressWarnings("unused") + public static abstract class TaskNotCallingGetTaskDependencies extends DefaultTask { + @TaskAction + public void execute() { + System.out.println("Task executed"); + } + } +} diff --git a/archrules-joda/build.gradle.kts b/archrules-joda/build.gradle.kts index 8059abd..5e4ed92 100644 --- a/archrules-joda/build.gradle.kts +++ b/archrules-joda/build.gradle.kts @@ -2,10 +2,9 @@ plugins { id("com.netflix.nebula.library") id("com.netflix.nebula.archrules.library") } + description = "Arch Rules for detecting usage of Joda" -repositories { - mavenCentral() -} + dependencies { archRulesImplementation(libs.jspecify) diff --git a/archrules-joda/src/archRules/java/com/netflix/nebula/archrules/joda/JodaRule.java b/archrules-joda/src/archRules/java/com/netflix/nebula/archrules/joda/JodaRule.java index 1e025c5..0af8c46 100644 --- a/archrules-joda/src/archRules/java/com/netflix/nebula/archrules/joda/JodaRule.java +++ b/archrules-joda/src/archRules/java/com/netflix/nebula/archrules/joda/JodaRule.java @@ -15,7 +15,7 @@ public class JodaRule implements ArchRulesService { /** * This rule is a stop-gap to find all usages of Joda. */ - public static ArchRule jodaRule = ArchRuleDefinition.priority(Priority.MEDIUM) + public static final ArchRule jodaRule = ArchRuleDefinition.priority(Priority.MEDIUM) .noClasses() .should(GeneralCodingRules.USE_JODATIME) .allowEmptyShould(true) diff --git a/archrules-testing-frameworks/build.gradle.kts b/archrules-testing-frameworks/build.gradle.kts index 2a377c1..1729470 100644 --- a/archrules-testing-frameworks/build.gradle.kts +++ b/archrules-testing-frameworks/build.gradle.kts @@ -6,9 +6,7 @@ plugins { id("com.netflix.nebula.archrules.library") } description = "Arch Rules for detecting usage of Junit4 and Testcontainers 1.x" -repositories { - mavenCentral() -} + dependencies { archRulesImplementation(libs.jspecify) diff --git a/archrules-testing-frameworks/src/archRules/java/com/netflix/nebula/archrules/testingframeworks/JUnit4Rule.java b/archrules-testing-frameworks/src/archRules/java/com/netflix/nebula/archrules/testingframeworks/JUnit4Rule.java index 363ef0a..c2e27ae 100644 --- a/archrules-testing-frameworks/src/archRules/java/com/netflix/nebula/archrules/testingframeworks/JUnit4Rule.java +++ b/archrules-testing-frameworks/src/archRules/java/com/netflix/nebula/archrules/testingframeworks/JUnit4Rule.java @@ -17,7 +17,7 @@ public class JUnit4Rule implements ArchRulesService { /** * This rule is a stop-gap to find all usages of JUnit4. */ - public static ArchRule junit4Rule = ArchRuleDefinition.priority(Priority.MEDIUM) + public static final ArchRule junit4Rule = ArchRuleDefinition.priority(Priority.MEDIUM) .noClasses() .should().dependOnClassesThat(resideInAPackage("org.junit..") .and(resideOutsideOfPackage("org.junit.jupiter.."))) diff --git a/archrules-testing-frameworks/src/archRules/java/com/netflix/nebula/archrules/testingframeworks/Testcontainers1xRule.java b/archrules-testing-frameworks/src/archRules/java/com/netflix/nebula/archrules/testingframeworks/Testcontainers1xRule.java index 2043c13..32b4e14 100644 --- a/archrules-testing-frameworks/src/archRules/java/com/netflix/nebula/archrules/testingframeworks/Testcontainers1xRule.java +++ b/archrules-testing-frameworks/src/archRules/java/com/netflix/nebula/archrules/testingframeworks/Testcontainers1xRule.java @@ -44,7 +44,7 @@ public class Testcontainers1xRule implements ArchRulesService { *

* The fix: Replace {@code DockerComposeContainer} with {@code ComposeContainer} everywhere. */ - public static ArchRule dockerComposeContainerRule = ArchRuleDefinition.priority(Priority.MEDIUM) + public static final ArchRule dockerComposeContainerRule = ArchRuleDefinition.priority(Priority.MEDIUM) .noClasses() .should().dependOnClassesThat().haveSimpleName("DockerComposeContainer") .allowEmptyShould(true) @@ -60,7 +60,7 @@ public class Testcontainers1xRule implements ArchRulesService { *

* The fix: Change {@code container.getContainerIpAddress()} to {@code container.getHost()}. */ - public static ArchRule containerIpAddressMethodRule = ArchRuleDefinition.priority(Priority.MEDIUM) + public static final ArchRule containerIpAddressMethodRule = ArchRuleDefinition.priority(Priority.MEDIUM) .noClasses() .should().callMethod("org.testcontainers.containers.ContainerState", "getContainerIpAddress") .allowEmptyShould(true) @@ -85,7 +85,7 @@ public class Testcontainers1xRule implements ArchRulesService { * *

*/ - public static ArchRule noArgConstructorRule = ArchRuleDefinition.priority(Priority.MEDIUM) + public static final ArchRule noArgConstructorRule = ArchRuleDefinition.priority(Priority.MEDIUM) .noClasses() .should().callConstructorWhere(target(new DescribedPredicate("no-arg Testcontainers container constructor") { @Override diff --git a/archrules-testing-frameworks/src/archRules/java/com/netflix/nebula/archrules/testingframeworks/Testcontainers2xRule.java b/archrules-testing-frameworks/src/archRules/java/com/netflix/nebula/archrules/testingframeworks/Testcontainers2xRule.java index aef6983..4a0fa8e 100644 --- a/archrules-testing-frameworks/src/archRules/java/com/netflix/nebula/archrules/testingframeworks/Testcontainers2xRule.java +++ b/archrules-testing-frameworks/src/archRules/java/com/netflix/nebula/archrules/testingframeworks/Testcontainers2xRule.java @@ -45,7 +45,7 @@ public class Testcontainers2xRule implements ArchRulesService { * * @see TestcontainersContainerModuleMapper for the full container-to-module mapping */ - public static ArchRule legacyContainerPackageRule = ArchRuleDefinition.priority(Priority.LOW) + public static final ArchRule legacyContainerPackageRule = ArchRuleDefinition.priority(Priority.LOW) .classes() .should(notDependOnDeprecatedContainerPackages()) .allowEmptyShould(true) diff --git a/settings.gradle.kts b/settings.gradle.kts index c803853..85c4d90 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -20,4 +20,5 @@ rootProject.name = "nebula-archrules" include(":archrules-deprecation") include(":archrules-testing-frameworks") include(":archrules-joda") -include(":archrules-nullability") \ No newline at end of file +include(":archrules-nullability") +include(":archrules-gradle-plugin-development") \ No newline at end of file