Skip to content

Avoid triggering full transitive resolution from GradleDependencyConfiguration.findResolvedDependency#7455

Open
steve-aom-elliott wants to merge 1 commit intomainfrom
fix/find-resolved-dependency-shallow
Open

Avoid triggering full transitive resolution from GradleDependencyConfiguration.findResolvedDependency#7455
steve-aom-elliott wants to merge 1 commit intomainfrom
fix/find-resolved-dependency-shallow

Conversation

@steve-aom-elliott
Copy link
Copy Markdown
Contributor

@steve-aom-elliott steve-aom-elliott commented Apr 22, 2026

What

GradleDependencyConfiguration.findResolvedDependency(groupId, artifactId) iterates the configuration's direct dependencies looking for a match. It was calling getDirectResolved(), which triggers a full transitive download if the configuration has been marked for re-resolution (e.g. after AddDependency / UpgradeDependencyVersion / ChangeDependency mutations).

Switch it to getDirectResolvedShallow(), which resolves only the direct dependencies and defers the transitive closure until a caller actually walks it.

Why

The method is called from ChangeDependency, RemoveRedundantDependencyVersions, RemoveRedundantSecurityResolutionRules, GradleConfigurationFilter, and several downstream recipe repositories (AddJupiterDependencies in rewrite-testing-frameworks, MergeBootstrapYamlWithApplicationYaml in rewrite-spring, AddJaxbRuntime / AddJaxwsRuntime in rewrite-migrate-java, etc.).

  • In a migration recipe chain that mutates many dependencies per build.gradle, each findResolvedDependency call was paying the full transitive-resolution cost via LazyResolutionContext.resolve() — which re-downloads POMs from the configured repositories. Building on Sam's getDirectResolvedShallow() work, this collapses another common hot path onto the shallow resolution.

How

  • findResolvedDependency(String, String) now iterates getDirectResolvedShallow().
  • Javadoc clarified: the method matches direct dependencies of the configuration. To match a transitive, callers should iterate getResolved() (the flat resolved list) directly.
  • Updated GradleProjectTest#changeConstraint, which used findResolvedDependency to look up jackson-databind (a transitive of rewrite-core). It now streams getResolved() and filters.

Tests

  • Full rewrite-gradle:test suite passes (20m 5s).
  • GradleProjectTest, ChangeDependencyTest, RemoveRedundantDependencyVersionsTest, RemoveRedundantSecurityResolutionRulesTest, UpgradeDependencyVersionTest pass.

…iguration.findResolvedDependency

findResolvedDependency iterates direct dependencies of the configuration looking for a match.
It was calling getDirectResolved(), which triggers a full transitive download if the
configuration has been marked for re-resolution (e.g. after AddDependency /
UpgradeDependencyVersion / ChangeDependency mutations). Switch to getDirectResolvedShallow(),
which resolves only the direct dependencies and defers the transitive closure until a
caller actually walks it.

The method is called from ChangeDependency, RemoveRedundantDependencyVersions,
RemoveRedundantSecurityResolutionRules, GradleConfigurationFilter, and several downstream
recipe repositories. In a migration recipe chain that rewrites many dependencies per
build.gradle, each call was paying the full transitive-resolution cost.

Updates one test (GradleProjectTest#changeConstraint) that searched for a transitive
dependency via findResolvedDependency; it now iterates getResolved() directly, since the
method is now scoped to direct dependencies of the configuration.
Copy link
Copy Markdown
Contributor

@Jenson3210 Jenson3210 left a comment

Choose a reason for hiding this comment

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

Note: ResolvedDependency.findDependency() recursively walks this.dependencies. With getDirectResolvedShallow(), those dependency lists are empty (transitive POMs were never downloaded), so findDependency() effectively degrades to matching on the direct dependency's own coordinates only — it won't walk into transitives and won't trigger any downloads.

This means callers like AddJupiterDependencies (rewrite-testing-frameworks) that search for junit-jupiter-api via findResolvedDependency("org.junit.jupiter", "junit-jupiter-api") will no longer find it if it's only present as a transitive (e.g., pulled in by spring-boot-starter-testjunit-jupiter). The recipe would then redundantly add junit-jupiter — harmless but slightly noisy.

Worth noting in the Javadoc that this is now a direct-only match, since the method name findResolvedDependency doesn't make that obvious and callers in other repos (rewrite-testing-frameworks, rewrite-spring, rewrite-migrate-java) may rely on transitive matching.

Copy link
Copy Markdown
Contributor

@Jenson3210 Jenson3210 left a comment

Choose a reason for hiding this comment

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

See comment above — findDependency() still walks this.dependencies recursively, but with shallow resolution those lists are empty. This silently changes findResolvedDependency from "search direct + transitive" to "search direct only" without updating callers in other repos that rely on transitive matching (AddJupiterDependencies, MergeBootstrapYaml, AddJaxbRuntime, etc.).

@github-project-automation github-project-automation Bot moved this from Ready to Review to In Progress in OpenRewrite Apr 24, 2026
@Jenson3210
Copy link
Copy Markdown
Contributor

Perhaps we can build a smarter loop that stops the moment it is found and does not resolve the later/deeper dependencies anymore?
Not sure how we can overcome this one

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.

2 participants