Skip to content

Commit 86bec62

Browse files
sullistimtebeek
andauthored
Migrate 'main' property in MigrateToGradle9 (#7504)
* Migrate 'main' property in MigrateToGradle9 * UseMainClassProperty: support Kotlin DSL and tighten hot path Detect `tasks.register<JavaExec>("name")` via `getTypeParameters()` so the recipe also rewrites `main` to `mainClass` in `.gradle.kts` files. Reorder the `visitAssignment` checks so the cheap `instanceof` and simple-name checks run before the cursor-message lookup. * UseMainClassProperty: use Lombok @value fields for displayName and description Matches the pattern used in JacocoReportDeprecations. --------- Co-authored-by: Tim te Beek <tim@moderne.io>
1 parent f68c4fb commit 86bec62

4 files changed

Lines changed: 149 additions & 1 deletion

File tree

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/*
2+
* Copyright 2026 the original author or authors.
3+
* <p>
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
* <p>
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
* <p>
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package org.openrewrite.gradle.gradle9;
17+
18+
import lombok.EqualsAndHashCode;
19+
import lombok.Value;
20+
import org.openrewrite.ExecutionContext;
21+
import org.openrewrite.Preconditions;
22+
import org.openrewrite.Recipe;
23+
import org.openrewrite.TreeVisitor;
24+
import org.openrewrite.gradle.IsBuildGradle;
25+
import org.openrewrite.groovy.tree.G;
26+
import org.openrewrite.java.JavaVisitor;
27+
import org.openrewrite.java.tree.Expression;
28+
import org.openrewrite.java.tree.J;
29+
30+
@Value
31+
@EqualsAndHashCode(callSuper = false)
32+
public class UseMainClassProperty extends Recipe {
33+
34+
private static final String IN_JAVA_EXEC = "IN_JAVA_EXEC";
35+
36+
String displayName = "Use `mainClass` instead of `main` for `JavaExec` tasks";
37+
38+
String description = "The `main` property on `JavaExec` tasks was deprecated in Gradle 7.1 and removed in Gradle 9.0. " +
39+
"Use the `mainClass` property instead. " +
40+
"See the [Gradle upgrade guide](https://docs.gradle.org/9.0.0/userguide/upgrading_major_version_9.html) for more information.";
41+
42+
@Override
43+
public TreeVisitor<?, ExecutionContext> getVisitor() {
44+
return Preconditions.check(new IsBuildGradle<>(), new JavaVisitor<ExecutionContext>() {
45+
@Override
46+
public J visitMethodInvocation(J.MethodInvocation method, ExecutionContext ctx) {
47+
if (hasJavaExecType(method)) {
48+
getCursor().putMessage(IN_JAVA_EXEC, true);
49+
}
50+
return super.visitMethodInvocation(method, ctx);
51+
}
52+
53+
@Override
54+
public J.Assignment visitAssignment(J.Assignment assignment, ExecutionContext ctx) {
55+
Expression variable = assignment.getVariable();
56+
if (variable instanceof J.Identifier) {
57+
J.Identifier id = (J.Identifier) variable;
58+
if ("main".equals(id.getSimpleName()) && getCursor().getNearestMessage(IN_JAVA_EXEC) != null) {
59+
return assignment.withVariable(id.withSimpleName("mainClass"));
60+
}
61+
} else if (variable instanceof J.FieldAccess) {
62+
J.FieldAccess fieldAccess = (J.FieldAccess) variable;
63+
if ("main".equals(fieldAccess.getSimpleName()) && getCursor().getNearestMessage(IN_JAVA_EXEC) != null) {
64+
return assignment.withVariable(
65+
fieldAccess.withName(fieldAccess.getName().withSimpleName("mainClass"))
66+
);
67+
}
68+
}
69+
return assignment;
70+
}
71+
72+
private boolean hasJavaExecType(J.MethodInvocation method) {
73+
// Groovy DSL: task foo(type: JavaExec) { ... }
74+
for (Expression arg : method.getArguments()) {
75+
if (arg instanceof G.MapEntry) {
76+
G.MapEntry entry = (G.MapEntry) arg;
77+
if (isTypeKey(entry.getKey()) && isJavaExecValue(entry.getValue())) {
78+
return true;
79+
}
80+
} else if (isJavaExecValue(arg)) {
81+
return true;
82+
}
83+
}
84+
// Kotlin DSL: tasks.register<JavaExec>("foo") { ... }
85+
if (method.getTypeParameters() != null) {
86+
for (Expression typeParameter : method.getTypeParameters()) {
87+
if (isJavaExecValue(typeParameter)) {
88+
return true;
89+
}
90+
}
91+
}
92+
return false;
93+
}
94+
95+
private boolean isTypeKey(Expression key) {
96+
return key instanceof G.Literal && "type".equals(((G.Literal) key).getValue());
97+
}
98+
99+
private boolean isJavaExecValue(Expression value) {
100+
return value instanceof J.Identifier && "JavaExec".equals(((J.Identifier) value).getSimpleName());
101+
}
102+
});
103+
}
104+
}

rewrite-gradle/src/main/resources/META-INF/rewrite/gradle-9.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,5 @@ recipeList:
2424
version: 9.x
2525
addIfMissing: false
2626
# Map notation has been deprecated in 9.1+
27-
- org.openrewrite.gradle.DependencyUseStringNotation
27+
- org.openrewrite.gradle.DependencyUseStringNotation
28+
- org.openrewrite.gradle.gradle9.UseMainClassProperty

rewrite-gradle/src/main/resources/META-INF/rewrite/recipes.csv

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ maven,org.openrewrite:rewrite-gradle,org.openrewrite.gradle.GradleBestPractices,
5555
maven,org.openrewrite:rewrite-gradle,org.openrewrite.gradle.EnableGradleBuildCache,Enable Gradle build cache,"Enable the Gradle build cache. By enabling build cache the build outputs are stored externally and fetched from the cache when it is determined that those inputs have no changed, avoiding the expensive work of regenerating them. See the [Gradle Build Cache](https://docs.gradle.org/current/userguide/build_cache.html) for more information.",2,,Gradle,,
5656
maven,org.openrewrite:rewrite-gradle,org.openrewrite.gradle.EnableGradleParallelExecution,Enable Gradle parallel execution,"Most builds consist of more than one project and some of those projects are usually independent of one another. Yet Gradle will only run one task at a time by default, regardless of the project structure. By using the `--parallel` switch, you can force Gradle to execute tasks in parallel as long as those tasks are in different projects. See the [Gradle performance documentation](https://docs.gradle.org/current/userguide/performance.html#parallel_execution) for more information.",2,,Gradle,,
5757
maven,org.openrewrite:rewrite-gradle,org.openrewrite.gradle.gradle8.JacocoReportDeprecations,Replace Gradle 8 introduced deprecations in JaCoCo report task,Set the `enabled` to `required` and the `destination` to `outputLocation` for Reports deprecations that were removed in gradle 8. See [the gradle docs on this topic](https://docs.gradle.org/current/userguide/upgrading_version_7.html#report_and_testreport_api_cleanup).,1,Gradle8,Gradle,,
58+
maven,org.openrewrite:rewrite-gradle,org.openrewrite.gradle.gradle9.UseMainClassProperty,Use `mainClass` instead of `main` for `JavaExec` tasks,The `main` property on `JavaExec` tasks was deprecated in Gradle 7.1 and removed in Gradle 9.0. Use the `mainClass` property instead.,1,Gradle9,Gradle,,
5859
maven,org.openrewrite:rewrite-gradle,org.openrewrite.gradle.plugins.AddBuildPlugin,Add Gradle plugin,Add a build plugin to a Gradle build file's `plugins` block.,1,Plugins,Gradle,"[{""name"":""pluginId"",""type"":""String"",""displayName"":""Plugin id"",""description"":""The plugin id to apply."",""example"":""com.jfrog.bintray"",""required"":true},{""name"":""version"",""type"":""String"",""displayName"":""Plugin version"",""description"":""An exact version number or node-style semver selector used to select the version number. You can also use `latest.release` for the latest available version and `latest.patch` if the current version is a valid semantic version. For more details, you can look at the documentation page of [version selectors](https://docs.openrewrite.org/reference/dependency-version-selectors)."",""example"":""3.x""},{""name"":""versionPattern"",""type"":""String"",""displayName"":""Version pattern"",""description"":""Allows version selection to be extended beyond the original Node Semver semantics. So for example,Setting 'version' to \""25-29\"" can be paired with a metadata pattern of \""-jre\"" to select Guava 29.0-jre"",""example"":""-jre""},{""name"":""apply"",""type"":""Boolean"",""displayName"":""Apply plugin"",""description"":""Immediate apply the plugin. Defaults to `true`."",""valid"":[""true"",""false""]},{""name"":""acceptTransitive"",""type"":""Boolean"",""displayName"":""Accept transitive"",""description"":""Some plugins apply other plugins. When this is set to true no plugin declaration will be added if the plugin is already applied transitively. When this is set to false the plugin will be added explicitly even if it is already applied transitively. Defaults to `true`."",""valid"":[""true"",""false""]}]",
5960
maven,org.openrewrite:rewrite-gradle,org.openrewrite.gradle.plugins.AddDevelocityGradlePlugin,Add the Develocity Gradle plugin,Add the Develocity Gradle plugin to settings.gradle files.,1,Plugins,Gradle,"[{""name"":""version"",""type"":""String"",""displayName"":""Plugin version"",""description"":""An exact version number or node-style semver selector used to select the version number. You can also use `latest.release` for the latest available version and `latest.patch` if the current version is a valid semantic version. For more details, you can look at the documentation page of [version selectors](https://docs.openrewrite.org/reference/dependency-version-selectors). Defaults to `latest.release`."",""example"":""3.x""},{""name"":""server"",""type"":""String"",""displayName"":""Server URL"",""description"":""The URL of the Develocity server. If omitted the recipe will set no URL and Gradle will direct scans to https://scans.gradle.com/"",""example"":""https://scans.gradle.com/""},{""name"":""allowUntrustedServer"",""type"":""Boolean"",""displayName"":""Allow untrusted server"",""description"":""When set to `true` the plugin will be configured to allow unencrypted http connections with the server. If set to `false` or omitted, the plugin will refuse to communicate without transport layer security enabled."",""example"":""true""},{""name"":""captureTaskInputFiles"",""type"":""Boolean"",""displayName"":""Capture task input files"",""description"":""When set to `true` the plugin will capture additional information about the inputs to Gradle tasks. This increases the size of build scans, but is useful for diagnosing issues with task caching. "",""example"":""true""},{""name"":""uploadInBackground"",""type"":""Boolean"",""displayName"":""Upload in background"",""description"":""When set to `true` the plugin will capture additional information about the outputs of Gradle tasks. This increases the size of build scans, but is useful for diagnosing issues with task caching. "",""example"":""true""},{""name"":""publishCriteria"",""type"":""PublishCriteria"",""displayName"":""Publish criteria"",""description"":""When set to `Always` the plugin will publish build scans of every single build. When set to `Failure` the plugin will only publish build scans when the build fails. When omitted scans will be published only when the `--scan` option is passed to the build."",""example"":""Always"",""valid"":[""Always"",""Failure""]}]","[{""name"":""org.openrewrite.maven.table.MavenMetadataFailures"",""displayName"":""Maven metadata failures"",""instanceName"":""Maven metadata failures"",""description"":""Attempts to resolve maven metadata that failed."",""columns"":[{""name"":""group"",""type"":""String"",""displayName"":""Group id"",""description"":""The groupId of the artifact for which the metadata download failed.""},{""name"":""artifactId"",""type"":""String"",""displayName"":""Artifact id"",""description"":""The artifactId of the artifact for which the metadata download failed.""},{""name"":""version"",""type"":""String"",""displayName"":""Version"",""description"":""The version of the artifact for which the metadata download failed.""},{""name"":""mavenRepositoryUri"",""type"":""String"",""displayName"":""Maven repository"",""description"":""The URL of the Maven repository that the metadata download failed on.""},{""name"":""snapshots"",""type"":""String"",""displayName"":""Snapshots"",""description"":""Does the repository support snapshots.""},{""name"":""releases"",""type"":""String"",""displayName"":""Releases"",""description"":""Does the repository support releases.""},{""name"":""failure"",""type"":""String"",""displayName"":""Failure"",""description"":""The reason the metadata download failed.""}]}]"
6061
maven,org.openrewrite:rewrite-gradle,org.openrewrite.gradle.plugins.AddSettingsPlugin,Add Gradle settings plugin,Add plugin to Gradle settings file `plugins` block by id.,1,Plugins,Gradle,"[{""name"":""pluginId"",""type"":""String"",""displayName"":""Plugin id"",""description"":""The plugin id to apply."",""example"":""com.jfrog.bintray"",""required"":true},{""name"":""version"",""type"":""String"",""displayName"":""Plugin version"",""description"":""An exact version number or node-style semver selector used to select the version number. You can also use `latest.release` for the latest available version and `latest.patch` if the current version is a valid semantic version. For more details, you can look at the documentation page of [version selectors](https://docs.openrewrite.org/reference/dependency-version-selectors). Defaults to `latest.release`."",""example"":""3.x""},{""name"":""versionPattern"",""type"":""String"",""displayName"":""Version pattern"",""description"":""Allows version selection to be extended beyond the original Node Semver semantics. So for example,Setting 'version' to \""25-29\"" can be paired with a metadata pattern of \""-jre\"" to select Guava 29.0-jre"",""example"":""-jre""},{""name"":""apply"",""type"":""Boolean"",""displayName"":""Apply plugin"",""description"":""Immediate apply the plugin. Defaults to `true`."",""valid"":[""true"",""false""]},{""name"":""acceptTransitive"",""type"":""Boolean"",""displayName"":""Accept transitive"",""description"":""Some plugins apply other plugins. When this is set to true no plugin declaration will be added if the plugin is already applied transitively. When this is set to false the plugin will be added explicitly even if it is already applied transitively. Defaults to `true`."",""valid"":[""true"",""false""]}]",

rewrite-gradle/src/test/java/org/openrewrite/gradle/MigrateToGradle9Test.java

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,14 @@ void multipleSubRecipesApplyTogether() {
4848
dependencies {
4949
implementation group: 'com.google.guava', name: 'guava', version: '31.1-jre'
5050
}
51+
52+
task doSomething(type: JavaExec) {
53+
main = "com.example.AppMain"
54+
}
55+
56+
tasks.register("runEverything", JavaExec) {
57+
main = "com.example.AppMain"
58+
}
5159
""",
5260
"""
5361
plugins {
@@ -62,6 +70,40 @@ void multipleSubRecipesApplyTogether() {
6270
dependencies {
6371
implementation "com.google.guava:guava:31.1-jre"
6472
}
73+
74+
task doSomething(type: JavaExec) {
75+
mainClass = "com.example.AppMain"
76+
}
77+
78+
tasks.register("runEverything", JavaExec) {
79+
mainClass = "com.example.AppMain"
80+
}
81+
"""
82+
)
83+
);
84+
}
85+
86+
@Test
87+
void useMainClassPropertyInKotlinDsl() {
88+
rewriteRun(
89+
buildGradleKts(
90+
"""
91+
plugins {
92+
`java`
93+
}
94+
95+
tasks.register<JavaExec>("doSomething") {
96+
main = "com.example.AppMain"
97+
}
98+
""",
99+
"""
100+
plugins {
101+
`java`
102+
}
103+
104+
tasks.register<JavaExec>("doSomething") {
105+
mainClass = "com.example.AppMain"
106+
}
65107
"""
66108
)
67109
);

0 commit comments

Comments
 (0)