Skip to content

Changing AddDependencyVisitor so it is able to broaden the scope of a direct dependency if needed - #6434

Merged
steve-aom-elliott merged 5 commits into
mainfrom
6426-add-dependency-skip-when-any-scope-already-present
Jan 8, 2026
Merged

Changing AddDependencyVisitor so it is able to broaden the scope of a direct dependency if needed#6434
steve-aom-elliott merged 5 commits into
mainfrom
6426-add-dependency-skip-when-any-scope-already-present

Conversation

@steve-aom-elliott

@steve-aom-elliott steve-aom-elliott commented Dec 12, 2025

Copy link
Copy Markdown
Contributor

What's changed?

  • Changed the logic for AddDependency / AddDependencyVisitor so that if it encounters an existing dependency with a narrower scope and an equal / lower version compared to the requested, it will instead opt to broaden the scope of the dependency.
  • If the existing dependency has the same scope, but a lower version, the version will be upgraded
  • Dropped acceptable request of import scope in favour of compile, as import is only valid for dependencies listed in dependencyManagement

What's your motivation?

Anything in particular you'd like reviewers to focus on?

  • When requesting a dependency that is available transitively, but at a lower version than requested, should we still be preventing it from adding a direct dependency if acceptTransitive is used? Currently if acceptTransitive is true, it will not add the dependency, but if acceptTransitive is false or not set, then it will add the dependency.
    • I feel like either we should be consistent between these two for the given situation or we should add to the option description for version to explicitly call out that it will be ignored if acceptTransitive is true and a transitive dependency matched on groupId and artifactId is found.

Checklist

  • I've added unit tests to cover both positive and negative cases
  • I've read and applied the recipe conventions and best practices
  • I've used the IntelliJ IDEA auto-formatter on affected files

@steve-aom-elliott steve-aom-elliott self-assigned this Dec 12, 2025
@steve-aom-elliott steve-aom-elliott added bug Something isn't working test provided Already replicated with a unit test, using JUnit pioneer's ExpectedToFail maven labels Dec 12, 2025
@github-project-automation github-project-automation Bot moved this to In Progress in OpenRewrite Dec 12, 2025
@steve-aom-elliott steve-aom-elliott moved this from In Progress to Ready to Review in OpenRewrite Dec 12, 2025
@timtebeek

Copy link
Copy Markdown
Member

Give how we already have a recipe to change scope I think it's fine indeed not to add a dependency if it is already present:
https://github.com/openrewrite/rewrite/blob/main/rewrite-maven/src/main/java/org/openrewrite/maven/ChangeDependencyScope.java

Thanks for looking into it!

@steve-aom-elliott

Copy link
Copy Markdown
Contributor Author

@sambsnyd Hoping to get your input on this change for AddDependencyVisitor to make it more broad when it's detecting whether the dependency already exists (existing at any scope rather than the same scope essentially)

@sambsnyd sambsnyd left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I agree the scope check should change, but just removing it isn't quite right.

In addDependencyDoesntAddWhenExistingDependencyWithScopeRuntime test the recipe is configured to add the dependency to the compile scope. If a larger composite recipe or migration is telling AddDependency to do this it is presumably because symbols from that jar are or will be needed to compile the code going forward. The runtime scope does not achieve that, so leaving the dependency in the runtime scope may result in errors. The most appropriate thing the recipe can do here is change the scope to compile (or, equivalently, removing the <scope>)

Consider the inverse situation: If the existing dependency starts off in the compile scope and the recipe is configured to add it to the runtime scope then no change needs to be made. Anything in the compile scope makes its way into the runtime scope automatically. So the most appropriate thing for the recipe to do in that case is nothing.

@steve-aom-elliott
steve-aom-elliott marked this pull request as draft December 24, 2025 22:58
@steve-aom-elliott steve-aom-elliott moved this from Ready to Review to In Progress in OpenRewrite Dec 24, 2025
@steve-aom-elliott
steve-aom-elliott force-pushed the 6426-add-dependency-skip-when-any-scope-already-present branch from afb9c14 to 880edd0 Compare December 24, 2025 22:58
@timtebeek

Copy link
Copy Markdown
Member

I think you were aware already, but there was that one more test failure in AddDependency now:

