diff --git a/archrules-gradle-plugin-development/src/archRules/java/com/netflix/nebula/archrules/gradleplugins/GradleDeprecatedApiRule.java b/archrules-gradle-plugin-development/src/archRules/java/com/netflix/nebula/archrules/gradleplugins/GradleDeprecatedApiRule.java
new file mode 100644
index 0000000..8b83996
--- /dev/null
+++ b/archrules-gradle-plugin-development/src/archRules/java/com/netflix/nebula/archrules/gradleplugins/GradleDeprecatedApiRule.java
@@ -0,0 +1,101 @@
+package com.netflix.nebula.archrules.gradleplugins;
+
+import com.netflix.nebula.archrules.core.ArchRulesService;
+import com.tngtech.archunit.core.domain.JavaAccess;
+import com.tngtech.archunit.core.domain.JavaClass;
+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 to prevent usage of deprecated Gradle APIs.
+ *
+ * Using deprecated Gradle APIs will cause build failures in future Gradle versions.
+ */
+@NullMarked
+public class GradleDeprecatedApiRule implements ArchRulesService {
+
+ private static final String GRADLE_API_PACKAGE = "org.gradle";
+
+ /**
+ * Prevents plugins from using deprecated Gradle APIs.
+ *
+ * Deprecated Gradle APIs will be removed in future versions, causing build failures.
+ * Replace deprecated APIs with their modern equivalents as documented in Gradle's
+ * upgrade guides.
+ */
+ public static final ArchRule pluginsShouldNotUseDeprecatedGradleApis = ArchRuleDefinition.priority(Priority.MEDIUM)
+ .classes()
+ .that().implement("org.gradle.api.Plugin")
+ .should(notUseDeprecatedGradleApis())
+ .allowEmptyShould(true)
+ .because(
+ "Plugins should not use deprecated Gradle APIs as they will be removed in future versions. " +
+ "Consult Gradle upgrade guides for modern alternatives. " +
+ "See https://docs.gradle.org/current/userguide/upgrading_version_8.html"
+ );
+
+ /**
+ * Prevents tasks from using deprecated Gradle APIs.
+ *
+ * Deprecated Gradle APIs will be removed in future versions, causing build failures.
+ * Replace deprecated APIs with their modern equivalents as documented in Gradle's
+ * upgrade guides.
+ */
+ public static final ArchRule tasksShouldNotUseDeprecatedGradleApis = ArchRuleDefinition.priority(Priority.MEDIUM)
+ .classes()
+ .that().areAssignableTo("org.gradle.api.Task")
+ .and().areNotInterfaces()
+ .should(notUseDeprecatedGradleApis())
+ .allowEmptyShould(true)
+ .because(
+ "Tasks should not use deprecated Gradle APIs as they will be removed in future versions. " +
+ "Consult Gradle upgrade guides for modern alternatives. " +
+ "See https://docs.gradle.org/current/userguide/upgrading_version_8.html"
+ );
+
+ private static ArchCondition notUseDeprecatedGradleApis() {
+ return new ArchCondition("not use deprecated Gradle APIs") {
+ @Override
+ public void check(JavaClass javaClass, ConditionEvents events) {
+ for (JavaAccess> access : javaClass.getAccessesFromSelf()) {
+ if (isDeprecatedGradleApi(access)) {
+ String message = String.format(
+ "Class %s uses deprecated Gradle API: %s. " +
+ "This API will be removed in a future Gradle version. " +
+ "Consult Gradle upgrade guides for alternatives.",
+ javaClass.getSimpleName(),
+ access.getDescription()
+ );
+ events.add(SimpleConditionEvent.violated(access, message));
+ }
+ }
+ }
+
+ private boolean isDeprecatedGradleApi(JavaAccess> access) {
+ String targetOwnerName = access.getTargetOwner().getName();
+
+ if (!targetOwnerName.startsWith(GRADLE_API_PACKAGE)) {
+ return false;
+ }
+
+ return access.getTarget().isAnnotatedWith(Deprecated.class);
+ }
+ };
+ }
+
+ @Override
+ public Map getRules() {
+ Map rules = new HashMap<>();
+ rules.put("gradle-plugin-no-deprecated-apis", pluginsShouldNotUseDeprecatedGradleApis);
+ rules.put("gradle-task-no-deprecated-apis", tasksShouldNotUseDeprecatedGradleApis);
+ return rules;
+ }
+}
diff --git a/archrules-gradle-plugin-development/src/archRules/java/com/netflix/nebula/archrules/gradleplugins/GradleInternalApiRule.java b/archrules-gradle-plugin-development/src/archRules/java/com/netflix/nebula/archrules/gradleplugins/GradleInternalApiRule.java
new file mode 100644
index 0000000..2a129a2
--- /dev/null
+++ b/archrules-gradle-plugin-development/src/archRules/java/com/netflix/nebula/archrules/gradleplugins/GradleInternalApiRule.java
@@ -0,0 +1,99 @@
+package com.netflix.nebula.archrules.gradleplugins;
+
+import com.netflix.nebula.archrules.core.ArchRulesService;
+import com.tngtech.archunit.core.domain.JavaAccess;
+import com.tngtech.archunit.core.domain.JavaClass;
+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 to prevent usage of internal Gradle APIs.
+ *
+ * Internal Gradle APIs are not part of the public API contract and may change
+ * or be removed without notice between Gradle versions.
+ */
+@NullMarked
+public class GradleInternalApiRule implements ArchRulesService {
+
+ private static final String GRADLE_PACKAGE = "org.gradle";
+ private static final String INTERNAL_PACKAGE_MARKER = ".internal.";
+
+ /**
+ * Prevents plugins from using internal Gradle APIs.
+ *
+ * Internal Gradle APIs (packages containing {@code .internal.}) are not stable
+ * and may change or be removed between versions without notice. Use only public
+ * Gradle APIs to ensure compatibility across Gradle versions.
+ */
+ public static final ArchRule pluginsShouldNotUseInternalGradleApis = ArchRuleDefinition.priority(Priority.HIGH)
+ .classes()
+ .that().implement("org.gradle.api.Plugin")
+ .should(notUseInternalGradleApis())
+ .allowEmptyShould(true)
+ .because(
+ "Plugins should not use internal Gradle APIs (packages containing '.internal.'). " +
+ "Internal APIs are not stable and may change or be removed without notice. " +
+ "Use only public Gradle APIs documented at https://docs.gradle.org/current/javadoc/"
+ );
+
+ /**
+ * Prevents tasks from using internal Gradle APIs.
+ *
+ * Internal Gradle APIs (packages containing {@code .internal.}) are not stable
+ * and may change or be removed between versions without notice. Use only public
+ * Gradle APIs to ensure compatibility across Gradle versions.
+ */
+ public static final ArchRule tasksShouldNotUseInternalGradleApis = ArchRuleDefinition.priority(Priority.HIGH)
+ .classes()
+ .that().areAssignableTo("org.gradle.api.Task")
+ .and().areNotInterfaces()
+ .should(notUseInternalGradleApis())
+ .allowEmptyShould(true)
+ .because(
+ "Tasks should not use internal Gradle APIs (packages containing '.internal.'). " +
+ "Internal APIs are not stable and may change or be removed without notice. " +
+ "Use only public Gradle APIs documented at https://docs.gradle.org/current/javadoc/"
+ );
+
+ private static ArchCondition notUseInternalGradleApis() {
+ return new ArchCondition("not use internal Gradle APIs") {
+ @Override
+ public void check(JavaClass javaClass, ConditionEvents events) {
+ for (JavaAccess> access : javaClass.getAccessesFromSelf()) {
+ if (isInternalGradleApi(access)) {
+ String message = String.format(
+ "Class %s uses internal Gradle API: %s. " +
+ "Internal APIs (packages containing '.internal.') are not stable and may change without notice. " +
+ "Use public Gradle APIs instead.",
+ javaClass.getSimpleName(),
+ access.getDescription()
+ );
+ events.add(SimpleConditionEvent.violated(access, message));
+ }
+ }
+ }
+
+ private boolean isInternalGradleApi(JavaAccess> access) {
+ String targetPackage = access.getTargetOwner().getPackageName();
+ return targetPackage.startsWith(GRADLE_PACKAGE) &&
+ targetPackage.contains(INTERNAL_PACKAGE_MARKER);
+ }
+ };
+ }
+
+ @Override
+ public Map getRules() {
+ Map rules = new HashMap<>();
+ rules.put("gradle-plugin-no-internal-apis", pluginsShouldNotUseInternalGradleApis);
+ rules.put("gradle-task-no-internal-apis", tasksShouldNotUseInternalGradleApis);
+ return rules;
+ }
+}
diff --git a/archrules-gradle-plugin-development/src/archRules/java/com/netflix/nebula/archrules/gradleplugins/GradleTaskCacheabilityRule.java b/archrules-gradle-plugin-development/src/archRules/java/com/netflix/nebula/archrules/gradleplugins/GradleTaskCacheabilityRule.java
new file mode 100644
index 0000000..c8f7c94
--- /dev/null
+++ b/archrules-gradle-plugin-development/src/archRules/java/com/netflix/nebula/archrules/gradleplugins/GradleTaskCacheabilityRule.java
@@ -0,0 +1,115 @@
+package com.netflix.nebula.archrules.gradleplugins;
+
+import com.netflix.nebula.archrules.core.ArchRulesService;
+import com.tngtech.archunit.core.domain.JavaClass;
+import com.tngtech.archunit.core.domain.JavaField;
+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 to ensure cacheable tasks properly declare path sensitivity.
+ *
+ * Cacheable tasks must declare how file paths should be compared for cache key calculation.
+ */
+@NullMarked
+public class GradleTaskCacheabilityRule implements ArchRulesService {
+
+ private static final String ANNOTATION_CACHEABLE_TASK = "org.gradle.api.tasks.CacheableTask";
+ private static final String ANNOTATION_INPUT_FILE = "org.gradle.api.tasks.InputFile";
+ private static final String ANNOTATION_INPUT_FILES = "org.gradle.api.tasks.InputFiles";
+ private static final String ANNOTATION_INPUT_DIRECTORY = "org.gradle.api.tasks.InputDirectory";
+ private static final String ANNOTATION_PATH_SENSITIVE = "org.gradle.api.tasks.PathSensitive";
+
+ /**
+ * Ensures that cacheable tasks declare path sensitivity on file inputs.
+ *
+ * Cacheable tasks with file inputs must specify {@code @PathSensitive} to define
+ * how file paths affect cache keys. Without this, tasks may not be relocatable
+ * across different machines, breaking the build cache.
+ */
+ public static final ArchRule cacheableTasksShouldDeclarePathSensitivity = ArchRuleDefinition.priority(Priority.HIGH)
+ .classes()
+ .that().areAnnotatedWith(ANNOTATION_CACHEABLE_TASK)
+ .should(declarePathSensitivityOnFileInputs())
+ .allowEmptyShould(true)
+ .because(
+ "Cacheable tasks with file inputs must declare @PathSensitive to specify how paths " +
+ "affect cache keys. This ensures build cache entries are relocatable across machines. " +
+ "See https://docs.gradle.org/current/userguide/build_cache.html#sec:task_output_caching_inputs"
+ );
+
+ private static ArchCondition declarePathSensitivityOnFileInputs() {
+ return new ArchCondition("declare @PathSensitive on file inputs") {
+ @Override
+ public void check(JavaClass taskClass, ConditionEvents events) {
+ for (JavaField field : taskClass.getAllFields()) {
+ checkFieldPathSensitivity(taskClass, field, events);
+ }
+
+ for (JavaMethod method : taskClass.getAllMethods()) {
+ checkMethodPathSensitivity(taskClass, method, events);
+ }
+ }
+
+ private void checkFieldPathSensitivity(JavaClass taskClass, JavaField field, ConditionEvents events) {
+ if (!hasFileInputAnnotation(field)) {
+ return;
+ }
+
+ if (!field.isAnnotatedWith(ANNOTATION_PATH_SENSITIVE)) {
+ String message = String.format(
+ "Cacheable task %s has field '%s' with file input annotation but missing @PathSensitive. " +
+ "Add @PathSensitive to specify how file paths affect cache keys.",
+ taskClass.getSimpleName(),
+ field.getName()
+ );
+ events.add(SimpleConditionEvent.violated(field, message));
+ }
+ }
+
+ private void checkMethodPathSensitivity(JavaClass taskClass, JavaMethod method, ConditionEvents events) {
+ if (!hasFileInputAnnotation(method)) {
+ return;
+ }
+
+ if (!method.isAnnotatedWith(ANNOTATION_PATH_SENSITIVE)) {
+ String message = String.format(
+ "Cacheable task %s has method '%s()' with file input annotation but missing @PathSensitive. " +
+ "Add @PathSensitive to specify how file paths affect cache keys.",
+ taskClass.getSimpleName(),
+ method.getName()
+ );
+ events.add(SimpleConditionEvent.violated(method, message));
+ }
+ }
+
+ private boolean hasFileInputAnnotation(JavaField field) {
+ return field.isAnnotatedWith(ANNOTATION_INPUT_FILE) ||
+ field.isAnnotatedWith(ANNOTATION_INPUT_FILES) ||
+ field.isAnnotatedWith(ANNOTATION_INPUT_DIRECTORY);
+ }
+
+ private boolean hasFileInputAnnotation(JavaMethod method) {
+ return method.isAnnotatedWith(ANNOTATION_INPUT_FILE) ||
+ method.isAnnotatedWith(ANNOTATION_INPUT_FILES) ||
+ method.isAnnotatedWith(ANNOTATION_INPUT_DIRECTORY);
+ }
+ };
+ }
+
+ @Override
+ public Map getRules() {
+ Map rules = new HashMap<>();
+ rules.put("gradle-task-cacheable-path-sensitivity", cacheableTasksShouldDeclarePathSensitivity);
+ return rules;
+ }
+}
diff --git a/archrules-gradle-plugin-development/src/archRules/java/com/netflix/nebula/archrules/gradleplugins/GradleTaskInputOutputRule.java b/archrules-gradle-plugin-development/src/archRules/java/com/netflix/nebula/archrules/gradleplugins/GradleTaskInputOutputRule.java
new file mode 100644
index 0000000..b8ea093
--- /dev/null
+++ b/archrules-gradle-plugin-development/src/archRules/java/com/netflix/nebula/archrules/gradleplugins/GradleTaskInputOutputRule.java
@@ -0,0 +1,136 @@
+package com.netflix.nebula.archrules.gradleplugins;
+
+import com.netflix.nebula.archrules.core.ArchRulesService;
+import com.tngtech.archunit.core.domain.JavaClass;
+import com.tngtech.archunit.core.domain.JavaField;
+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.Arrays;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Rules to ensure Gradle tasks properly declare their inputs and outputs.
+ *
+ * Tasks must declare inputs and outputs for incremental builds and caching to work correctly.
+ */
+@NullMarked
+public class GradleTaskInputOutputRule implements ArchRulesService {
+
+ private static final String ANNOTATION_INPUT = "org.gradle.api.tasks.Input";
+ private static final String ANNOTATION_INPUT_FILE = "org.gradle.api.tasks.InputFile";
+ private static final String ANNOTATION_INPUT_FILES = "org.gradle.api.tasks.InputFiles";
+ private static final String ANNOTATION_INPUT_DIRECTORY = "org.gradle.api.tasks.InputDirectory";
+ private static final String ANNOTATION_OUTPUT_FILE = "org.gradle.api.tasks.OutputFile";
+ private static final String ANNOTATION_OUTPUT_FILES = "org.gradle.api.tasks.OutputFiles";
+ private static final String ANNOTATION_OUTPUT_DIRECTORY = "org.gradle.api.tasks.OutputDirectory";
+ private static final String ANNOTATION_OUTPUT_DIRECTORIES = "org.gradle.api.tasks.OutputDirectories";
+ private static final String ANNOTATION_TASK_ACTION = "org.gradle.api.tasks.TaskAction";
+
+ private static class LazyHolder {
+ private static final Set INPUT_OUTPUT_ANNOTATIONS = new HashSet<>(Arrays.asList(
+ ANNOTATION_INPUT,
+ ANNOTATION_INPUT_FILE,
+ ANNOTATION_INPUT_FILES,
+ ANNOTATION_INPUT_DIRECTORY,
+ ANNOTATION_OUTPUT_FILE,
+ ANNOTATION_OUTPUT_FILES,
+ ANNOTATION_OUTPUT_DIRECTORY,
+ ANNOTATION_OUTPUT_DIRECTORIES
+ ));
+ }
+
+ private static Set getInputOutputAnnotations() {
+ return LazyHolder.INPUT_OUTPUT_ANNOTATIONS;
+ }
+
+ /**
+ * Ensures that task classes declare at least one input or output.
+ *
+ * Tasks without declared inputs/outputs cannot participate in incremental builds
+ * or build caching, which significantly impacts build performance.
+ */
+ public static final ArchRule tasksShouldDeclareInputsOrOutputs = ArchRuleDefinition.priority(Priority.HIGH)
+ .classes()
+ .that().areAssignableTo("org.gradle.api.DefaultTask")
+ .and().areNotInterfaces()
+ .and().doNotHaveSimpleName("DefaultTask")
+ .should(declareInputsOrOutputs())
+ .allowEmptyShould(true)
+ .because(
+ "Tasks must declare inputs and outputs using @Input, @InputFile, @InputDirectory, " +
+ "@Output, @OutputFile, or @OutputDirectory annotations. " +
+ "This is required for incremental builds and caching to work correctly. " +
+ "See https://docs.gradle.org/current/userguide/incremental_build.html"
+ );
+
+ private static ArchCondition declareInputsOrOutputs() {
+ return new ArchCondition("declare at least one input or output") {
+ @Override
+ public void check(JavaClass taskClass, ConditionEvents events) {
+ if (!hasTaskAction(taskClass)) {
+ return;
+ }
+
+ boolean hasInputOrOutput = hasInputOutputAnnotation(taskClass);
+
+ if (!hasInputOrOutput) {
+ String message = String.format(
+ "Task %s has @TaskAction method(s) but no declared inputs or outputs. " +
+ "Add @Input, @InputFile, @InputDirectory, @Output, @OutputFile, or @OutputDirectory " +
+ "annotations to enable incremental builds and caching.",
+ taskClass.getSimpleName()
+ );
+ events.add(SimpleConditionEvent.violated(taskClass, message));
+ }
+ }
+
+ private boolean hasTaskAction(JavaClass taskClass) {
+ return taskClass.getAllMethods().stream()
+ .anyMatch(method -> method.isAnnotatedWith(ANNOTATION_TASK_ACTION));
+ }
+
+ private boolean hasInputOutputAnnotation(JavaClass taskClass) {
+ for (JavaField field : taskClass.getAllFields()) {
+ if (hasAnyInputOutputAnnotation(field)) {
+ return true;
+ }
+ }
+
+ for (JavaMethod method : taskClass.getAllMethods()) {
+ if (hasAnyInputOutputAnnotation(method)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private boolean hasAnyInputOutputAnnotation(JavaField field) {
+ return getInputOutputAnnotations().stream()
+ .anyMatch(field::isAnnotatedWith);
+ }
+
+ private boolean hasAnyInputOutputAnnotation(JavaMethod method) {
+ return getInputOutputAnnotations().stream()
+ .anyMatch(method::isAnnotatedWith);
+ }
+ };
+ }
+
+ @Override
+ public Map getRules() {
+ Map rules = new HashMap<>();
+ rules.put("gradle-task-input-output-declaration", tasksShouldDeclareInputsOrOutputs);
+ return rules;
+ }
+}
diff --git a/archrules-gradle-plugin-development/src/archRulesTest/java/com/netflix/nebula/archrules/gradleplugins/GradleDeprecatedApiRuleTest.java b/archrules-gradle-plugin-development/src/archRulesTest/java/com/netflix/nebula/archrules/gradleplugins/GradleDeprecatedApiRuleTest.java
new file mode 100644
index 0000000..bb09ada
--- /dev/null
+++ b/archrules-gradle-plugin-development/src/archRulesTest/java/com/netflix/nebula/archrules/gradleplugins/GradleDeprecatedApiRuleTest.java
@@ -0,0 +1,56 @@
+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.Plugin;
+import org.gradle.api.Project;
+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 GradleDeprecatedApiRuleTest {
+ private static final Logger LOG = LoggerFactory.getLogger(GradleDeprecatedApiRuleTest.class);
+
+ @Test
+ public void pluginNotUsingDeprecatedApis_should_pass() {
+ final EvaluationResult result = Runner.check(
+ GradleDeprecatedApiRule.pluginsShouldNotUseDeprecatedGradleApis,
+ PluginNotUsingDeprecatedApis.class
+ );
+ LOG.info(result.getFailureReport().toString());
+ assertThat(result.hasViolation()).isFalse();
+ }
+
+ @Test
+ public void taskNotUsingDeprecatedApis_should_pass() {
+ final EvaluationResult result = Runner.check(
+ GradleDeprecatedApiRule.tasksShouldNotUseDeprecatedGradleApis,
+ TaskNotUsingDeprecatedApis.class
+ );
+ LOG.info(result.getFailureReport().toString());
+ assertThat(result.hasViolation()).isFalse();
+ }
+
+ @SuppressWarnings("unused")
+ public static class PluginNotUsingDeprecatedApis implements Plugin {
+ @Override
+ public void apply(Project project) {
+ project.getTasks().register("myTask", task -> {
+ task.setGroup("custom");
+ task.setDescription("My custom task");
+ });
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public static abstract class TaskNotUsingDeprecatedApis extends DefaultTask {
+ @TaskAction
+ public void execute() {
+ System.out.println("Task executed without deprecated APIs");
+ }
+ }
+}
diff --git a/archrules-gradle-plugin-development/src/archRulesTest/java/com/netflix/nebula/archrules/gradleplugins/GradleInternalApiRuleTest.java b/archrules-gradle-plugin-development/src/archRulesTest/java/com/netflix/nebula/archrules/gradleplugins/GradleInternalApiRuleTest.java
new file mode 100644
index 0000000..383f4a2
--- /dev/null
+++ b/archrules-gradle-plugin-development/src/archRulesTest/java/com/netflix/nebula/archrules/gradleplugins/GradleInternalApiRuleTest.java
@@ -0,0 +1,129 @@
+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.Plugin;
+import org.gradle.api.Project;
+import org.gradle.api.internal.project.ProjectInternal;
+import org.gradle.api.provider.Property;
+import org.gradle.api.tasks.TaskAction;
+import org.gradle.internal.service.ServiceRegistry;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class GradleInternalApiRuleTest {
+ private static final Logger LOG = LoggerFactory.getLogger(GradleInternalApiRuleTest.class);
+
+ @Test
+ public void pluginNotUsingInternalApis_should_pass() {
+ final EvaluationResult result = Runner.check(
+ GradleInternalApiRule.pluginsShouldNotUseInternalGradleApis,
+ PluginNotUsingInternalApis.class
+ );
+ LOG.info(result.getFailureReport().toString());
+ assertThat(result.hasViolation()).isFalse();
+ }
+
+ @Test
+ public void taskNotUsingInternalApis_should_pass() {
+ final EvaluationResult result = Runner.check(
+ GradleInternalApiRule.tasksShouldNotUseInternalGradleApis,
+ TaskNotUsingInternalApis.class
+ );
+ LOG.info(result.getFailureReport().toString());
+ assertThat(result.hasViolation()).isFalse();
+ }
+
+ @Test
+ public void pluginUsingPublicGradleApis_should_pass() {
+ final EvaluationResult result = Runner.check(
+ GradleInternalApiRule.pluginsShouldNotUseInternalGradleApis,
+ PluginUsingPublicGradleApis.class
+ );
+ LOG.info(result.getFailureReport().toString());
+ assertThat(result.hasViolation()).isFalse();
+ }
+
+ @Test
+ public void pluginUsingInternalApi_should_fail() {
+ final EvaluationResult result = Runner.check(
+ GradleInternalApiRule.pluginsShouldNotUseInternalGradleApis,
+ PluginUsingInternalApi.class
+ );
+ LOG.info(result.getFailureReport().toString());
+ assertThat(result.hasViolation()).isTrue();
+ assertThat(result.getFailureReport().toString()).contains("internal Gradle API");
+ assertThat(result.getFailureReport().toString()).contains(".internal.");
+ }
+
+ @Test
+ public void taskUsingInternalApi_should_fail() {
+ final EvaluationResult result = Runner.check(
+ GradleInternalApiRule.tasksShouldNotUseInternalGradleApis,
+ TaskUsingInternalApi.class
+ );
+ LOG.info(result.getFailureReport().toString());
+ assertThat(result.hasViolation()).isTrue();
+ assertThat(result.getFailureReport().toString()).contains("internal Gradle API");
+ }
+
+ @SuppressWarnings("unused")
+ public static class PluginNotUsingInternalApis implements Plugin {
+ @Override
+ public void apply(Project project) {
+ project.getTasks().register("myTask", task -> {
+ task.setGroup("custom");
+ task.setDescription("My custom task");
+ });
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public static class PluginUsingPublicGradleApis implements Plugin {
+ @Override
+ public void apply(Project project) {
+ project.getTasks().register("myTask");
+ project.getExtensions().create("myExtension", MyExtension.class);
+ String version = project.getVersion().toString();
+ System.out.println("Project version: " + version);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public static abstract class TaskNotUsingInternalApis extends DefaultTask {
+ @TaskAction
+ public void execute() {
+ String taskName = getName();
+ System.out.println("Executing task: " + taskName);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public static class PluginUsingInternalApi implements Plugin {
+ @Override
+ public void apply(Project project) {
+ ProjectInternal projectInternal = (ProjectInternal) project;
+ ServiceRegistry services = projectInternal.getServices();
+ System.out.println("Using internal API: " + services);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public static abstract class TaskUsingInternalApi extends DefaultTask {
+ @TaskAction
+ public void execute() {
+ ProjectInternal projectInternal = (ProjectInternal) getProject();
+ ServiceRegistry services = projectInternal.getServices();
+ System.out.println("Using internal API: " + services);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public static abstract class MyExtension {
+ public abstract Property getValue();
+ }
+}
diff --git a/archrules-gradle-plugin-development/src/archRulesTest/java/com/netflix/nebula/archrules/gradleplugins/GradleTaskCacheabilityRuleTest.java b/archrules-gradle-plugin-development/src/archRulesTest/java/com/netflix/nebula/archrules/gradleplugins/GradleTaskCacheabilityRuleTest.java
new file mode 100644
index 0000000..e19e9ab
--- /dev/null
+++ b/archrules-gradle-plugin-development/src/archRulesTest/java/com/netflix/nebula/archrules/gradleplugins/GradleTaskCacheabilityRuleTest.java
@@ -0,0 +1,164 @@
+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.file.RegularFileProperty;
+import org.gradle.api.tasks.CacheableTask;
+import org.gradle.api.tasks.InputFile;
+import org.gradle.api.tasks.OutputFile;
+import org.gradle.api.tasks.PathSensitive;
+import org.gradle.api.tasks.PathSensitivity;
+import org.gradle.api.tasks.TaskAction;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class GradleTaskCacheabilityRuleTest {
+ private static final Logger LOG = LoggerFactory.getLogger(GradleTaskCacheabilityRuleTest.class);
+
+ @Test
+ public void cacheableTaskWithoutPathSensitive_should_fail() {
+ final EvaluationResult result = Runner.check(
+ GradleTaskCacheabilityRule.cacheableTasksShouldDeclarePathSensitivity,
+ CacheableTaskWithoutPathSensitive.class
+ );
+ LOG.info(result.getFailureReport().toString());
+ assertThat(result.hasViolation()).isTrue();
+ assertThat(result.getFailureReport().toString()).contains("missing @PathSensitive");
+ }
+
+ @Test
+ public void cacheableTaskWithPathSensitive_should_pass() {
+ final EvaluationResult result = Runner.check(
+ GradleTaskCacheabilityRule.cacheableTasksShouldDeclarePathSensitivity,
+ CacheableTaskWithPathSensitive.class
+ );
+ LOG.info(result.getFailureReport().toString());
+ assertThat(result.hasViolation()).isFalse();
+ }
+
+ @Test
+ public void cacheableTaskWithInputFileMethodMissingPathSensitive_should_fail() {
+ final EvaluationResult result = Runner.check(
+ GradleTaskCacheabilityRule.cacheableTasksShouldDeclarePathSensitivity,
+ CacheableTaskWithInputFileMethodMissingPathSensitive.class
+ );
+ LOG.info(result.getFailureReport().toString());
+ assertThat(result.hasViolation()).isTrue();
+ assertThat(result.getFailureReport().toString()).contains("missing @PathSensitive");
+ }
+
+ @Test
+ public void cacheableTaskWithInputFileMethodWithPathSensitive_should_pass() {
+ final EvaluationResult result = Runner.check(
+ GradleTaskCacheabilityRule.cacheableTasksShouldDeclarePathSensitivity,
+ CacheableTaskWithInputFileMethodWithPathSensitive.class
+ );
+ LOG.info(result.getFailureReport().toString());
+ assertThat(result.hasViolation()).isFalse();
+ }
+
+ @Test
+ public void cacheableTaskWithOnlyOutputs_should_pass() {
+ final EvaluationResult result = Runner.check(
+ GradleTaskCacheabilityRule.cacheableTasksShouldDeclarePathSensitivity,
+ CacheableTaskWithOnlyOutputs.class
+ );
+ LOG.info(result.getFailureReport().toString());
+ assertThat(result.hasViolation()).isFalse();
+ }
+
+ @Test
+ public void nonCacheableTaskWithoutPathSensitive_should_pass() {
+ final EvaluationResult result = Runner.check(
+ GradleTaskCacheabilityRule.cacheableTasksShouldDeclarePathSensitivity,
+ NonCacheableTaskWithoutPathSensitive.class
+ );
+ LOG.info(result.getFailureReport().toString());
+ assertThat(result.hasViolation()).isFalse();
+ }
+
+ @SuppressWarnings("unused")
+ @CacheableTask
+ public static abstract class CacheableTaskWithoutPathSensitive extends DefaultTask {
+ @InputFile
+ public File inputFile;
+
+ @OutputFile
+ public File outputFile;
+
+ @TaskAction
+ public void execute() {
+ System.out.println("Processing");
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @CacheableTask
+ public static abstract class CacheableTaskWithPathSensitive extends DefaultTask {
+ @InputFile
+ @PathSensitive(PathSensitivity.RELATIVE)
+ public File inputFile;
+
+ @OutputFile
+ public File outputFile;
+
+ @TaskAction
+ public void execute() {
+ System.out.println("Processing");
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @CacheableTask
+ public static abstract class CacheableTaskWithInputFileMethodMissingPathSensitive extends DefaultTask {
+ @InputFile
+ public abstract RegularFileProperty getInputFile();
+
+ @TaskAction
+ public void execute() {
+ System.out.println("Processing");
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @CacheableTask
+ public static abstract class CacheableTaskWithInputFileMethodWithPathSensitive extends DefaultTask {
+ @InputFile
+ @PathSensitive(PathSensitivity.RELATIVE)
+ public abstract RegularFileProperty getInputFile();
+
+ @TaskAction
+ public void execute() {
+ System.out.println("Processing");
+ }
+ }
+
+ @SuppressWarnings("unused")
+ @CacheableTask
+ public static abstract class CacheableTaskWithOnlyOutputs extends DefaultTask {
+ @OutputFile
+ public abstract RegularFileProperty getOutputFile();
+
+ @TaskAction
+ public void execute() {
+ System.out.println("Generating output");
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public static abstract class NonCacheableTaskWithoutPathSensitive extends DefaultTask {
+ @InputFile
+ public File inputFile;
+
+ @TaskAction
+ public void execute() {
+ System.out.println("Processing");
+ }
+ }
+}
diff --git a/archrules-gradle-plugin-development/src/archRulesTest/java/com/netflix/nebula/archrules/gradleplugins/GradleTaskInputOutputRuleTest.java b/archrules-gradle-plugin-development/src/archRulesTest/java/com/netflix/nebula/archrules/gradleplugins/GradleTaskInputOutputRuleTest.java
new file mode 100644
index 0000000..fd7125f
--- /dev/null
+++ b/archrules-gradle-plugin-development/src/archRulesTest/java/com/netflix/nebula/archrules/gradleplugins/GradleTaskInputOutputRuleTest.java
@@ -0,0 +1,119 @@
+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.tasks.Input;
+import org.gradle.api.tasks.InputFile;
+import org.gradle.api.tasks.OutputFile;
+import org.gradle.api.tasks.TaskAction;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class GradleTaskInputOutputRuleTest {
+ private static final Logger LOG = LoggerFactory.getLogger(GradleTaskInputOutputRuleTest.class);
+
+ @Test
+ public void taskWithoutInputOutput_should_fail() {
+ final EvaluationResult result = Runner.check(
+ GradleTaskInputOutputRule.tasksShouldDeclareInputsOrOutputs,
+ TaskWithoutInputOutput.class
+ );
+ LOG.info(result.getFailureReport().toString());
+ assertThat(result.hasViolation()).isTrue();
+ assertThat(result.getFailureReport().toString()).contains("no declared inputs or outputs");
+ }
+
+ @Test
+ public void taskWithInputAnnotation_should_pass() {
+ final EvaluationResult result = Runner.check(
+ GradleTaskInputOutputRule.tasksShouldDeclareInputsOrOutputs,
+ TaskWithInputAnnotation.class
+ );
+ LOG.info(result.getFailureReport().toString());
+ assertThat(result.hasViolation()).isFalse();
+ }
+
+ @Test
+ public void taskWithInputFileAnnotation_should_pass() {
+ final EvaluationResult result = Runner.check(
+ GradleTaskInputOutputRule.tasksShouldDeclareInputsOrOutputs,
+ TaskWithInputFileAnnotation.class
+ );
+ LOG.info(result.getFailureReport().toString());
+ assertThat(result.hasViolation()).isFalse();
+ }
+
+ @Test
+ public void taskWithOutputAnnotation_should_pass() {
+ final EvaluationResult result = Runner.check(
+ GradleTaskInputOutputRule.tasksShouldDeclareInputsOrOutputs,
+ TaskWithOutputAnnotation.class
+ );
+ LOG.info(result.getFailureReport().toString());
+ assertThat(result.hasViolation()).isFalse();
+ }
+
+ @Test
+ public void taskWithoutTaskAction_should_pass() {
+ final EvaluationResult result = Runner.check(
+ GradleTaskInputOutputRule.tasksShouldDeclareInputsOrOutputs,
+ TaskWithoutTaskAction.class
+ );
+ LOG.info(result.getFailureReport().toString());
+ assertThat(result.hasViolation()).isFalse();
+ }
+
+ @SuppressWarnings("unused")
+ public static abstract class TaskWithoutInputOutput extends DefaultTask {
+ @TaskAction
+ public void execute() {
+ System.out.println("Task executed without inputs/outputs");
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public static abstract class TaskWithInputAnnotation extends DefaultTask {
+ @Input
+ public String message;
+
+ @TaskAction
+ public void execute() {
+ System.out.println(message);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public static abstract class TaskWithInputFileAnnotation extends DefaultTask {
+ @InputFile
+ public File inputFile;
+
+ @TaskAction
+ public void execute() {
+ System.out.println("Processing: " + inputFile);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public static abstract class TaskWithOutputAnnotation extends DefaultTask {
+ @OutputFile
+ public File outputFile;
+
+ @TaskAction
+ public void execute() {
+ System.out.println("Writing to: " + outputFile);
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public static abstract class TaskWithoutTaskAction extends DefaultTask {
+ public void someMethod() {
+ System.out.println("Not a task action");
+ }
+ }
+}