Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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<JavaClass> notUseDeprecatedGradleApis() {
return new ArchCondition<JavaClass>("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<String, ArchRule> getRules() {
Map<String, ArchRule> rules = new HashMap<>();
rules.put("gradle-plugin-no-deprecated-apis", pluginsShouldNotUseDeprecatedGradleApis);
rules.put("gradle-task-no-deprecated-apis", tasksShouldNotUseDeprecatedGradleApis);
return rules;
}
}
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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.
* <p>
* 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.
* <p>
* 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<JavaClass> notUseInternalGradleApis() {
return new ArchCondition<JavaClass>("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<String, ArchRule> getRules() {
Map<String, ArchRule> rules = new HashMap<>();
rules.put("gradle-plugin-no-internal-apis", pluginsShouldNotUseInternalGradleApis);
rules.put("gradle-task-no-internal-apis", tasksShouldNotUseInternalGradleApis);
return rules;
}
}
Original file line number Diff line number Diff line change
@@ -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.
* <p>
* 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.
* <p>
* 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<JavaClass> declarePathSensitivityOnFileInputs() {
return new ArchCondition<JavaClass>("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<String, ArchRule> getRules() {
Map<String, ArchRule> rules = new HashMap<>();
rules.put("gradle-task-cacheable-path-sensitivity", cacheableTasksShouldDeclarePathSensitivity);
return rules;
}
}
Loading