Skip to content

Commit 513878e

Browse files
authored
Add Gradle plugin development ArchUnit rules for lazy task registration and Provider API usage with field-specific recommendations (#25)
1 parent db7302b commit 513878e

File tree

4 files changed

+628
-0
lines changed

4 files changed

+628
-0
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
package com.netflix.nebula.archrules.gradleplugins;
2+
3+
import com.netflix.nebula.archrules.core.ArchRulesService;
4+
import com.tngtech.archunit.core.domain.JavaMethodCall;
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.Arrays;
15+
import java.util.HashMap;
16+
import java.util.HashSet;
17+
import java.util.Map;
18+
import java.util.Set;
19+
20+
/**
21+
* Rules for Gradle plugin classes to ensure they use lazy task registration.
22+
* <p>
23+
* Lazy task registration (using {@code tasks.register()}) improves configuration time
24+
* by avoiding eager task creation. This is critical for build performance, especially
25+
* in large multi-module projects.
26+
*/
27+
@NullMarked
28+
public class GradlePluginLazyTaskRegistrationRule implements ArchRulesService {
29+
30+
private static final Set<String> EAGER_TASK_CREATION_METHODS = new HashSet<>(Arrays.asList(
31+
"task",
32+
"create"
33+
));
34+
35+
/**
36+
* Prevents Plugin classes from using eager task creation methods.
37+
* <p>
38+
* Eager task creation (using {@code task()} or {@code tasks.create()}) creates
39+
* all tasks during configuration phase, even if they won't be executed.
40+
* Use {@code tasks.register()} instead for lazy task creation.
41+
*/
42+
public static final ArchRule pluginsShouldUseLazyTaskRegistration = ArchRuleDefinition.priority(Priority.MEDIUM)
43+
.classes()
44+
.that().implement("org.gradle.api.Plugin")
45+
.should(useLazyTaskRegistration())
46+
.allowEmptyShould(true)
47+
.because(
48+
"Plugins should use tasks.register() instead of task() or tasks.create() for lazy task registration. " +
49+
"Eager task creation runs during configuration phase on EVERY build, significantly impacting performance. " +
50+
"Lazy registration with tasks.register() only creates tasks when needed. " +
51+
"See https://docs.gradle.org/current/userguide/task_configuration_avoidance.html"
52+
);
53+
54+
private static ArchCondition<JavaClass> useLazyTaskRegistration() {
55+
return new ArchCondition<JavaClass>("use lazy task registration (tasks.register())") {
56+
@Override
57+
public void check(JavaClass pluginClass, ConditionEvents events) {
58+
pluginClass.getMethodCallsFromSelf().forEach(call -> {
59+
if (isEagerTaskCreation(call)) {
60+
String message = String.format(
61+
"Plugin %s uses eager task creation with %s.%s() at %s. " +
62+
"Use tasks.register() instead for lazy task registration.",
63+
pluginClass.getSimpleName(),
64+
call.getTargetOwner().getSimpleName(),
65+
call.getName(),
66+
call.getDescription()
67+
);
68+
events.add(SimpleConditionEvent.violated(call, message));
69+
}
70+
});
71+
}
72+
73+
private boolean isEagerTaskCreation(JavaMethodCall call) {
74+
String methodName = call.getName();
75+
76+
if (!EAGER_TASK_CREATION_METHODS.contains(methodName)) {
77+
return false;
78+
}
79+
80+
JavaClass targetOwner = call.getTargetOwner();
81+
return targetOwner.isAssignableTo("org.gradle.api.Project") ||
82+
targetOwner.isAssignableTo("org.gradle.api.tasks.TaskContainer");
83+
}
84+
};
85+
}
86+
87+
@Override
88+
public Map<String, ArchRule> getRules() {
89+
Map<String, ArchRule> rules = new HashMap<>();
90+
rules.put("gradle-plugin-lazy-task-registration", pluginsShouldUseLazyTaskRegistration);
91+
return rules;
92+
}
93+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
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.Arrays;
16+
import java.util.HashMap;
17+
import java.util.HashSet;
18+
import java.util.Map;
19+
import java.util.Set;
20+
21+
/**
22+
* Rules for Gradle task classes to ensure they use the Provider API for lazy configuration.
23+
* <p>
24+
* The Provider API ({@code Property<T>}, {@code Provider<T>}, {@code ConfigurableFileCollection})
25+
* enables lazy configuration and configuration avoidance, which are critical for build performance.
26+
*/
27+
@NullMarked
28+
public class GradleTaskProviderApiRule implements ArchRulesService {
29+
30+
private static final String JAVA_IO_FILE = "java.io.File";
31+
private static final String JAVA_LANG_STRING = "java.lang.String";
32+
private static final String JAVA_LANG_INTEGER = "java.lang.Integer";
33+
private static final String JAVA_LANG_LONG = "java.lang.Long";
34+
private static final String JAVA_LANG_BOOLEAN = "java.lang.Boolean";
35+
private static final String JAVA_LANG_DOUBLE = "java.lang.Double";
36+
private static final String JAVA_LANG_FLOAT = "java.lang.Float";
37+
private static final String JAVA_UTIL_LIST = "java.util.List";
38+
private static final String JAVA_UTIL_SET = "java.util.Set";
39+
private static final String JAVA_UTIL_MAP = "java.util.Map";
40+
41+
private static final String ANNOTATION_INPUT = "org.gradle.api.tasks.Input";
42+
private static final String ANNOTATION_INPUT_FILE = "org.gradle.api.tasks.InputFile";
43+
private static final String ANNOTATION_INPUT_FILES = "org.gradle.api.tasks.InputFiles";
44+
private static final String ANNOTATION_INPUT_DIRECTORY = "org.gradle.api.tasks.InputDirectory";
45+
private static final String ANNOTATION_OUTPUT_FILE = "org.gradle.api.tasks.OutputFile";
46+
private static final String ANNOTATION_OUTPUT_FILES = "org.gradle.api.tasks.OutputFiles";
47+
private static final String ANNOTATION_OUTPUT_DIRECTORY = "org.gradle.api.tasks.OutputDirectory";
48+
private static final String ANNOTATION_OUTPUT_DIRECTORIES = "org.gradle.api.tasks.OutputDirectories";
49+
50+
private static final String RECOMMENDATION_REGULAR_FILE_PROPERTY = "RegularFileProperty";
51+
private static final String RECOMMENDATION_DIRECTORY_PROPERTY = "DirectoryProperty";
52+
private static final String RECOMMENDATION_LIST_PROPERTY = "ListProperty<T>";
53+
private static final String RECOMMENDATION_SET_PROPERTY = "SetProperty<T>";
54+
private static final String RECOMMENDATION_MAP_PROPERTY = "MapProperty<K, V>";
55+
56+
private static class LazyHolder {
57+
private static final Set<String> MUTABLE_TYPES_THAT_SHOULD_USE_PROVIDER = new HashSet<>(Arrays.asList(
58+
JAVA_LANG_STRING,
59+
JAVA_LANG_INTEGER,
60+
JAVA_LANG_LONG,
61+
JAVA_LANG_BOOLEAN,
62+
JAVA_LANG_DOUBLE,
63+
JAVA_LANG_FLOAT,
64+
JAVA_IO_FILE,
65+
JAVA_UTIL_LIST,
66+
JAVA_UTIL_SET,
67+
JAVA_UTIL_MAP
68+
));
69+
70+
private static final Map<String, String> TYPE_TO_PROVIDER;
71+
static {
72+
Map<String, String> map = new HashMap<>();
73+
map.put(JAVA_UTIL_LIST, RECOMMENDATION_LIST_PROPERTY);
74+
map.put(JAVA_UTIL_SET, RECOMMENDATION_SET_PROPERTY);
75+
map.put(JAVA_UTIL_MAP, RECOMMENDATION_MAP_PROPERTY);
76+
TYPE_TO_PROVIDER = map;
77+
}
78+
}
79+
80+
private static Set<String> getMutableTypesThatShouldUseProvider() {
81+
return LazyHolder.MUTABLE_TYPES_THAT_SHOULD_USE_PROVIDER;
82+
}
83+
84+
private static Map<String, String> getTypeToProviderMap() {
85+
return LazyHolder.TYPE_TO_PROVIDER;
86+
}
87+
88+
/**
89+
* Detects task input/output properties that should use Provider API types.
90+
* <p>
91+
* Task properties annotated with {@code @Input}, {@code @InputFile}, {@code @InputDirectory},
92+
* {@code @OutputFile}, {@code @OutputDirectory}, etc. should use Provider API types
93+
* ({@code Property<T>}, {@code RegularFileProperty}, {@code DirectoryProperty}, etc.)
94+
* instead of plain types for lazy configuration.
95+
*/
96+
public static final ArchRule taskInputOutputPropertiesShouldUseProviderApi = ArchRuleDefinition.priority(Priority.MEDIUM)
97+
.classes()
98+
.that().areAssignableTo("org.gradle.api.Task")
99+
.and().areNotInterfaces()
100+
.should(useProviderApiForInputOutputProperties())
101+
.allowEmptyShould(true)
102+
.because(
103+
"Task input/output properties should use Provider API types (Property<T>, RegularFileProperty, " +
104+
"DirectoryProperty, ConfigurableFileCollection) instead of plain types. " +
105+
"This enables lazy configuration and configuration avoidance, which significantly improves build performance. " +
106+
"See https://docs.gradle.org/current/userguide/lazy_configuration.html"
107+
);
108+
109+
private static ArchCondition<JavaClass> useProviderApiForInputOutputProperties() {
110+
return new ArchCondition<JavaClass>("use Provider API for input/output properties") {
111+
@Override
112+
public void check(JavaClass taskClass, ConditionEvents events) {
113+
for (JavaField field : taskClass.getAllFields()) {
114+
checkFieldForProviderApiUsage(taskClass, field, events);
115+
}
116+
117+
for (JavaMethod method : taskClass.getAllMethods()) {
118+
checkMethodForProviderApiUsage(taskClass, method, events);
119+
}
120+
}
121+
122+
private void checkFieldForProviderApiUsage(JavaClass taskClass, JavaField field, ConditionEvents events) {
123+
if (!hasInputOutputAnnotation(field)) {
124+
return;
125+
}
126+
127+
if (shouldUseProviderApi(field.getRawType()) && !usesProviderApi(field.getRawType())) {
128+
String recommendation = getSpecificRecommendation(field.getRawType(), field);
129+
String message = String.format(
130+
"Task %s has field '%s' of type %s with input/output annotation. " +
131+
"Use %s for lazy configuration.",
132+
taskClass.getSimpleName(),
133+
field.getName(),
134+
field.getRawType().getSimpleName(),
135+
recommendation
136+
);
137+
events.add(SimpleConditionEvent.violated(field, message));
138+
}
139+
}
140+
141+
private void checkMethodForProviderApiUsage(JavaClass taskClass, JavaMethod method, ConditionEvents events) {
142+
if (!hasInputOutputAnnotation(method)) {
143+
return;
144+
}
145+
146+
if (!method.getName().startsWith("get") || method.getRawParameterTypes().size() > 0) {
147+
return;
148+
}
149+
150+
JavaClass returnType = method.getRawReturnType();
151+
if (shouldUseProviderApi(returnType) && !usesProviderApi(returnType)) {
152+
String recommendation = getSpecificRecommendation(returnType, method);
153+
String message = String.format(
154+
"Task %s has getter '%s()' returning type %s with input/output annotation. " +
155+
"Use %s for lazy configuration.",
156+
taskClass.getSimpleName(),
157+
method.getName(),
158+
returnType.getSimpleName(),
159+
recommendation
160+
);
161+
events.add(SimpleConditionEvent.violated(method, message));
162+
}
163+
}
164+
165+
private boolean hasInputOutputAnnotation(JavaField field) {
166+
return field.isAnnotatedWith(ANNOTATION_INPUT) ||
167+
field.isAnnotatedWith(ANNOTATION_INPUT_FILE) ||
168+
field.isAnnotatedWith(ANNOTATION_INPUT_FILES) ||
169+
field.isAnnotatedWith(ANNOTATION_INPUT_DIRECTORY) ||
170+
field.isAnnotatedWith(ANNOTATION_OUTPUT_FILE) ||
171+
field.isAnnotatedWith(ANNOTATION_OUTPUT_FILES) ||
172+
field.isAnnotatedWith(ANNOTATION_OUTPUT_DIRECTORY) ||
173+
field.isAnnotatedWith(ANNOTATION_OUTPUT_DIRECTORIES);
174+
}
175+
176+
private boolean hasInputOutputAnnotation(JavaMethod method) {
177+
return method.isAnnotatedWith(ANNOTATION_INPUT) ||
178+
method.isAnnotatedWith(ANNOTATION_INPUT_FILE) ||
179+
method.isAnnotatedWith(ANNOTATION_INPUT_FILES) ||
180+
method.isAnnotatedWith(ANNOTATION_INPUT_DIRECTORY) ||
181+
method.isAnnotatedWith(ANNOTATION_OUTPUT_FILE) ||
182+
method.isAnnotatedWith(ANNOTATION_OUTPUT_FILES) ||
183+
method.isAnnotatedWith(ANNOTATION_OUTPUT_DIRECTORY) ||
184+
method.isAnnotatedWith(ANNOTATION_OUTPUT_DIRECTORIES);
185+
}
186+
187+
private boolean shouldUseProviderApi(JavaClass type) {
188+
String typeName = type.getName();
189+
return getMutableTypesThatShouldUseProvider().contains(typeName);
190+
}
191+
192+
private boolean usesProviderApi(JavaClass type) {
193+
return type.isAssignableTo("org.gradle.api.provider.Property") ||
194+
type.isAssignableTo("org.gradle.api.provider.Provider") ||
195+
type.isAssignableTo("org.gradle.api.file.RegularFileProperty") ||
196+
type.isAssignableTo("org.gradle.api.file.DirectoryProperty") ||
197+
type.isAssignableTo("org.gradle.api.file.ConfigurableFileCollection") ||
198+
type.isAssignableTo("org.gradle.api.file.FileCollection");
199+
}
200+
201+
private String getSpecificRecommendation(JavaClass type, JavaField field) {
202+
return getRecommendationForType(
203+
type,
204+
field.isAnnotatedWith(ANNOTATION_INPUT_FILE) || field.isAnnotatedWith(ANNOTATION_OUTPUT_FILE),
205+
field.isAnnotatedWith(ANNOTATION_INPUT_DIRECTORY) || field.isAnnotatedWith(ANNOTATION_OUTPUT_DIRECTORY)
206+
);
207+
}
208+
209+
private String getSpecificRecommendation(JavaClass type, JavaMethod method) {
210+
return getRecommendationForType(
211+
type,
212+
method.isAnnotatedWith(ANNOTATION_INPUT_FILE) || method.isAnnotatedWith(ANNOTATION_OUTPUT_FILE),
213+
method.isAnnotatedWith(ANNOTATION_INPUT_DIRECTORY) || method.isAnnotatedWith(ANNOTATION_OUTPUT_DIRECTORY)
214+
);
215+
}
216+
217+
private String getRecommendationForType(JavaClass type, boolean isFileAnnotation, boolean isDirectoryAnnotation) {
218+
String typeName = type.getName();
219+
220+
if (JAVA_IO_FILE.equals(typeName)) {
221+
if (isFileAnnotation) {
222+
return RECOMMENDATION_REGULAR_FILE_PROPERTY;
223+
}
224+
if (isDirectoryAnnotation) {
225+
return RECOMMENDATION_DIRECTORY_PROPERTY;
226+
}
227+
return RECOMMENDATION_REGULAR_FILE_PROPERTY + " or " + RECOMMENDATION_DIRECTORY_PROPERTY;
228+
}
229+
230+
String mappedRecommendation = getTypeToProviderMap().get(typeName);
231+
if (mappedRecommendation != null) {
232+
return mappedRecommendation;
233+
}
234+
235+
return "Property<" + type.getSimpleName() + ">";
236+
}
237+
};
238+
}
239+
240+
@Override
241+
public Map<String, ArchRule> getRules() {
242+
Map<String, ArchRule> rules = new HashMap<>();
243+
rules.put("gradle-task-provider-api", taskInputOutputPropertiesShouldUseProviderApi);
244+
return rules;
245+
}
246+
}

0 commit comments

Comments
 (0)