Skip to content

Commit efc2f78

Browse files
authored
gradle plugin development rules (#27)
* gradle-plugin-development: add rules for task input/output declarations, cacheable task path sensitivity, and deprecated API usage * gradle-plugin-development: Add GradleInternalApiRule to detect and prevent usage of internal Gradle APIs
1 parent d4c7f6b commit efc2f78

File tree

8 files changed

+919
-0
lines changed

8 files changed

+919
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
package com.netflix.nebula.archrules.gradleplugins;
2+
3+
import com.netflix.nebula.archrules.core.ArchRulesService;
4+
import com.tngtech.archunit.core.domain.JavaAccess;
5+
import com.tngtech.archunit.core.domain.JavaClass;
6+
import com.tngtech.archunit.lang.ArchCondition;
7+
import com.tngtech.archunit.lang.ArchRule;
8+
import com.tngtech.archunit.lang.ConditionEvents;
9+
import com.tngtech.archunit.lang.Priority;
10+
import com.tngtech.archunit.lang.SimpleConditionEvent;
11+
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
12+
import org.jspecify.annotations.NullMarked;
13+
14+
import java.util.HashMap;
15+
import java.util.Map;
16+
17+
/**
18+
* Rules to prevent usage of deprecated Gradle APIs.
19+
* <p>
20+
* Using deprecated Gradle APIs will cause build failures in future Gradle versions.
21+
*/
22+
@NullMarked
23+
public class GradleDeprecatedApiRule implements ArchRulesService {
24+
25+
private static final String GRADLE_API_PACKAGE = "org.gradle";
26+
27+
/**
28+
* Prevents plugins from using deprecated Gradle APIs.
29+
* <p>
30+
* Deprecated Gradle APIs will be removed in future versions, causing build failures.
31+
* Replace deprecated APIs with their modern equivalents as documented in Gradle's
32+
* upgrade guides.
33+
*/
34+
public static final ArchRule pluginsShouldNotUseDeprecatedGradleApis = ArchRuleDefinition.priority(Priority.MEDIUM)
35+
.classes()
36+
.that().implement("org.gradle.api.Plugin")
37+
.should(notUseDeprecatedGradleApis())
38+
.allowEmptyShould(true)
39+
.because(
40+
"Plugins should not use deprecated Gradle APIs as they will be removed in future versions. " +
41+
"Consult Gradle upgrade guides for modern alternatives. " +
42+
"See https://docs.gradle.org/current/userguide/upgrading_version_8.html"
43+
);
44+
45+
/**
46+
* Prevents tasks from using deprecated Gradle APIs.
47+
* <p>
48+
* Deprecated Gradle APIs will be removed in future versions, causing build failures.
49+
* Replace deprecated APIs with their modern equivalents as documented in Gradle's
50+
* upgrade guides.
51+
*/
52+
public static final ArchRule tasksShouldNotUseDeprecatedGradleApis = ArchRuleDefinition.priority(Priority.MEDIUM)
53+
.classes()
54+
.that().areAssignableTo("org.gradle.api.Task")
55+
.and().areNotInterfaces()
56+
.should(notUseDeprecatedGradleApis())
57+
.allowEmptyShould(true)
58+
.because(
59+
"Tasks should not use deprecated Gradle APIs as they will be removed in future versions. " +
60+
"Consult Gradle upgrade guides for modern alternatives. " +
61+
"See https://docs.gradle.org/current/userguide/upgrading_version_8.html"
62+
);
63+
64+
private static ArchCondition<JavaClass> notUseDeprecatedGradleApis() {
65+
return new ArchCondition<JavaClass>("not use deprecated Gradle APIs") {
66+
@Override
67+
public void check(JavaClass javaClass, ConditionEvents events) {
68+
for (JavaAccess<?> access : javaClass.getAccessesFromSelf()) {
69+
if (isDeprecatedGradleApi(access)) {
70+
String message = String.format(
71+
"Class %s uses deprecated Gradle API: %s. " +
72+
"This API will be removed in a future Gradle version. " +
73+
"Consult Gradle upgrade guides for alternatives.",
74+
javaClass.getSimpleName(),
75+
access.getDescription()
76+
);
77+
events.add(SimpleConditionEvent.violated(access, message));
78+
}
79+
}
80+
}
81+
82+
private boolean isDeprecatedGradleApi(JavaAccess<?> access) {
83+
String targetOwnerName = access.getTargetOwner().getName();
84+
85+
if (!targetOwnerName.startsWith(GRADLE_API_PACKAGE)) {
86+
return false;
87+
}
88+
89+
return access.getTarget().isAnnotatedWith(Deprecated.class);
90+
}
91+
};
92+
}
93+
94+
@Override
95+
public Map<String, ArchRule> getRules() {
96+
Map<String, ArchRule> rules = new HashMap<>();
97+
rules.put("gradle-plugin-no-deprecated-apis", pluginsShouldNotUseDeprecatedGradleApis);
98+
rules.put("gradle-task-no-deprecated-apis", tasksShouldNotUseDeprecatedGradleApis);
99+
return rules;
100+
}
101+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
package com.netflix.nebula.archrules.gradleplugins;
2+
3+
import com.netflix.nebula.archrules.core.ArchRulesService;
4+
import com.tngtech.archunit.core.domain.JavaAccess;
5+
import com.tngtech.archunit.core.domain.JavaClass;
6+
import com.tngtech.archunit.lang.ArchCondition;
7+
import com.tngtech.archunit.lang.ArchRule;
8+
import com.tngtech.archunit.lang.ConditionEvents;
9+
import com.tngtech.archunit.lang.Priority;
10+
import com.tngtech.archunit.lang.SimpleConditionEvent;
11+
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
12+
import org.jspecify.annotations.NullMarked;
13+
14+
import java.util.HashMap;
15+
import java.util.Map;
16+
17+
/**
18+
* Rules to prevent usage of internal Gradle APIs.
19+
* <p>
20+
* Internal Gradle APIs are not part of the public API contract and may change
21+
* or be removed without notice between Gradle versions.
22+
*/
23+
@NullMarked
24+
public class GradleInternalApiRule implements ArchRulesService {
25+
26+
private static final String GRADLE_PACKAGE = "org.gradle";
27+
private static final String INTERNAL_PACKAGE_MARKER = ".internal.";
28+
29+
/**
30+
* Prevents plugins from using internal Gradle APIs.
31+
* <p>
32+
* Internal Gradle APIs (packages containing {@code .internal.}) are not stable
33+
* and may change or be removed between versions without notice. Use only public
34+
* Gradle APIs to ensure compatibility across Gradle versions.
35+
*/
36+
public static final ArchRule pluginsShouldNotUseInternalGradleApis = ArchRuleDefinition.priority(Priority.HIGH)
37+
.classes()
38+
.that().implement("org.gradle.api.Plugin")
39+
.should(notUseInternalGradleApis())
40+
.allowEmptyShould(true)
41+
.because(
42+
"Plugins should not use internal Gradle APIs (packages containing '.internal.'). " +
43+
"Internal APIs are not stable and may change or be removed without notice. " +
44+
"Use only public Gradle APIs documented at https://docs.gradle.org/current/javadoc/"
45+
);
46+
47+
/**
48+
* Prevents tasks from using internal Gradle APIs.
49+
* <p>
50+
* Internal Gradle APIs (packages containing {@code .internal.}) are not stable
51+
* and may change or be removed between versions without notice. Use only public
52+
* Gradle APIs to ensure compatibility across Gradle versions.
53+
*/
54+
public static final ArchRule tasksShouldNotUseInternalGradleApis = ArchRuleDefinition.priority(Priority.HIGH)
55+
.classes()
56+
.that().areAssignableTo("org.gradle.api.Task")
57+
.and().areNotInterfaces()
58+
.should(notUseInternalGradleApis())
59+
.allowEmptyShould(true)
60+
.because(
61+
"Tasks should not use internal Gradle APIs (packages containing '.internal.'). " +
62+
"Internal APIs are not stable and may change or be removed without notice. " +
63+
"Use only public Gradle APIs documented at https://docs.gradle.org/current/javadoc/"
64+
);
65+
66+
private static ArchCondition<JavaClass> notUseInternalGradleApis() {
67+
return new ArchCondition<JavaClass>("not use internal Gradle APIs") {
68+
@Override
69+
public void check(JavaClass javaClass, ConditionEvents events) {
70+
for (JavaAccess<?> access : javaClass.getAccessesFromSelf()) {
71+
if (isInternalGradleApi(access)) {
72+
String message = String.format(
73+
"Class %s uses internal Gradle API: %s. " +
74+
"Internal APIs (packages containing '.internal.') are not stable and may change without notice. " +
75+
"Use public Gradle APIs instead.",
76+
javaClass.getSimpleName(),
77+
access.getDescription()
78+
);
79+
events.add(SimpleConditionEvent.violated(access, message));
80+
}
81+
}
82+
}
83+
84+
private boolean isInternalGradleApi(JavaAccess<?> access) {
85+
String targetPackage = access.getTargetOwner().getPackageName();
86+
return targetPackage.startsWith(GRADLE_PACKAGE) &&
87+
targetPackage.contains(INTERNAL_PACKAGE_MARKER);
88+
}
89+
};
90+
}
91+
92+
@Override
93+
public Map<String, ArchRule> getRules() {
94+
Map<String, ArchRule> rules = new HashMap<>();
95+
rules.put("gradle-plugin-no-internal-apis", pluginsShouldNotUseInternalGradleApis);
96+
rules.put("gradle-task-no-internal-apis", tasksShouldNotUseInternalGradleApis);
97+
return rules;
98+
}
99+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
package com.netflix.nebula.archrules.gradleplugins;
2+
3+
import com.netflix.nebula.archrules.core.ArchRulesService;
4+
import com.tngtech.archunit.core.domain.JavaClass;
5+
import com.tngtech.archunit.core.domain.JavaField;
6+
import com.tngtech.archunit.core.domain.JavaMethod;
7+
import com.tngtech.archunit.lang.ArchCondition;
8+
import com.tngtech.archunit.lang.ArchRule;
9+
import com.tngtech.archunit.lang.ConditionEvents;
10+
import com.tngtech.archunit.lang.Priority;
11+
import com.tngtech.archunit.lang.SimpleConditionEvent;
12+
import com.tngtech.archunit.lang.syntax.ArchRuleDefinition;
13+
import org.jspecify.annotations.NullMarked;
14+
15+
import java.util.HashMap;
16+
import java.util.Map;
17+
18+
/**
19+
* Rules to ensure cacheable tasks properly declare path sensitivity.
20+
* <p>
21+
* Cacheable tasks must declare how file paths should be compared for cache key calculation.
22+
*/
23+
@NullMarked
24+
public class GradleTaskCacheabilityRule implements ArchRulesService {
25+
26+
private static final String ANNOTATION_CACHEABLE_TASK = "org.gradle.api.tasks.CacheableTask";
27+
private static final String ANNOTATION_INPUT_FILE = "org.gradle.api.tasks.InputFile";
28+
private static final String ANNOTATION_INPUT_FILES = "org.gradle.api.tasks.InputFiles";
29+
private static final String ANNOTATION_INPUT_DIRECTORY = "org.gradle.api.tasks.InputDirectory";
30+
private static final String ANNOTATION_PATH_SENSITIVE = "org.gradle.api.tasks.PathSensitive";
31+
32+
/**
33+
* Ensures that cacheable tasks declare path sensitivity on file inputs.
34+
* <p>
35+
* Cacheable tasks with file inputs must specify {@code @PathSensitive} to define
36+
* how file paths affect cache keys. Without this, tasks may not be relocatable
37+
* across different machines, breaking the build cache.
38+
*/
39+
public static final ArchRule cacheableTasksShouldDeclarePathSensitivity = ArchRuleDefinition.priority(Priority.HIGH)
40+
.classes()
41+
.that().areAnnotatedWith(ANNOTATION_CACHEABLE_TASK)
42+
.should(declarePathSensitivityOnFileInputs())
43+
.allowEmptyShould(true)
44+
.because(
45+
"Cacheable tasks with file inputs must declare @PathSensitive to specify how paths " +
46+
"affect cache keys. This ensures build cache entries are relocatable across machines. " +
47+
"See https://docs.gradle.org/current/userguide/build_cache.html#sec:task_output_caching_inputs"
48+
);
49+
50+
private static ArchCondition<JavaClass> declarePathSensitivityOnFileInputs() {
51+
return new ArchCondition<JavaClass>("declare @PathSensitive on file inputs") {
52+
@Override
53+
public void check(JavaClass taskClass, ConditionEvents events) {
54+
for (JavaField field : taskClass.getAllFields()) {
55+
checkFieldPathSensitivity(taskClass, field, events);
56+
}
57+
58+
for (JavaMethod method : taskClass.getAllMethods()) {
59+
checkMethodPathSensitivity(taskClass, method, events);
60+
}
61+
}
62+
63+
private void checkFieldPathSensitivity(JavaClass taskClass, JavaField field, ConditionEvents events) {
64+
if (!hasFileInputAnnotation(field)) {
65+
return;
66+
}
67+
68+
if (!field.isAnnotatedWith(ANNOTATION_PATH_SENSITIVE)) {
69+
String message = String.format(
70+
"Cacheable task %s has field '%s' with file input annotation but missing @PathSensitive. " +
71+
"Add @PathSensitive to specify how file paths affect cache keys.",
72+
taskClass.getSimpleName(),
73+
field.getName()
74+
);
75+
events.add(SimpleConditionEvent.violated(field, message));
76+
}
77+
}
78+
79+
private void checkMethodPathSensitivity(JavaClass taskClass, JavaMethod method, ConditionEvents events) {
80+
if (!hasFileInputAnnotation(method)) {
81+
return;
82+
}
83+
84+
if (!method.isAnnotatedWith(ANNOTATION_PATH_SENSITIVE)) {
85+
String message = String.format(
86+
"Cacheable task %s has method '%s()' with file input annotation but missing @PathSensitive. " +
87+
"Add @PathSensitive to specify how file paths affect cache keys.",
88+
taskClass.getSimpleName(),
89+
method.getName()
90+
);
91+
events.add(SimpleConditionEvent.violated(method, message));
92+
}
93+
}
94+
95+
private boolean hasFileInputAnnotation(JavaField field) {
96+
return field.isAnnotatedWith(ANNOTATION_INPUT_FILE) ||
97+
field.isAnnotatedWith(ANNOTATION_INPUT_FILES) ||
98+
field.isAnnotatedWith(ANNOTATION_INPUT_DIRECTORY);
99+
}
100+
101+
private boolean hasFileInputAnnotation(JavaMethod method) {
102+
return method.isAnnotatedWith(ANNOTATION_INPUT_FILE) ||
103+
method.isAnnotatedWith(ANNOTATION_INPUT_FILES) ||
104+
method.isAnnotatedWith(ANNOTATION_INPUT_DIRECTORY);
105+
}
106+
};
107+
}
108+
109+
@Override
110+
public Map<String, ArchRule> getRules() {
111+
Map<String, ArchRule> rules = new HashMap<>();
112+
rules.put("gradle-task-cacheable-path-sensitivity", cacheableTasksShouldDeclarePathSensitivity);
113+
return rules;
114+
}
115+
}

0 commit comments

Comments
 (0)