Skip to content

Always include Gradle API stubs in GradleParser classpath#7232

Draft
pstreef wants to merge 2 commits intomainfrom
pstreef/gradle-parser-settings-cp
Draft

Always include Gradle API stubs in GradleParser classpath#7232
pstreef wants to merge 2 commits intomainfrom
pstreef/gradle-parser-settings-cp

Conversation

@pstreef
Copy link
Copy Markdown
Contributor

@pstreef pstreef commented Apr 1, 2026

Problem

When settingsClasspath or buildscriptClasspath is explicitly set on GradleParser.Builder (even to an empty list), the parser replaces the default Gradle API stubs entirely instead of merging them. This causes incorrect type attribution on DSL methods like pluginManagement(), repositories(), and gradlePluginPortal().

For example, pluginManagement() ends up with a garbage declaring type like org.gradle.api.initialization.org.gradle.api.initialization.13789550265458Kt instead of the correct org.gradle.api.initialization.Settings. Recipes that use MethodMatcher (e.g. FindRepository) then fail to match these methods, causing downstream recipes like AddSettingsPluginRepository to not detect existing entries and add duplicates.

This happens in practice when the Gradle plugin extracts the settings buildscript classpath (typically empty when there are no custom settings plugins) and passes it to GradleParser.

Solution

Introduce mergeClasspath() which always includes the default Gradle API stubs (gradle-core-api, gradle-base-services, etc.) alongside any externally-provided classpath, using a LinkedHashSet to avoid duplicates. All four parser initialization paths (groovy/kotlin x build/settings) now use this method.

Includes a reproducer test that sets settingsClasspath to an empty list and verifies AddSettingsPluginRepository correctly detects an existing gradlePluginPortal().

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()`.
@pstreef pstreef force-pushed the pstreef/gradle-parser-settings-cp branch from 58b3cb4 to a007914 Compare April 1, 2026 13:15
- 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
@shanman190
Copy link
Copy Markdown
Contributor

When parsing Gradle scripts in process, the Gradle API will be available, so we wouldn't want to add the Gradle 6 implementation of classes back in that we use when out of process as a workaround.

The class value discovered looks like a dynamic proxy which likely extends the target class, so that makes me suspicious that our matcher isn't checking supertypes in FindRepository.

Copy link
Copy Markdown
Member

@sambsnyd sambsnyd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense to me

@github-project-automation github-project-automation bot moved this from In Progress to Ready to Review in OpenRewrite Apr 1, 2026
@sambsnyd
Copy link
Copy Markdown
Member

sambsnyd commented Apr 1, 2026

When parsing Gradle scripts in process, the Gradle API will be available, so we wouldn't want to add the Gradle 6 implementation of classes back in that we use when out of process as a workaround.

That's a good point. Of course we also use GradleParser in recipes to generate new LST snippets and we do want the API available there as much as possible.

The class value discovered looks like a dynamic proxy which likely extends the target class, so that makes me suspicious that our matcher isn't checking supertypes in FindRepository.

That sounds plausible

@shanman190
Copy link
Copy Markdown
Contributor

That's a good point. Of course we also use GradleParser in recipes to generate new LST snippets and we do want the API available there as much as possible.

Yep. In recipes, we don't configure a classpath (buildscript or settings), so that is what results in use default loading the Gradle 6 shim already.

@timtebeek timtebeek requested review from timtebeek and removed request for timtebeek April 1, 2026 21:28
@pstreef
Copy link
Copy Markdown
Contributor Author

pstreef commented Apr 2, 2026

Thanks for the detailed feedback. You raise a good point about the in-process case — DefaultProjectParser runs inside Gradle where the real API is on the classloader, so unconditionally merging the bundled Gradle API jars isn't right.

The supertype/dynamic proxy angle is interesting too. I'm going to leave this PR as draft — @shanman190, would you be open to picking up the root cause fix? You clearly have better context on the MethodMatcher/FindRepository interaction and the in-process parsing constraints.

In the meantime, #7230 (the recipe-level workaround) is merged and handles the immediate symptom. I have a follow-up ready for Shannon's review comment there too (#7245 — gating the name-based fallback on url == null so it doesn't incorrectly block multiple maven {} entries).

@timtebeek timtebeek moved this from Ready to Review to In Progress in OpenRewrite Apr 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

4 participants