From 8532fab534337445593b58b42aabbf68776eb8df Mon Sep 17 00:00:00 2001 From: Alex Lockhart Date: Wed, 25 Feb 2026 15:18:33 -0400 Subject: [PATCH] Fix AddDependency bypassing onlyIfUsing with explicit config (#6834) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When a ScanningRecipe like AddDependency was used inside a declarative YAML recipe, scanNestedScanningRecipes() recursed into getRecipeList() of all recipes — including leaf ScanningRecipes. This caused their accumulators to be populated during precondition scanning rather than during normal recipe execution, breaking onlyIfUsing filtering. Fix: Distinguish DeclarativeRecipe containers (recurse into raw preconditions and recipeList fields) from leaf ScanningRecipes (skip recursion — let the framework handle their scanning during execution). Raw fields are used instead of getRecipeList() to avoid PreconditionBellwether wrapping. Tests: Added addDependencyOnlyIfUsingInDeclarativeRecipe() in both rewrite-maven and rewrite-gradle with two projects — one using Guava (dependency added) and one not (dependency left unchanged). Fixes #6821 --- .../openrewrite/config/DeclarativeRecipe.java | 31 ++++++- .../openrewrite/gradle/AddDependencyTest.java | 92 +++++++++++++++++++ .../openrewrite/maven/AddDependencyTest.java | 90 ++++++++++++++++++ 3 files changed, 209 insertions(+), 4 deletions(-) diff --git a/rewrite-core/src/main/java/org/openrewrite/config/DeclarativeRecipe.java b/rewrite-core/src/main/java/org/openrewrite/config/DeclarativeRecipe.java index 98a12ad900..89f4d44d95 100644 --- a/rewrite-core/src/main/java/org/openrewrite/config/DeclarativeRecipe.java +++ b/rewrite-core/src/main/java/org/openrewrite/config/DeclarativeRecipe.java @@ -178,8 +178,21 @@ private void registerNestedScanningRecipes(Recipe recipe, Accumulator acc, Execu if (recipe instanceof ScanningRecipe && isScanningRequired(recipe)) { acc.recipeToAccumulator.put(recipe, ((ScanningRecipe) recipe).getInitialValue(ctx)); } - for (Recipe nested : recipe.getRecipeList()) { - registerNestedScanningRecipes(nested, acc, ctx); + // Recurse into DeclarativeRecipes using raw fields (preconditions + recipeList) + // to avoid getRecipeList() which wraps entries with bellwethers. + // Don't recurse into leaf ScanningRecipes — they have no precondition children. + if (recipe instanceof DeclarativeRecipe) { + DeclarativeRecipe dr = (DeclarativeRecipe) recipe; + for (Recipe precondition : dr.preconditions) { + registerNestedScanningRecipes(precondition, acc, ctx); + } + for (Recipe r : dr.recipeList) { + registerNestedScanningRecipes(r, acc, ctx); + } + } else if (!(recipe instanceof ScanningRecipe)) { + for (Recipe nested : recipe.getRecipeList()) { + registerNestedScanningRecipes(nested, acc, ctx); + } } } @@ -203,8 +216,18 @@ private void scanNestedScanningRecipes(Recipe recipe, Accumulator acc, @Nullable Object recipeAcc = acc.recipeToAccumulator.get(recipe); scanningRecipe.getScanner(recipeAcc).visit(tree, ctx); } - for (Recipe nested : recipe.getRecipeList()) { - scanNestedScanningRecipes(nested, acc, tree, ctx); + // Recurse into nested DeclarativeRecipes used as preconditions, scanning their + // raw preconditions and recipeList fields directly. We avoid getRecipeList() which + // wraps entries with bellwethers. Leaf ScanningRecipes (e.g. AddDependency) are + // not recursed into — their recipeList is scanned during recipe execution. + if (recipe instanceof DeclarativeRecipe) { + DeclarativeRecipe nested = (DeclarativeRecipe) recipe; + for (Recipe precondition : nested.preconditions) { + scanNestedScanningRecipes(precondition, acc, tree, ctx); + } + for (Recipe r : nested.recipeList) { + scanNestedScanningRecipes(r, acc, tree, ctx); + } } } diff --git a/rewrite-gradle/src/test/java/org/openrewrite/gradle/AddDependencyTest.java b/rewrite-gradle/src/test/java/org/openrewrite/gradle/AddDependencyTest.java index e82c04b6d2..d1ae99cb4e 100644 --- a/rewrite-gradle/src/test/java/org/openrewrite/gradle/AddDependencyTest.java +++ b/rewrite-gradle/src/test/java/org/openrewrite/gradle/AddDependencyTest.java @@ -1990,6 +1990,98 @@ void doNotAddDependencyToAppliedScripts() { } + @Test + @Issue("https://github.com/openrewrite/rewrite/issues/6821") + void addDependencyOnlyIfUsingInDeclarativeRecipe() { + rewriteRun( + spec -> spec.recipeFromYaml( + """ + --- + type: specs.openrewrite.org/v1beta/recipe + name: com.example.AddGuavaIfUsed + displayName: Add Guava if used + description: Adds Guava dependency when com.google.common types are used. + recipeList: + - org.openrewrite.gradle.AddDependency: + groupId: com.google.guava + artifactId: guava + version: 29.0-jre + onlyIfUsing: com.google.common.collect.* + """, + "com.example.AddGuavaIfUsed" + ), + // Project that DOES use Guava - dependency should be added + mavenProject("uses-guava", + srcMainJava( + java( + """ + package com.example; + + import com.google.common.collect.ImmutableList; + + public class UsesGuava { + public void useGuava() { + ImmutableList list = ImmutableList.of("a", "b", "c"); + } + } + """ + ) + ), + buildGradle( + """ + plugins { + id "java-library" + } + + repositories { + mavenCentral() + } + """, + """ + plugins { + id "java-library" + } + + repositories { + mavenCentral() + } + + dependencies { + implementation "com.google.guava:guava:29.0-jre" + } + """ + ) + ), + // Project that does NOT use Guava - dependency should NOT be added + mavenProject("no-guava", + srcMainJava( + java( + """ + package com.example; + + public class NoGuava { + public void doesNotUseGuava() { + System.out.println("No Guava here"); + } + } + """ + ) + ), + buildGradle( + """ + plugins { + id "java-library" + } + + repositories { + mavenCentral() + } + """ + ) + ) + ); + } + private AddDependency addDependency(@SuppressWarnings("SameParameterValue") String gav) { return addDependency(gav, null, null); } diff --git a/rewrite-maven/src/test/java/org/openrewrite/maven/AddDependencyTest.java b/rewrite-maven/src/test/java/org/openrewrite/maven/AddDependencyTest.java index 43ae0fe9fe..0add9fb675 100644 --- a/rewrite-maven/src/test/java/org/openrewrite/maven/AddDependencyTest.java +++ b/rewrite-maven/src/test/java/org/openrewrite/maven/AddDependencyTest.java @@ -2181,6 +2181,96 @@ void addDependencyUpgradesScopeOnlyWhenExistingDependencyHasNarrowerScopeAndEqua ) ); } + + @Issue("https://github.com/openrewrite/rewrite/issues/6821") + @Test + void addDependencyOnlyIfUsingInDeclarativeRecipe() { + rewriteRun( + spec -> spec.recipeFromYaml( + """ + --- + type: specs.openrewrite.org/v1beta/recipe + name: com.example.AddGuavaIfUsed + displayName: Add Guava if used + description: Adds Guava dependency when com.google.common types are used. + recipeList: + - org.openrewrite.maven.AddDependency: + groupId: com.google.guava + artifactId: guava + version: 29.0-jre + onlyIfUsing: com.google.common.collect.* + """, + "com.example.AddGuavaIfUsed" + ), + // Project that DOES use Guava - dependency should be added + mavenProject("uses-guava", + srcMainJava( + java( + """ + package com.example; + + import com.google.common.collect.ImmutableList; + + public class UsesGuava { + public void useGuava() { + ImmutableList list = ImmutableList.of("a", "b", "c"); + } + } + """ + ) + ), + pomXml( + """ + + com.mycompany.app + uses-guava + 1 + + """, + """ + + com.mycompany.app + uses-guava + 1 + + + com.google.guava + guava + 29.0-jre + + + + """ + ) + ), + // Project that does NOT use Guava - dependency should NOT be added + // This test case fails in v3.24.0 (dependency incorrectly added) + mavenProject("no-guava", + srcMainJava( + java( + """ + package com.example; + + public class NoGuava { + public void doesNotUseGuava() { + System.out.println("No Guava here"); + } + } + """ + ) + ), + pomXml( + """ + + com.mycompany.app + no-guava + 1 + + """ + ) + ) + ); + } private AddDependency addDependency(@SuppressWarnings("SameParameterValue") String gav) { return addDependency(gav, null, null, null);