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..6a39554070d 100644 --- a/rewrite-gradle/src/main/java/org/openrewrite/gradle/GradleParser.java +++ b/rewrite-gradle/src/main/java/org/openrewrite/gradle/GradleParser.java @@ -28,7 +28,9 @@ import java.nio.file.Path; 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; @@ -65,12 +67,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 +76,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 +85,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 +94,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 +196,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/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 eeb957fe31d..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 @@ -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.builder().methodInvocations(false).build()), + settingsGradleKtsWithEmptyClasspath( + """ + pluginManagement { + repositories { + gradlePluginPortal() + } + } + """ + ) + ); + } }