AddDependencyTest > addDependencyUpgradesWhenExistingDependencyHasNarrowerScope() FAILED
    org.opentest4j.AssertionFailedError: [Unexpected result in "project/pom.xml":
    diff --git a/project/pom.xml b/project/pom.xml
    index 2040b2e..da1a094 100644
    --- a/project/pom.xml
    +++ b/project/pom.xml
    @@ -6,7 +6,7 @@ 
             <dependency>
                 <groupId>com.google.guava</groupId>
                 <artifactId>guava</artifactId>
    -            <version>29.0-jre</version>
    +            <version>28.0-jre</version>
                 <scope>compile</scope>
             </dependency>
         </dependencies>
    ] 

@steve-aom-elliott

steve-aom-elliott commented Jan 2, 2026

Copy link
Copy Markdown
Contributor Author

I think you were aware already, but there was that one more test failure in AddDependency now:

AddDependencyTest > addDependencyUpgradesWhenExistingDependencyHasNarrowerScope() FAILED
    org.opentest4j.AssertionFailedError: [Unexpected result in "project/pom.xml":
    diff --git a/project/pom.xml b/project/pom.xml
    index 2040b2e..da1a094 100644
    --- a/project/pom.xml
    +++ b/project/pom.xml
    @@ -6,7 +6,7 @@ 
             <dependency>
                 <groupId>com.google.guava</groupId>
                 <artifactId>guava</artifactId>
    -            <version>29.0-jre</version>
    +            <version>28.0-jre</version>
                 <scope>compile</scope>
             </dependency>
         </dependencies>
    ] 

