From a0079144814f1f8d59790e5c6a0d7965a79f4fbb Mon Sep 17 00:00:00 2001
From: Peter Streef
Date: Wed, 1 Apr 2026 14:48:41 +0200
Subject: [PATCH 1/2] Always include Gradle API stubs in GradleParser classpath
When `settingsClasspath` or `buildscriptClasspath` is explicitly set
(even to empty), `GradleParser` previously replaced the default Gradle
API stubs entirely. This caused incorrect type attribution on DSL
methods like `pluginManagement()`, `repositories()`, and
`gradlePluginPortal()`, leading recipes like
`AddSettingsPluginRepository` to fail to detect existing entries.
Introduce `mergeClasspath()` which always includes the default Gradle
API stubs alongside any externally-provided classpath. Add a
reproducer test that sets `settingsClasspath` to empty and verifies
`AddSettingsPluginRepository` correctly detects an existing
`gradlePluginPortal()`.
---
.../org/openrewrite/gradle/GradleParser.java | 42 +++++++++----------
.../AddSettingsPluginRepositoryTest.java | 40 ++++++++++++++++++
2 files changed, 60 insertions(+), 22 deletions(-)
diff --git a/rewrite-gradle/src/main/java/org/openrewrite/gradle/GradleParser.java b/rewrite-gradle/src/main/java/org/openrewrite/gradle/GradleParser.java
index d88909d1ce2..30c57a2c86b 100644
--- a/rewrite-gradle/src/main/java/org/openrewrite/gradle/GradleParser.java
+++ b/rewrite-gradle/src/main/java/org/openrewrite/gradle/GradleParser.java
@@ -27,8 +27,7 @@
import org.openrewrite.kotlin.KotlinParser;
import java.nio.file.Path;
-import java.util.Collection;
-import java.util.List;
+import java.util.*;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
@@ -65,12 +64,8 @@ public class GradleParser implements Parser {
@Override
public Stream parseInputs(Iterable sources, @Nullable Path relativeTo, ExecutionContext ctx) {
if (groovyBuildParser == null) {
- Collection buildscriptClasspath = base.buildscriptClasspath;
- if (buildscriptClasspath == null) {
- buildscriptClasspath = defaultClasspath(ctx);
- }
groovyBuildParser = GroovyParser.builder(base.groovyParser)
- .classpath(buildscriptClasspath)
+ .classpath(mergeClasspath(base.buildscriptClasspath, ctx))
.compilerCustomizers(
new DefaultImportsCustomizer(),
config -> config.setScriptBaseClass("RewriteGradleProject")
@@ -78,12 +73,8 @@ public Stream parseInputs(Iterable sources, @Nullable Path re
.build();
}
if (kotlinBuildParser == null) {
- Collection buildscriptClasspath = base.buildscriptClasspath;
- if (buildscriptClasspath == null) {
- buildscriptClasspath = defaultClasspath(ctx);
- }
kotlinBuildParser = KotlinParser.builder(base.kotlinParser)
- .classpath(buildscriptClasspath)
+ .classpath(mergeClasspath(base.buildscriptClasspath, ctx))
.dependsOn(KTS_BUILD_STUBS)
.isKotlinScript(true)
.scriptImplicitReceivers("org.gradle.api.Project")
@@ -91,12 +82,8 @@ public Stream parseInputs(Iterable sources, @Nullable Path re
.build();
}
if (groovySettingsParser == null) {
- Collection settingsClasspath = base.settingsClasspath;
- if (settingsClasspath == null) {
- settingsClasspath = defaultClasspath(ctx);
- }
groovySettingsParser = GroovyParser.builder(base.groovyParser)
- .classpath(settingsClasspath)
+ .classpath(mergeClasspath(base.settingsClasspath, ctx))
.compilerCustomizers(
new DefaultImportsCustomizer(),
config -> config.setScriptBaseClass("RewriteSettings")
@@ -104,12 +91,8 @@ public Stream parseInputs(Iterable sources, @Nullable Path re
.build();
}
if (kotlinSettingsParser == null) {
- Collection settingsClasspath = base.settingsClasspath;
- if (settingsClasspath == null) {
- settingsClasspath = defaultClasspath(ctx);
- }
kotlinSettingsParser = KotlinParser.builder(base.kotlinParser)
- .classpath(settingsClasspath)
+ .classpath(mergeClasspath(base.settingsClasspath, ctx))
.dependsOn(KTS_SETTINGS_STUBS)
.isKotlinScript(true)
.scriptImplicitReceivers("org.gradle.api.initialization.Settings")
@@ -210,6 +193,21 @@ public String getDslName() {
}
}
+ /**
+ * Always include the default Gradle API stubs alongside any externally-provided classpath.
+ * The external classpath (e.g. settings buildscript dependencies) typically contains custom
+ * plugin jars but not the Gradle API itself, which is needed for correct type attribution
+ * of DSL methods like pluginManagement(), repositories(), gradlePluginPortal(), etc.
+ */
+ private Collection mergeClasspath(@Nullable Collection provided, ExecutionContext ctx) {
+ if (provided == null) {
+ return defaultClasspath(ctx);
+ }
+ Set merged = new LinkedHashSet<>(defaultClasspath(ctx));
+ merged.addAll(provided);
+ return merged;
+ }
+
private List defaultClasspath(ExecutionContext ctx) {
if (defaultClasspath == null) {
defaultClasspath = JavaParser.dependenciesFromResources(ctx,
diff --git a/rewrite-gradle/src/test/java/org/openrewrite/gradle/plugins/AddSettingsPluginRepositoryTest.java b/rewrite-gradle/src/test/java/org/openrewrite/gradle/plugins/AddSettingsPluginRepositoryTest.java
index eeb957fe31d..dec5c6613e8 100644
--- a/rewrite-gradle/src/test/java/org/openrewrite/gradle/plugins/AddSettingsPluginRepositoryTest.java
+++ b/rewrite-gradle/src/test/java/org/openrewrite/gradle/plugins/AddSettingsPluginRepositoryTest.java
@@ -18,10 +18,19 @@
import org.junit.jupiter.api.Test;
import org.openrewrite.DocumentExample;
import org.openrewrite.SourceFile;
+import org.openrewrite.gradle.GradleParser;
import org.openrewrite.java.JavaIsoVisitor;
import org.openrewrite.java.tree.J;
import org.openrewrite.java.tree.JavaType;
+import org.openrewrite.kotlin.KotlinParser;
+import org.openrewrite.kotlin.tree.K;
import org.openrewrite.test.RewriteTest;
+import org.openrewrite.test.SourceSpec;
+import org.openrewrite.test.SourceSpecs;
+import org.openrewrite.test.TypeValidation;
+
+import java.nio.file.Paths;
+import java.util.Collections;
import static org.openrewrite.gradle.Assertions.settingsGradle;
import static org.openrewrite.gradle.Assertions.settingsGradleKts;
@@ -682,4 +691,35 @@ void addToExistingPluginManagementWithPluginsBlockKts() {
)
);
}
+
+ /**
+ * Helper to create settingsGradleKts specs using a GradleParser with empty settingsClasspath.
+ * When settingsClasspath is explicitly set to empty (e.g. no custom plugins in the settings
+ * buildscript), the Gradle API stubs must still be included for correct type attribution.
+ */
+ private static SourceSpecs settingsGradleKtsWithEmptyClasspath(String before) {
+ GradleParser.Builder parser = GradleParser.builder()
+ .kotlinParser(KotlinParser.builder().logCompilationWarningsAndErrors(true))
+ .settingsClasspath(Collections.emptyList());
+ SourceSpec gradle = new SourceSpec<>(K.CompilationUnit.class, "gradle", parser, before, null);
+ gradle.path(Paths.get("settings.gradle.kts"));
+ return gradle;
+ }
+
+ @Test
+ void skipWhenExistsGradlePluginPortalKtsWithEmptyClasspath() {
+ rewriteRun(
+ spec -> spec.recipe(new AddSettingsPluginRepository("gradlePluginPortal", null))
+ .typeValidationOptions(TypeValidation.none()),
+ settingsGradleKtsWithEmptyClasspath(
+ """
+ pluginManagement {
+ repositories {
+ gradlePluginPortal()
+ }
+ }
+ """
+ )
+ );
+ }
}
From b07d12455c6385ae9bb261ffc0682ff437f8604f Mon Sep 17 00:00:00 2001
From: Peter Streef
Date: Wed, 1 Apr 2026 15:25:23 +0200
Subject: [PATCH 2/2] Add type attribution tests and use explicit imports
- Add GradleParserTest for settings.gradle.kts and build.gradle.kts
type attribution with empty classpaths, verifying pluginManagement()
resolves to Settings and repositories() resolves to Project
- Narrow TypeValidation in AddSettingsPluginRepositoryTest to only
disable methodInvocations instead of disabling all validation
- Use explicit java.util imports instead of wildcard in GradleParser
---
.../org/openrewrite/gradle/GradleParser.java | 5 +-
.../openrewrite/gradle/GradleParserTest.java | 48 +++++++++++++++++++
.../AddSettingsPluginRepositoryTest.java | 2 +-
3 files changed, 53 insertions(+), 2 deletions(-)
diff --git a/rewrite-gradle/src/main/java/org/openrewrite/gradle/GradleParser.java b/rewrite-gradle/src/main/java/org/openrewrite/gradle/GradleParser.java
index 30c57a2c86b..6a39554070d 100644
--- a/rewrite-gradle/src/main/java/org/openrewrite/gradle/GradleParser.java
+++ b/rewrite-gradle/src/main/java/org/openrewrite/gradle/GradleParser.java
@@ -27,7 +27,10 @@
import org.openrewrite.kotlin.KotlinParser;
import java.nio.file.Path;
-import java.util.*;
+import java.util.Collection;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
diff --git a/rewrite-gradle/src/test/java/org/openrewrite/gradle/GradleParserTest.java b/rewrite-gradle/src/test/java/org/openrewrite/gradle/GradleParserTest.java
index 7beb5212d91..49cc3276532 100644
--- a/rewrite-gradle/src/test/java/org/openrewrite/gradle/GradleParserTest.java
+++ b/rewrite-gradle/src/test/java/org/openrewrite/gradle/GradleParserTest.java
@@ -27,6 +27,8 @@
import org.openrewrite.test.RewriteTest;
import org.openrewrite.tree.ParseError;
+import java.nio.file.Paths;
+import java.util.Collections;
import java.util.List;
import java.util.Optional;
import java.util.stream.Stream;
@@ -571,6 +573,52 @@ void escapedBackslashesAndInterpolationInGString(@Language("groovy") String groo
assertThat(sourceFile).isNotInstanceOf(ParseError.class);
}
+ @Test
+ void settingsKtsTypeAttributionWithEmptyClasspath() {
+ var parser = new GradleParser(GradleParser.builder().settingsClasspath(Collections.emptyList()));
+ Stream sourceFileStream = parser.parseInputs(
+ List.of(Parser.Input.fromString(Paths.get("settings.gradle.kts"),
+ "pluginManagement {\n repositories {\n gradlePluginPortal()\n }\n}\n")),
+ null, new InMemoryExecutionContext());
+ Optional sf = sourceFileStream.findFirst();
+ assertThat(sf).isPresent();
+ new org.openrewrite.TreeVisitor() {
+ @Override
+ public org.openrewrite.Tree visit(org.openrewrite.Tree tree, Integer p) {
+ if (tree instanceof J.MethodInvocation mi && "pluginManagement".equals(mi.getSimpleName())) {
+ assertThat(mi.getMethodType()).isNotNull();
+ assertThat(mi.getMethodType().getDeclaringType().getFullyQualifiedName())
+ .as("pluginManagement() should resolve to Settings")
+ .isEqualTo("org.gradle.api.initialization.Settings");
+ }
+ return super.visit(tree, p);
+ }
+ }.visit(sf.get(), 0);
+ }
+
+ @Test
+ void buildKtsTypeAttributionWithEmptyClasspath() {
+ var parser = new GradleParser(GradleParser.builder().buildscriptClasspath(Collections.emptyList()));
+ Stream sourceFileStream = parser.parseInputs(
+ List.of(Parser.Input.fromString(Paths.get("build.gradle.kts"),
+ "repositories {\n mavenCentral()\n}\n")),
+ null, new InMemoryExecutionContext());
+ Optional sf = sourceFileStream.findFirst();
+ assertThat(sf).isPresent();
+ new org.openrewrite.TreeVisitor() {
+ @Override
+ public org.openrewrite.Tree visit(org.openrewrite.Tree tree, Integer p) {
+ if (tree instanceof J.MethodInvocation mi && "repositories".equals(mi.getSimpleName())) {
+ assertThat(mi.getMethodType()).isNotNull();
+ assertThat(mi.getMethodType().getDeclaringType().getFullyQualifiedName())
+ .as("repositories() should resolve to Project")
+ .isEqualTo("org.gradle.api.Project");
+ }
+ return super.visit(tree, p);
+ }
+ }.visit(sf.get(), 0);
+ }
+
/**
* Produces a stream of test expressions like `def a = "\\${System.getProperty('user.name')}"`
*/
diff --git a/rewrite-gradle/src/test/java/org/openrewrite/gradle/plugins/AddSettingsPluginRepositoryTest.java b/rewrite-gradle/src/test/java/org/openrewrite/gradle/plugins/AddSettingsPluginRepositoryTest.java
index dec5c6613e8..db71a2c325c 100644
--- a/rewrite-gradle/src/test/java/org/openrewrite/gradle/plugins/AddSettingsPluginRepositoryTest.java
+++ b/rewrite-gradle/src/test/java/org/openrewrite/gradle/plugins/AddSettingsPluginRepositoryTest.java
@@ -710,7 +710,7 @@ private static SourceSpecs settingsGradleKtsWithEmptyClasspath(String before) {
void skipWhenExistsGradlePluginPortalKtsWithEmptyClasspath() {
rewriteRun(
spec -> spec.recipe(new AddSettingsPluginRepository("gradlePluginPortal", null))
- .typeValidationOptions(TypeValidation.none()),
+ .typeValidationOptions(TypeValidation.builder().methodInvocations(false).build()),
settingsGradleKtsWithEmptyClasspath(
"""
pluginManagement {