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"); + } + } +}