Yes, currently working through the logic to try to restrict changes so that:

  • If the requested would be a version downgrade -> make no changes, even if it would broaden scope (this is a safety case for situations where we have multiple recipes for different release versions of something to prevent it from taking an already correct dependency and changing it to a broader scope instance of it despite having asked for a lower version. I've seen our recipes where we later narrow a scope and upgrade a version, and this check prevents undoing that)
  • If the requested would either leave the version unchanged or upgrade it -> if scope would be equal or greater -> try and upgrade version and broaden scope as applicable (those recipes can bail early already)
  • If the scope would be a narrowing -> make no changes

@timtebeek

Copy link
Copy Markdown
Member

Looks like we've had a previous effort here as well; any lessons there we can apply here? Do we need both or just one?

@steve-aom-elliott
steve-aom-elliott force-pushed the 6426-add-dependency-skip-when-any-scope-already-present branch from cb0ce43 to 0db8fb7 Compare January 6, 2026 22:11
@steve-aom-elliott

steve-aom-elliott commented Jan 6, 2026

Copy link
Copy Markdown
Contributor Author

Pushed up the very rough last point I reached but am working on rewriting it more simply here: https://github.com/openrewrite/rewrite/tree/6426-v2 where I tried to take a less elaborate approach. Still hitting some issues with exception markers doubling up such that I'm working on resolving still.

Comment thread rewrite-maven/src/main/java/org/openrewrite/maven/AddDependencyVisitor.java Outdated
@steve-aom-elliott
steve-aom-elliott force-pushed the 6426-add-dependency-skip-when-any-scope-already-present branch from 0db8fb7 to 286241c Compare January 7, 2026 22:25
@steve-aom-elliott

Copy link
Copy Markdown
Contributor Author

Finished reworking the code so now it handles the placeholders, broadening of scope and upgrading of version correctly. Will review for improvements / cleanup tomorrow.

… provided you're requesting an equal or higher version number. If requesting same scope but higher version number, the version number will be upgraded.
…t` is only applicable for dependencies in `dependencyManagement`
@steve-aom-elliott
steve-aom-elliott force-pushed the 6426-add-dependency-skip-when-any-scope-already-present branch from 286241c to 2e99c1d Compare January 8, 2026 17:13
@steve-aom-elliott steve-aom-elliott changed the title Dropping scope check for AddDependencyVisitor so it won't double up on dependencies Changing AddDependencyVisitor so it is able to broaden the scope of a direct dependency if needed Jan 8, 2026
@timtebeek

Copy link
Copy Markdown
Member

We're now seeing the lang3 version dropped in a second cycle in

RemoveRedundantDependencyVersionsTest > supportSpringBootDependenciesPlugin() FAILED
    org.opentest4j.AssertionFailedError: [Expected recipe to complete in 1 cycle, but took at least one more cycle. Between the last two executed cycles there were changes to "build.gradle"] 
    expected: 
      "plugins {
          id "java"
          id("org.springframework.boot") version "3.5.6"
          id("io.spring.dependency-management") version "1.1.7"
      }
  
      repositories {
          mavenCentral()
      }
  
      dependencies {
          implementation('org.springframework.boot:spring-boot')
          implementation("org.apache.commons:commons-lang3:3.14.0")
          implementation("org.openrewrite:rewrite-core")
          implementation("javax.validation:validation-api")
          implementation("org.openrewrite.recipe:rewrite-quarkus")
      }
  
      dependencyManagement {
          dependencies {
              dependency("javax.validation:validation-api:2.0.1.Final")
              dependencySet('org.openrewrite:8.62.2') {
                  entry 'rewrite-core'
              }
          }
          imports {
              mavenBom "org.openrewrite.recipe:rewrite-recipe-bom:3.14.1"
          }
      }"
     but was: 
      "plugins {
          id "java"
          id("org.springframework.boot") version "3.5.6"
          id("io.spring.dependency-management") version "1.1.7"
      }
  
      repositories {
          mavenCentral()
      }
  
      dependencies {
          implementation('org.springframework.boot:spring-boot')
          implementation("org.apache.commons:commons-lang3")
          implementation("org.openrewrite:rewrite-core")
          implementation("javax.validation:validation-api")
          implementation("org.openrewrite.recipe:rewrite-quarkus")
      }
  
      dependencyManagement {
          dependencies {
              dependency("javax.validation:validation-api:2.0.1.Final")
              dependencySet('org.openrewrite:8.62.2') {
                  entry 'rewrite-core'
              }
          }
          imports {
              mavenBom "org.openrewrite.recipe:rewrite-recipe-bom:3.14.1"
          }
      }"
        at app//org.openrewrite.test.LargeSourceSetCheckingExpectedCycles.afterCycle(LargeSourceSetCheckingExpectedCycles.java:97)
        at app//org.openrewrite.RecipeScheduler.runRecipeCycles(RecipeScheduler.java:97)
        at app//org.openrewrite.RecipeScheduler.scheduleRun(RecipeScheduler.java:42)
        at app//org.openrewrite.Recipe.run(Recipe.java:451)
        at app//org.openrewrite.test.RewriteTest.rewriteRun(RewriteTest.java:400)
        at app//org.openrewrite.test.RewriteTest.rewriteRun(RewriteTest.java:132)
        at app//org.openrewrite.gradle.RemoveRedundantDependencyVersionsTest.supportSpringBootDependenciesPlugin(RemoveRedundantDependencyVersionsTest.java:741)

The change itself is good; it taking two cycles is unfortunate. Sharing here to decide what to do.

@steve-aom-elliott

Copy link
Copy Markdown
Contributor Author

@timtebeek So what's odd here is that test (which involves Gradle) failing on this run considering I've only changed the Maven AddDependency

@steve-aom-elliott steve-aom-elliott moved this from In Progress to Ready to Review in OpenRewrite Jan 8, 2026
@steve-aom-elliott
steve-aom-elliott marked this pull request as ready for review January 8, 2026 20:32
Comment thread rewrite-maven/src/test/java/org/openrewrite/maven/AddDependencyTest.java Outdated
Co-authored-by: Tim te Beek <tim@moderne.io>
@steve-aom-elliott
steve-aom-elliott merged commit 5ec28fc into main Jan 8, 2026
2 checks passed
@steve-aom-elliott
steve-aom-elliott deleted the 6426-add-dependency-skip-when-any-scope-already-present branch January 8, 2026 21:23
@github-project-automation github-project-automation Bot moved this from Ready to Review to Done in OpenRewrite Jan 8, 2026
knutwannheden added a commit that referenced this pull request Jan 14, 2026
* UUID generation fallback for Node version pre-14.17.0 (#6495)

* UUID generation fallback for Node version pre-14.17.0

* Apply suggestions from code review

* Use module-load-time selection for performance

* Polish

---------

Co-authored-by: Tim te Beek <tim@moderne.io>
Co-authored-by: Knut Wannheden <knut@moderne.io>

* Changing `AddDependencyVisitor` so it is able to broaden the scope of a direct dependency if needed (#6434)

* Allowing `AddDependency` to broaden the scope of existing dependency, provided you're requesting an equal or higher version number. If requesting same scope but higher version number, the version number will be upgraded.

* Dropping `import` scope validity in favour of `compile`, given `import` is only applicable for dependencies in `dependencyManagement`

Co-authored-by: Tim te Beek <tim@moderne.io>

---------

Co-authored-by: Tim te Beek <tim@moderne.io>

* Add style parameter to OrderImports. (#6496)

* Drop Lombok hint from RemoveUnusedImports class (#6500)

* Drop Lombok hint from RemoveUnusedImports class

Since we've supported Lombok for quite some time now.

* Also update recipes.csv

* In yaml `CopyValue`, invoke `UnfoldProperties` after `MergeYaml` (#6499)

* unit test with poc for UnfoldProperties

* use imperative CopyValue in unit test

* Invoke UnfoldProperties after MergeYaml

* Apply suggestions from code review

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Slight polish

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Tim te Beek <tim@moderne.io>

* Allow adding comments to Maven plugins too

Fixes #6502

* JavaScript: Make visitor base clases sync and add async alternative

* Polish parser APIs

* More async cleanups

* JavaScript: Notify about parsed source files from `parseProject()`

* Polish object property shorthand

* Fix Prettier integration when calling it using sync API

* Altering check for `AddDependency` to compare versions differently when direct vs indirect dependency. (#6505)

* If transitive, actually just allow regardless of version, as per old behaviour.

* Changing the parsing of the `relativePath` for `parent` in the `MavenPomDownloader` to prevent the situation where `/` was producing `/\pom.xml` on Windows rather than `\pom.xml` (#6506)

* Claude/catalogue async apis u67 hy (#6507)

* Convert package-manager and related APIs from async to sync

- Convert runInstallInTempDir and runWorkspaceInstallInTempDir to use sync
  fs APIs (fs.mkdtempSync, fs.writeFileSync, fs.readFileSync, fs.rmSync)
- Convert runInstallIfNeeded callback from async to sync
- Convert updateNodeResolutionMarker to sync (was marked async with no awaits)
- Convert createLockFileEditor to use sync JsonVisitor and TreeVisitor
- Update add-dependency, upgrade-dependency-version, and
  upgrade-transitive-dependency-version recipes to use sync visitors
- Convert Result.diff() to sync (createTwoFilesPatch is already sync)

The package manager operations were unnecessarily async since the underlying
process spawning (spawnSync) was already synchronous. Only the file I/O was
async, which is negligible compared to the npm/yarn/pnpm install time.

* Remove unnecessary async from IsSourceFile.preVisit()

* Add async property to TreeVisitor and AsyncTreeVisitor

Add a readonly `async` property to both visitor base classes:
- TreeVisitor: async = false
- AsyncTreeVisitor: async = true

This allows runtime discrimination of sync vs async visitors via the
RecipeVisitor union type. Useful for sync-to-sync recipe composition
where a sync visitor wants to call another sync visitor without async
overhead.

* Lift async I/O out of visitors into editorWithData()

Refactor package-manager recipes to do async I/O (npm install) in
editorWithData() before returning the visitor, keeping visitors pure.

Changes:
- Add async runInstallInTempDirAsync() using spawn() and fs.promises
- Add async runWorkspaceInstallInTempDirAsync() for workspace support
- Refactor AddDependency, UpgradeDependencyVersion, and
  UpgradeTransitiveDependencyVersion to run package manager installs
  in editorWithData() before returning visitors
- Remove unused runInstallIfNeeded() helper function
- Visitors are now pure tree transformations with no I/O

This provides a cleaner separation of concerns:
- Async I/O happens in the recipe's async editor() method
- Visitors are pure, synchronous tree transformations using pre-computed data

* More sync visitors

* Simplify receivers by extending visitors

---------

Co-authored-by: Claude <noreply@anthropic.com>

* Bugfix

* Add back runInstallIfNeeded()

* Also parse `jrxml` as XML

Fixes #6512

* Update description for onlyIfUsing option

Clarified description for 'onlyIfUsing' option to specify its importance in multi-module projects.

Fixes #5795

* Retain nested class imports in `ChangePackage` (#6515)

Fixes #6513

* Add Python language support (#6508)

* Compact array RpcObjectData only for JS

* Less async test code

---------

Co-authored-by: Benjamin Muschko <benjamin.muschko@gmail.com>
Co-authored-by: Tim te Beek <tim@moderne.io>
Co-authored-by: Steve Elliott <steve@moderne.io>
Co-authored-by: Sam Snyder <sam@moderne.io>
Co-authored-by: David Grieve <david@moderne.io>
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Jonathan Schnéider <jkschneider@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working maven test provided Already replicated with a unit test, using JUnit pioneer's ExpectedToFail

Projects

Archived in project

Development

Successfully merging this pull request may close these issues.

3 participants