├── gradle.properties ├── gradle ├── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties └── licenseHeader.txt ├── lombok.config ├── .gitignore ├── suppressions.xml ├── .editorconfig ├── .github ├── workflows │ ├── stale.yml │ ├── receive-pr.yml │ ├── repository-backup.yml │ ├── comment-pr.yml │ ├── publish.yml │ ├── ci.yml │ └── migrations.yml └── dependabot.yml ├── src ├── test │ ├── resources │ │ └── uc │ │ │ ├── og-definitions.json │ │ │ └── og-unofficial-definitions.json │ └── java │ │ └── org │ │ └── openrewrite │ │ └── java │ │ └── dependencies │ │ ├── internal │ │ └── StaticVersionComparatorTest.java │ │ ├── oldgroupids │ │ └── ParseDefinitionMigrationsTest.java │ │ ├── DependencyInsightTest.java │ │ ├── FindDependencyTest.java │ │ ├── AddDependencyTest.java │ │ ├── search │ │ ├── DoesNotIncludeDependencyTest.java │ │ └── FindMinimumDependencyVersionTest.java │ │ ├── UpgradeDependencyVersionTest.java │ │ ├── DependencyListTest.java │ │ ├── RemoveDependencyTest.java │ │ ├── DependencyResolutionDiagnosticTest.java │ │ └── ChangeDependencyTest.java └── main │ └── java │ └── org │ └── openrewrite │ └── java │ └── dependencies │ ├── package-info.java │ ├── search │ ├── package-info.java │ ├── DoesNotIncludeDependency.java │ ├── RepositoryHasDependency.java │ ├── ModuleHasDependency.java │ ├── FindMinimumJUnitVersion.java │ └── FindMinimumDependencyVersion.java │ ├── internal │ ├── package-info.java │ ├── Version.java │ ├── StaticVersionComparator.java │ └── VersionParser.java │ ├── oldgroupids │ ├── Migration.java │ └── ParseDefinitionMigrations.java │ ├── table │ ├── RelocatedDependencyReport.java │ ├── GradleDependencyConfigurationErrors.java │ ├── DependencyListReport.java │ └── RepositoryAccessibilityReport.java │ ├── DependencyInsight.java │ ├── FindDependency.java │ ├── RemoveDependency.java │ ├── ChangeDependency.java │ ├── UpgradeTransitiveDependencyVersion.java │ ├── UpgradeDependencyVersion.java │ ├── AddDependency.java │ └── DependencyList.java ├── settings.gradle.kts ├── .claude └── settings.json ├── README.md ├── gradlew.bat └── gradlew /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.caching=true 2 | org.gradle.parallel=true 3 | kotlin.stdlib.default.dependency=false 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openrewrite/rewrite-java-dependencies/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /lombok.config: -------------------------------------------------------------------------------- 1 | # https://projectlombok.org/features/configuration 2 | config.stopBubbling = true 3 | lombok.anyConstructor.addConstructorProperties=true 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | .gradle/ 3 | out/ 4 | .idea/ 5 | .classpath 6 | .project 7 | .settings/ 8 | bin/ 9 | *.log 10 | advisory-database/ 11 | oga-maven-plugin/ 12 | -------------------------------------------------------------------------------- /suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | insert_final_newline = true 5 | trim_trailing_whitespace = true 6 | 7 | [src/test*/java/**.java] 8 | indent_size = 4 9 | ij_continuation_indent_size = 2 10 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: stale 3 | 4 | on: 5 | workflow_dispatch: {} 6 | schedule: 7 | - cron: 36 4 * * 1 8 | 9 | jobs: 10 | build: 11 | uses: openrewrite/gh-automation/.github/workflows/stale.yml@main 12 | -------------------------------------------------------------------------------- /src/test/resources/uc/og-definitions.json: -------------------------------------------------------------------------------- 1 | { 2 | "date": "2024/12/05", 3 | "migration": [ 4 | { 5 | "old": "acegisecurity", 6 | "new": "org.acegisecurity" 7 | }, 8 | { 9 | "old": "activation:activation", 10 | "new": "javax.activation:activation" 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/test/resources/uc/og-unofficial-definitions.json: -------------------------------------------------------------------------------- 1 | { 2 | "date": "2024/01/12", 3 | "migration": [ 4 | { 5 | "old": "com.jcraft:jsch", 6 | "proposal": [ 7 | "com.github.mwiede:jsch" 8 | ], 9 | "context": "See https://www.matez.de/index.php/2020/06/22/the-future-of-jsch-without-ssh-rsa/" 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | distributionSha256Sum=72f44c9f8ebcb1af43838f45ee5c4aa9c5444898b3468ab3f4af7b6076c5bc3f 8 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: 2 3 | updates: 4 | - package-ecosystem: github-actions 5 | directory: / 6 | schedule: 7 | interval: daily 8 | commit-message: 9 | prefix: "chore(ci)" 10 | - package-ecosystem: gradle 11 | directory: / 12 | schedule: 13 | interval: daily 14 | commit-message: 15 | prefix: "chore(ci)" 16 | open-pull-requests-limit: 0 17 | -------------------------------------------------------------------------------- /.github/workflows/receive-pr.yml: -------------------------------------------------------------------------------- 1 | name: receive-pr 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize] 6 | branches: 7 | - main 8 | 9 | concurrency: 10 | group: '${{ github.workflow }} @ ${{ github.ref }}' 11 | cancel-in-progress: true 12 | 13 | # https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ 14 | # Since this pull request receives untrusted code, we should **NOT** have any secrets in the environment. 15 | jobs: 16 | upload-patch: 17 | uses: openrewrite/gh-automation/.github/workflows/receive-pr.yml@main -------------------------------------------------------------------------------- /gradle/licenseHeader.txt: -------------------------------------------------------------------------------- 1 | Copyright ${year} the original author or authors. 2 |

3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 |

7 | https://www.apache.org/licenses/LICENSE-2.0 8 |

9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. -------------------------------------------------------------------------------- /.github/workflows/repository-backup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: repository-backup 3 | on: 4 | workflow_dispatch: {} 5 | schedule: 6 | - cron: 0 17 * * * 7 | 8 | concurrency: 9 | group: backup-${{ github.ref }} 10 | cancel-in-progress: false 11 | 12 | jobs: 13 | repository-backup: 14 | uses: openrewrite/gh-automation/.github/workflows/repository-backup.yml@main 15 | secrets: 16 | bucket_mirror_target: ${{ secrets.S3_GITHUB_REPOSITORY_BACKUPS_BUCKET_NAME }} 17 | bucket_access_key_id: ${{ secrets.S3_GITHUB_REPOSITORY_BACKUPS_ACCESS_KEY_ID }} 18 | bucket_secret_access_key: ${{ secrets.S3_GITHUB_REPOSITORY_BACKUPS_SECRET_ACCESS_KEY }} 19 | -------------------------------------------------------------------------------- /.github/workflows/comment-pr.yml: -------------------------------------------------------------------------------- 1 | name: comment-pr 2 | 3 | # https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#using-data-from-the-triggering-workflow 4 | on: 5 | workflow_run: 6 | workflows: ["receive-pr"] 7 | types: 8 | - completed 9 | 10 | # https://securitylab.github.com/research/github-actions-preventing-pwn-requests/ 11 | # Since this pull request has write permissions on the target repo, we should **NOT** execute any untrusted code. 12 | jobs: 13 | post-suggestions: 14 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 15 | uses: openrewrite/gh-automation/.github/workflows/comment-pr.yml@main 16 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: publish 3 | 4 | on: 5 | push: 6 | tags: 7 | - v[0-9]+.[0-9]+.[0-9]+ 8 | - v[0-9]+.[0-9]+.[0-9]+-rc.[0-9]+ 9 | 10 | concurrency: 11 | group: publish-${{ github.ref }} 12 | cancel-in-progress: false 13 | 14 | jobs: 15 | release: 16 | uses: openrewrite/gh-automation/.github/workflows/publish-gradle.yml@main 17 | secrets: 18 | gradle_enterprise_access_key: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} 19 | sonatype_username: ${{ secrets.SONATYPE_USERNAME }} 20 | sonatype_token: ${{ secrets.SONATYPE_TOKEN}} 21 | ossrh_signing_key: ${{ secrets.OSSRH_SIGNING_KEY }} 22 | ossrh_signing_password: ${{ secrets.OSSRH_SIGNING_PASSWORD }} 23 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: ci 3 | 4 | on: 5 | push: 6 | branches: 7 | - main 8 | tags-ignore: 9 | - "*" 10 | pull_request: 11 | branches: 12 | - main 13 | workflow_dispatch: {} 14 | schedule: 15 | - cron: 0 17 * * * 16 | 17 | concurrency: 18 | group: ci-${{ github.ref }} 19 | cancel-in-progress: true 20 | 21 | jobs: 22 | build: 23 | uses: openrewrite/gh-automation/.github/workflows/ci-gradle.yml@main 24 | secrets: 25 | gradle_enterprise_access_key: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} 26 | sonatype_username: ${{ secrets.SONATYPE_USERNAME }} 27 | sonatype_token: ${{ secrets.SONATYPE_TOKEN}} 28 | ossrh_signing_key: ${{ secrets.OSSRH_SIGNING_KEY }} 29 | ossrh_signing_password: ${{ secrets.OSSRH_SIGNING_PASSWORD }} 30 | OPS_GITHUB_ACTIONS_WEBHOOK: ${{ secrets.OPS_GITHUB_ACTIONS_WEBHOOK }} 31 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/dependencies/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | @NullMarked 17 | @NonNullFields 18 | package org.openrewrite.java.dependencies; 19 | 20 | import org.jspecify.annotations.NullMarked; 21 | import org.openrewrite.internal.lang.NonNullFields; 22 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/dependencies/search/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | @NullMarked 17 | @NonNullFields 18 | package org.openrewrite.java.dependencies.search; 19 | 20 | import org.jspecify.annotations.NullMarked; 21 | import org.openrewrite.internal.lang.NonNullFields; 22 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/dependencies/internal/package-info.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | @NullMarked 17 | @NonNullFields 18 | package org.openrewrite.java.dependencies.internal; 19 | 20 | import org.jspecify.annotations.NullMarked; 21 | import org.openrewrite.internal.lang.NonNullFields; 22 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "rewrite-java-dependencies" 2 | 3 | plugins { 4 | id("com.gradle.develocity") version "latest.release" 5 | id("com.gradle.common-custom-user-data-gradle-plugin") version "latest.release" 6 | } 7 | 8 | develocity { 9 | server = "https://ge.openrewrite.org/" 10 | val isCiServer = System.getenv("CI")?.equals("true") ?: false 11 | val accessKey = System.getenv("GRADLE_ENTERPRISE_ACCESS_KEY") 12 | val authenticated = !accessKey.isNullOrBlank() 13 | buildCache { 14 | remote(develocity.buildCache) { 15 | isEnabled = true 16 | isPush = isCiServer && authenticated 17 | } 18 | } 19 | 20 | buildScan { 21 | capture { 22 | fileFingerprints = true 23 | } 24 | 25 | publishing { 26 | onlyIf { 27 | authenticated 28 | } 29 | } 30 | 31 | uploadInBackground = !isCiServer 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.claude/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "permissions": { 3 | "allow": [ 4 | "Bash(./gradlew:*)", 5 | "Bash(cat:*)", 6 | "Bash(echo:*)", 7 | "Bash(find:*)", 8 | "Bash(gh:*)", 9 | "Bash(git:*)", 10 | "Bash(grep:*)", 11 | "Bash(javac:*)", 12 | "Bash(mv:*)", 13 | "Bash(npm test:*)", 14 | "Bash(rg:*)", 15 | "Bash(rm:*)", 16 | "Bash(timeout:*)", 17 | "WebFetch(domain:github.com)", 18 | "mcp__github__get_issue", 19 | "mcp__github__get_issue_comments", 20 | "mcp__github__get_pull_request", 21 | "mcp__github__get_pull_request_comments", 22 | "mcp__idea__find_files_by_name_substring", 23 | "mcp__idea__get_file_text_by_path", 24 | "mcp__idea__get_open_in_editor_file_path", 25 | "mcp__idea__get_open_in_editor_file_text", 26 | "mcp__idea__get_selected_in_editor_text", 27 | "mcp__idea__list_directory_tree_in_folder", 28 | "mcp__idea__list_files_in_folder", 29 | "mcp__idea__open_file_in_editor", 30 | "mcp__idea__replace_selected_text", 31 | "mcp__idea__replace_specific_text", 32 | "mcp__idea__search_in_files_content" 33 | ], 34 | "deny": [] 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/dependencies/internal/Version.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.openrewrite.java.dependencies.internal; 18 | 19 | public interface Version { 20 | /** 21 | * Returns the original {@link String} representation of the version. 22 | */ 23 | String getSource(); 24 | 25 | /** 26 | * Returns all the parts of this version. e.g. 1.2.3 returns [1,2,3] or 1.2-beta4 returns [1,2,beta,4]. 27 | */ 28 | String[] getParts(); 29 | 30 | /** 31 | * Returns all the numeric parts of this version as {@link Long}, with nulls in non-numeric positions. eg. 1.2.3 returns [1,2,3] or 1.2-beta4 returns [1,2,null,4]. 32 | */ 33 | Long[] getNumericParts(); 34 | } 35 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/dependencies/oldgroupids/Migration.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies.oldgroupids; 17 | 18 | import com.fasterxml.jackson.annotation.JsonPropertyOrder; 19 | import lombok.AllArgsConstructor; 20 | import lombok.Data; 21 | import org.jspecify.annotations.Nullable; 22 | 23 | @AllArgsConstructor 24 | @Data 25 | @JsonPropertyOrder({"oldGroupId", "oldArtifactId", "newGroupId", "newArtifactId", "context"}) 26 | public class Migration { 27 | String oldGroupId; 28 | 29 | @Nullable 30 | String oldArtifactId; 31 | 32 | String newGroupId; 33 | 34 | @Nullable 35 | String newArtifactId; 36 | 37 | @Nullable 38 | String context; 39 | } 40 | -------------------------------------------------------------------------------- /src/test/java/org/openrewrite/java/dependencies/internal/StaticVersionComparatorTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies.internal; 17 | 18 | import org.junit.jupiter.api.Test; 19 | 20 | import static org.assertj.core.api.Assertions.assertThat; 21 | 22 | 23 | class StaticVersionComparatorTest { 24 | VersionParser vp = new VersionParser(); 25 | StaticVersionComparator svc = new StaticVersionComparator(); 26 | 27 | @Test 28 | void milestone() { 29 | assertThat(svc.compare(v("2.0.0"), v("1.0.0"))).isOne(); 30 | assertThat(svc.compare(v("1.0.0"), v("1.0.0-M1"))).isOne(); 31 | assertThat(svc.compare(v("1.0.0-M2"), v("1.0.0-M1"))).isOne(); 32 | assertThat(svc.compare(v("1.0.0-rc-1"), v("1.0.0-M1"))).isOne(); 33 | } 34 | 35 | Version v(String version) { 36 | return vp.transform(version); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /.github/workflows/migrations.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Update the Old GroupId migrations CSV 3 | 4 | on: 5 | workflow_dispatch: {} 6 | schedule: 7 | - cron: 0 10 * * MON 8 | 9 | jobs: 10 | update-migrations: 11 | if: github.event_name != 'schedule' || github.repository_owner == 'openrewrite' 12 | runs-on: ubuntu-latest 13 | steps: 14 | # Checkout and build parser 15 | - name: Checkout 16 | uses: actions/checkout@v6 17 | with: 18 | fetch-depth: 0 19 | - uses: actions/setup-java@v5 20 | with: 21 | cache: 'gradle' 22 | distribution: 'temurin' 23 | java-version: '21' 24 | 25 | # Update migrations 26 | - name: Checkout oga-maven-plugin 27 | uses: actions/checkout@v6 28 | with: 29 | repository: jonathanlermitage/oga-maven-plugin 30 | path: oga-maven-plugin 31 | - name: Update migrations 32 | run: ./gradlew parseDefinitionMigrations --args="./oga-maven-plugin src/main/resources/migrations.csv" 33 | 34 | # Commit and push 35 | - name: configure-git-user 36 | run: | 37 | git config user.email "team@moderne.io" 38 | git config user.name "team-moderne[bot]" 39 | - name: Create timestamp 40 | run: echo "NOW=$(date +'%Y-%m-%dT%H%M')" >> $GITHUB_ENV 41 | - name: Commit and push 42 | run: | 43 | git add src/main/resources/migrations.csv 44 | git diff --quiet HEAD || (git commit -m "[Auto] Old GroupId migrations as of ${{ env.NOW }}" && git push origin main) 45 | -------------------------------------------------------------------------------- /src/test/java/org/openrewrite/java/dependencies/oldgroupids/ParseDefinitionMigrationsTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies.oldgroupids; 17 | 18 | import org.junit.jupiter.api.Test; 19 | import org.junit.jupiter.api.io.TempDir; 20 | 21 | import java.io.File; 22 | import java.nio.file.Path; 23 | 24 | import static org.assertj.core.api.Assertions.assertThat; 25 | 26 | class ParseDefinitionMigrationsTest { 27 | @Test 28 | void parseDefinitionMigrations(@TempDir Path tempDir) throws Exception { 29 | Path csv = tempDir.resolve("migrations.csv"); 30 | ParseDefinitionMigrations.parseDefinitionMigrations(new File("src/test/resources"), csv.toFile()); 31 | assertThat(csv).hasContent(""" 32 | acegisecurity,,org.acegisecurity,, 33 | activation,activation,javax.activation,activation, 34 | com.jcraft,jsch,com.github.mwiede,jsch,"See https://www.matez.de/index.php/2020/06/22/the-future-of-jsch-without-ssh-rsa/" 35 | """); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | 6 | OpenRewrite Logo 7 | 8 | 9 |

10 | 11 |
12 |

rewrite-java-dependencies

13 |
14 | 15 |
16 | 17 | 18 | [![ci](https://github.com/openrewrite/rewrite-java-dependencies/actions/workflows/ci.yml/badge.svg)](https://github.com/openrewrite/rewrite-java-dependencies/actions/workflows/ci.yml) 19 | [![Apache 2.0](https://img.shields.io/github/license/openrewrite/rewrite-java-dependencies.svg)](https://www.apache.org/licenses/LICENSE-2.0) 20 | [![Maven Central](https://img.shields.io/maven-central/v/org.openrewrite.recipe/rewrite-java-dependencies.svg)](https://mvnrepository.com/artifact/org.openrewrite.recipe/rewrite-java-dependencies) 21 | [![Revved up by Develocity](https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.openrewrite.org/scans) 22 | [![Contributing Guide](https://img.shields.io/badge/Contributing-Guide-informational)](https://github.com/openrewrite/.github/blob/main/CONTRIBUTING.md) 23 |
24 | 25 | ### What is this? 26 | 27 | This project implements a [Rewrite module](https://github.com/openrewrite/rewrite) that performs common Java dependency management tasks. 28 | 29 | Browse [a selection of recipes available through this module in the recipe catalog](https://docs.openrewrite.org/recipes/java/dependencies). 30 | 31 | ## Contributing 32 | 33 | We appreciate all types of contributions. See the [contributing guide](https://github.com/openrewrite/.github/blob/main/CONTRIBUTING.md) for detailed instructions on how to get started. 34 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/dependencies/table/RelocatedDependencyReport.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies.table; 17 | 18 | import com.fasterxml.jackson.annotation.JsonIgnoreType; 19 | import lombok.Value; 20 | import org.jspecify.annotations.Nullable; 21 | import org.openrewrite.Column; 22 | import org.openrewrite.DataTable; 23 | import org.openrewrite.Recipe; 24 | 25 | @JsonIgnoreType 26 | public class RelocatedDependencyReport extends DataTable { 27 | public RelocatedDependencyReport(Recipe recipe) { 28 | super(recipe, 29 | "Relocated dependencies", 30 | "A list of dependencies in use that have relocated."); 31 | } 32 | 33 | @Value 34 | public static class Row { 35 | @Column(displayName = "Dependency group id", 36 | description = "The Group ID of the dependency in use.") 37 | String dependencyGroupId; 38 | 39 | @Column(displayName = "Dependency artifact id", 40 | description = "The Artifact ID of the dependency in use.") 41 | @Nullable 42 | String dependencyArtifactId; 43 | 44 | @Column(displayName = "Relocated dependency group id", 45 | description = "The Group ID of the relocated dependency.") 46 | String relocatedGroupId; 47 | 48 | @Column(displayName = "Relocated ependency artifact id", 49 | description = "The Artifact ID of the relocated dependency.") 50 | @Nullable 51 | String relocatedArtifactId; 52 | 53 | @Column(displayName = "Context", 54 | description = "Context for the relocation, if any.") 55 | @Nullable 56 | String context; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/dependencies/table/GradleDependencyConfigurationErrors.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies.table; 17 | 18 | import com.fasterxml.jackson.annotation.JsonIgnoreType; 19 | import lombok.Value; 20 | import org.openrewrite.Column; 21 | import org.openrewrite.DataTable; 22 | import org.openrewrite.Recipe; 23 | 24 | @JsonIgnoreType 25 | public class GradleDependencyConfigurationErrors extends DataTable { 26 | public GradleDependencyConfigurationErrors(Recipe recipe) { 27 | super(recipe, "Gradle dependency configuration errors", 28 | "Records Gradle dependency configurations which failed to resolve during parsing. " + 29 | "Partial success/failure is common, a failure in this list does not mean that every dependency failed to resolve."); 30 | } 31 | 32 | @Value 33 | public static class Row { 34 | @Column(displayName = "Project path", 35 | description = "The path of the project which contains the dependency configuration.") 36 | String projectPath; 37 | 38 | @Column(displayName = "Configuration name", 39 | description = "The name of the dependency configuration which failed to resolve.") 40 | String configurationName; 41 | 42 | @Column(displayName = "Exception type", 43 | description = "The type of exception encountered when attempting to resolve the dependency configuration.") 44 | String exceptionType; 45 | 46 | @Column(displayName = "Error message", 47 | description = "The error message encountered when attempting to resolve the dependency configuration.") 48 | String exceptionMessage; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/test/java/org/openrewrite/java/dependencies/DependencyInsightTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies; 17 | 18 | import org.junit.jupiter.api.Test; 19 | import org.openrewrite.DocumentExample; 20 | import org.openrewrite.test.RecipeSpec; 21 | import org.openrewrite.test.RewriteTest; 22 | 23 | import static org.openrewrite.maven.Assertions.pomXml; 24 | 25 | class DependencyInsightTest implements RewriteTest { 26 | 27 | @Override 28 | public void defaults(RecipeSpec spec) { 29 | spec.recipe(new DependencyInsight("org.springframework*", "*", null, null)); 30 | } 31 | 32 | @DocumentExample 33 | @Test 34 | void maven() { 35 | rewriteRun( 36 | //language=xml 37 | pomXml( 38 | """ 39 | 40 | com.example 41 | foo 42 | 1.0.0 43 | 44 | 45 | 46 | org.springframework 47 | spring-core 48 | 5.2.6.RELEASE 49 | 50 | 51 | 52 | """, 53 | """ 54 | 55 | com.example 56 | foo 57 | 1.0.0 58 | 59 | 60 | 61 | org.springframework 62 | spring-core 63 | 5.2.6.RELEASE 64 | 65 | 66 | 67 | """ 68 | ) 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/dependencies/table/DependencyListReport.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies.table; 17 | 18 | import com.fasterxml.jackson.annotation.JsonIgnoreType; 19 | import lombok.Value; 20 | import org.openrewrite.Column; 21 | import org.openrewrite.DataTable; 22 | import org.openrewrite.Recipe; 23 | 24 | @JsonIgnoreType 25 | public class DependencyListReport extends DataTable { 26 | public DependencyListReport(Recipe recipe) { 27 | super(recipe, 28 | "Dependency report", 29 | "Lists all Gradle and Maven dependencies"); 30 | } 31 | 32 | @Value 33 | public static class Row { 34 | 35 | @Column(displayName = "Build tool", 36 | description = "The build tool used to manage dependencies (Gradle or Maven).") 37 | String buildTool; 38 | 39 | @Column(displayName = "Group id", 40 | description = "The Group ID of the Gradle project or Maven module requesting the dependency.") 41 | String groupId; 42 | 43 | @Column(displayName = "Artifact id", 44 | description = "The Artifact ID of the Gradle project or Maven module requesting the dependency.") 45 | String artifactId; 46 | 47 | @Column(displayName = "Version", 48 | description = "The version of Gradle project or Maven module requesting the dependency.") 49 | String version; 50 | 51 | @Column(displayName = "Dependency group id", 52 | description = "The Group ID of the dependency.") 53 | String dependencyGroupId; 54 | 55 | @Column(displayName = "Dependency artifact id", 56 | description = "The Artifact ID of the dependency.") 57 | String dependencyArtifactId; 58 | 59 | @Column(displayName = "Dependency version", 60 | description = "The version of the dependency.") 61 | String dependencyVersion; 62 | 63 | @Column(displayName = "Direct Dependency", 64 | description = "When `true` the project directly depends on the dependency. When `false` the project " + 65 | "depends on the dependency transitively through at least one direct dependency.") 66 | boolean direct; 67 | 68 | @Column(displayName = "Resolution failure", 69 | description = "The reason why the dependency could not be resolved. Blank when resolution was not attempted.") 70 | String resolutionFailure; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/dependencies/table/RepositoryAccessibilityReport.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies.table; 17 | 18 | import com.fasterxml.jackson.annotation.JsonIgnoreType; 19 | import lombok.Value; 20 | import org.jspecify.annotations.Nullable; 21 | import org.openrewrite.Column; 22 | import org.openrewrite.DataTable; 23 | import org.openrewrite.Recipe; 24 | 25 | @JsonIgnoreType 26 | public class RepositoryAccessibilityReport extends DataTable { 27 | 28 | public RepositoryAccessibilityReport(Recipe recipe) { 29 | super(recipe, 30 | "Repository accessibility report", 31 | "Listing of all dependency repositories and whether they are accessible."); 32 | 33 | } 34 | 35 | @Value 36 | public static class Row { 37 | @Column(displayName = "Repository URI", 38 | description = "The URI of the repository") 39 | String uri; 40 | 41 | @Column(displayName = "Ping exception type", 42 | description = "Empty if the repository responded to a ping. Otherwise, the type of exception encountered " + 43 | "when attempting to access the repository.") 44 | String pingExceptionType; 45 | 46 | @Column(displayName = "Ping error message", 47 | description = "Empty if the repository was accessible. Otherwise, the error message encountered when " + 48 | "attempting to access the repository.") 49 | String pingExceptionMessage; 50 | 51 | @Column(displayName = "Ping HTTP code", 52 | description = "The HTTP response code returned by the repository. May be empty for non-HTTP repositories.") 53 | @Nullable 54 | Integer pingHttpCode; 55 | 56 | @Column(displayName = "Dependency resolution exception type", 57 | description = "Empty if ping failed, or if the repository successfully downloaded the specified dependency. Otherwise, the type of exception encountered " + 58 | "when attempting to access the repository.") 59 | String dependencyResolveExceptionType; 60 | 61 | @Column(displayName = "Dependency resolution error message", 62 | description = "Empty if ping failed, or if the repository successfully downloaded the specified dependency. Otherwise, the error message encountered when " + 63 | "attempting to access the repository.") 64 | String dependencyResolveExceptionMessage; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | 74 | 75 | @rem Execute Gradle 76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 77 | 78 | :end 79 | @rem End local scope for the variables with windows NT shell 80 | if %ERRORLEVEL% equ 0 goto mainEnd 81 | 82 | :fail 83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 84 | rem the _cmd.exe /c_ return code! 85 | set EXIT_CODE=%ERRORLEVEL% 86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 88 | exit /b %EXIT_CODE% 89 | 90 | :mainEnd 91 | if "%OS%"=="Windows_NT" endlocal 92 | 93 | :omega 94 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/dependencies/DependencyInsight.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies; 17 | 18 | import lombok.EqualsAndHashCode; 19 | import lombok.Value; 20 | import org.jspecify.annotations.Nullable; 21 | import org.openrewrite.*; 22 | import org.openrewrite.maven.table.DependenciesInUse; 23 | 24 | @EqualsAndHashCode(callSuper = false) 25 | @Value 26 | public class DependencyInsight extends Recipe { 27 | // Populated by the other DependencyInsight visitors. Listed here so the saas knows to display this when the recipe runs 28 | transient DependenciesInUse dependenciesInUse = new DependenciesInUse(this); 29 | 30 | @Override 31 | public String getDisplayName() { 32 | return "Dependency insight for Gradle and Maven"; 33 | } 34 | 35 | @Override 36 | public String getDescription() { 37 | return "Finds dependencies, including transitive dependencies, in both Gradle and Maven projects. " + 38 | "Matches within all Gradle dependency configurations and Maven scopes."; 39 | } 40 | 41 | @Option(displayName = "Group pattern", 42 | description = "Group ID glob pattern used to match dependencies.", 43 | example = "com.fasterxml.jackson*") 44 | String groupIdPattern; 45 | 46 | @Option(displayName = "Artifact pattern", 47 | description = "Artifact ID glob pattern used to match dependencies.", 48 | example = "jackson-*") 49 | String artifactIdPattern; 50 | 51 | @Option(displayName = "Version", 52 | description = "Match only dependencies with the specified version. " + 53 | "Node-style [version selectors](https://docs.openrewrite.org/reference/dependency-version-selectors) may be used. " + 54 | "All versions are searched by default.", 55 | example = "1.x", 56 | required = false) 57 | @Nullable 58 | String version; 59 | 60 | @Option(displayName = "Scope", 61 | description = "Match dependencies with the specified Maven scope. All scopes are searched by default.", 62 | valid = {"compile", "test", "runtime", "provided", "system"}, 63 | example = "compile", 64 | required = false) 65 | @Nullable 66 | String scope; 67 | 68 | @Override 69 | public TreeVisitor getVisitor() { 70 | return new TreeVisitor() { 71 | final TreeVisitor gdi = new org.openrewrite.gradle.search.DependencyInsight(groupIdPattern, artifactIdPattern, version, null) 72 | .getVisitor(); 73 | final TreeVisitor mdi = new org.openrewrite.maven.search.DependencyInsight(groupIdPattern, artifactIdPattern, scope, version, false) 74 | .getVisitor(); 75 | 76 | @Override 77 | public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { 78 | if (!(tree instanceof SourceFile)) { 79 | return tree; 80 | } 81 | SourceFile s = (SourceFile) tree; 82 | if (gdi.isAcceptable(s, ctx)) { 83 | s = (SourceFile) gdi.visitNonNull(s, ctx); 84 | } else if (mdi.isAcceptable(s, ctx)) { 85 | s = (SourceFile) mdi.visitNonNull(s, ctx); 86 | } 87 | return s; 88 | } 89 | }; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/dependencies/internal/StaticVersionComparator.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.openrewrite.java.dependencies.internal; 18 | 19 | import java.util.Comparator; 20 | import java.util.HashMap; 21 | import java.util.Locale; 22 | import java.util.Map; 23 | 24 | /** 25 | * Allows for comparison of Version instances. 26 | * Note that this comparator only considers the 'parts' of a version, and does not consider the part 'separators'. 27 | * This means that it considers `1.1.1 == 1-1-1 == 1.1-1`, and should not be used in cases where this is important. 28 | * One example where this comparator is inappropriate is if versions should be retained in a TreeMap/TreeSet. 29 | */ 30 | public class StaticVersionComparator implements Comparator { 31 | static final Map SPECIAL_MEANINGS = new HashMap<>(); 32 | 33 | static { 34 | SPECIAL_MEANINGS.put("dev", -1); 35 | SPECIAL_MEANINGS.put("m", 1); 36 | SPECIAL_MEANINGS.put("rc", 2); 37 | SPECIAL_MEANINGS.put("snapshot", 3); 38 | SPECIAL_MEANINGS.put("final", 4); 39 | SPECIAL_MEANINGS.put("ga", 5); 40 | SPECIAL_MEANINGS.put("release", 6); 41 | SPECIAL_MEANINGS.put("sp", 7); 42 | } 43 | 44 | /** 45 | * Compares 2 versions. Algorithm is inspired by PHP version_compare one. 46 | */ 47 | @Override 48 | public int compare(Version version1, Version version2) { 49 | if (version1.equals(version2)) { 50 | return 0; 51 | } 52 | 53 | String[] parts1 = version1.getParts(); 54 | String[] parts2 = version2.getParts(); 55 | Long[] numericParts1 = version1.getNumericParts(); 56 | Long[] numericParts2 = version2.getNumericParts(); 57 | 58 | int i = 0; 59 | for (; i < parts1.length && i < parts2.length; i++) { 60 | String part1 = parts1[i]; 61 | String part2 = parts2[i]; 62 | 63 | Long numericPart1 = numericParts1[i]; 64 | Long numericPart2 = numericParts2[i]; 65 | 66 | boolean is1Number = numericPart1 != null; 67 | boolean is2Number = numericPart2 != null; 68 | 69 | if (part1.equals(part2)) { 70 | continue; 71 | } 72 | if (is1Number && !is2Number) { 73 | return 1; 74 | } 75 | if (is2Number && !is1Number) { 76 | return -1; 77 | } 78 | if (is1Number && is2Number) { 79 | int result = numericPart1.compareTo(numericPart2); 80 | if (result == 0) { 81 | continue; 82 | } 83 | return result; 84 | } 85 | // both are strings, we compare them taking into account special meaning 86 | Integer sm1 = SPECIAL_MEANINGS.get(part1.toLowerCase(Locale.US)); 87 | Integer sm2 = SPECIAL_MEANINGS.get(part2.toLowerCase(Locale.US)); 88 | if (sm1 != null) { 89 | sm2 = sm2 == null ? 0 : sm2; 90 | return sm1 - sm2; 91 | } 92 | if (sm2 != null) { 93 | return -sm2; 94 | } 95 | return part1.compareTo(part2); 96 | } 97 | if (i < parts1.length) { 98 | return numericParts1[i] == null ? -1 : 1; 99 | } 100 | if (i < parts2.length) { 101 | return numericParts2[i] == null ? 1 : -1; 102 | } 103 | 104 | return 0; 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/dependencies/FindDependency.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies; 17 | 18 | import lombok.EqualsAndHashCode; 19 | import lombok.Value; 20 | import org.jspecify.annotations.Nullable; 21 | import org.openrewrite.*; 22 | 23 | @Value 24 | @EqualsAndHashCode(callSuper = false) 25 | public class FindDependency extends Recipe { 26 | 27 | @Option(displayName = "Group ID", 28 | description = "The first part of a dependency coordinate identifying its publisher.", 29 | example = "com.google.guava") 30 | String groupId; 31 | 32 | @Option(displayName = "Artifact ID", 33 | description = "The second part of a dependency coordinate uniquely identifying it among artifacts from the same publisher.", 34 | example = "guava") 35 | String artifactId; 36 | 37 | @Option(displayName = "Version", 38 | description = "An exact version number or node-style semver selector used to select the version number.", 39 | example = "3.0.0", 40 | required = false) 41 | @Nullable 42 | String version; 43 | 44 | @Option(displayName = "Version pattern", 45 | description = "Allows version selection to be extended beyond the original Node Semver semantics. " + 46 | "So for example, setting 'version' to \"25-29\" can be paired with a metadata pattern of \"-jre\" to select Guava 29.0-jre", 47 | example = "-jre", 48 | required = false) 49 | @Nullable 50 | String versionPattern; 51 | 52 | @Option(displayName = "Configuration", 53 | description = "For Gradle only, the dependency configuration to search for dependencies in. If omitted then all configurations will be searched.", 54 | example = "api", 55 | required = false) 56 | @Nullable 57 | String configuration; 58 | 59 | @Override 60 | public String getDisplayName() { 61 | return "Find Maven and Gradle dependencies"; 62 | } 63 | 64 | @Override 65 | public String getDescription() { 66 | return "Finds direct dependencies declared in Maven and Gradle build files. " + 67 | "This does *not* search transitive dependencies. " + 68 | "To detect both direct and transitive dependencies use `org.openrewrite.java.dependencies.DependencyInsight` " + 69 | "This recipe works for both Maven and Gradle projects."; 70 | } 71 | 72 | @Override 73 | public TreeVisitor getVisitor() { 74 | return new TreeVisitor() { 75 | 76 | final TreeVisitor mavenFindDependency = new org.openrewrite.maven.search.FindDependency(groupId,artifactId,version,versionPattern) 77 | .getVisitor(); 78 | final TreeVisitor gradleFindDependency = new org.openrewrite.gradle.search.FindDependency(groupId, artifactId, configuration, version, versionPattern) 79 | .getVisitor(); 80 | 81 | @Override 82 | public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { 83 | if (!(tree instanceof SourceFile)) { 84 | return tree; 85 | } 86 | SourceFile s = (SourceFile) tree; 87 | if (mavenFindDependency.isAcceptable(s, ctx)) { 88 | return mavenFindDependency.visitNonNull(tree, ctx); 89 | } 90 | if (gradleFindDependency.isAcceptable(s, ctx)) { 91 | // Handle Gradle projects 92 | return gradleFindDependency.visitNonNull(tree, ctx); 93 | } 94 | return s; 95 | } 96 | }; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/dependencies/internal/VersionParser.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package org.openrewrite.java.dependencies.internal; 18 | 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | import java.util.Map; 22 | import java.util.concurrent.ConcurrentHashMap; 23 | 24 | public class VersionParser { 25 | private final Map cache = new ConcurrentHashMap<>(); 26 | 27 | public Version transform(String original) { 28 | return cache.computeIfAbsent(original, this::parse); 29 | } 30 | 31 | private Version parse(String original) { 32 | List parts = new ArrayList<>(); 33 | boolean digit = false; 34 | int startPart = 0; 35 | int pos = 0; 36 | int endBaseStr = 0; 37 | for (; pos < original.length(); pos++) { 38 | char ch = original.charAt(pos); 39 | if (ch == '.' || ch == '_' || ch == '-' || ch == '+') { 40 | parts.add(original.substring(startPart, pos)); 41 | startPart = pos + 1; 42 | digit = false; 43 | if (ch != '.' && endBaseStr == 0) { 44 | endBaseStr = pos; 45 | } 46 | } else if (ch >= '0' && ch <= '9') { 47 | if (!digit && pos > startPart) { 48 | if (endBaseStr == 0) { 49 | endBaseStr = pos; 50 | } 51 | parts.add(original.substring(startPart, pos)); 52 | startPart = pos; 53 | } 54 | digit = true; 55 | } else { 56 | if (digit) { 57 | if (endBaseStr == 0) { 58 | endBaseStr = pos; 59 | } 60 | parts.add(original.substring(startPart, pos)); 61 | startPart = pos; 62 | } 63 | digit = false; 64 | } 65 | } 66 | if (pos > startPart) { 67 | parts.add(original.substring(startPart, pos)); 68 | } 69 | return new DefaultVersion(original, parts); 70 | } 71 | 72 | private static class DefaultVersion implements Version { 73 | private final String source; 74 | private final String[] parts; 75 | private final Long[] numericParts; 76 | 77 | public DefaultVersion(String source, List parts) { 78 | this.source = source; 79 | this.parts = parts.toArray(new String[0]); 80 | this.numericParts = new Long[this.parts.length]; 81 | for (int i = 0; i < parts.size(); i++) { 82 | try { 83 | this.numericParts[i] = Long.parseLong(this.parts[i]); 84 | } catch (NumberFormatException ignored) { 85 | } 86 | } 87 | } 88 | 89 | @Override 90 | public String toString() { 91 | return source; 92 | } 93 | 94 | @Override 95 | public boolean equals(Object obj) { 96 | if (obj == this) { 97 | return true; 98 | } 99 | if (obj == null || obj.getClass() != getClass()) { 100 | return false; 101 | } 102 | DefaultVersion other = (DefaultVersion) obj; 103 | return source.equals(other.source); 104 | } 105 | 106 | @Override 107 | public int hashCode() { 108 | return source.hashCode(); 109 | } 110 | 111 | @Override 112 | public String[] getParts() { 113 | return parts; 114 | } 115 | 116 | @Override 117 | public Long[] getNumericParts() { 118 | return numericParts; 119 | } 120 | 121 | @Override 122 | public String getSource() { 123 | return source; 124 | } 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/test/java/org/openrewrite/java/dependencies/FindDependencyTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | * 4 | * Moderne Proprietary. Only for use by Moderne customers under the terms of a commercial contract. 5 | */ 6 | package org.openrewrite.java.dependencies; 7 | 8 | import org.junit.jupiter.api.Test; 9 | import org.openrewrite.DocumentExample; 10 | import org.openrewrite.test.RecipeSpec; 11 | import org.openrewrite.test.RewriteTest; 12 | 13 | import static org.openrewrite.gradle.Assertions.buildGradle; 14 | import static org.openrewrite.gradle.toolingapi.Assertions.withToolingApi; 15 | import static org.openrewrite.maven.Assertions.pomXml; 16 | 17 | @SuppressWarnings("GroovyAssignabilityCheck") 18 | class FindDependencyTest implements RewriteTest { 19 | 20 | @Override 21 | public void defaults(RecipeSpec spec) { 22 | spec.recipe(new FindDependency("org.openrewrite", "rewrite-core", "8.0.0", null, null)); 23 | } 24 | 25 | @DocumentExample 26 | @Test 27 | void findMavenDependency() { 28 | rewriteRun( 29 | //language=xml 30 | pomXml( 31 | """ 32 | 33 | com.mycompany.app 34 | my-app 35 | 1 36 | 37 | 38 | org.openrewrite 39 | rewrite-core 40 | 8.0.0 41 | 42 | 43 | 44 | """, 45 | """ 46 | 47 | com.mycompany.app 48 | my-app 49 | 1 50 | 51 | 52 | org.openrewrite 53 | rewrite-core 54 | 8.0.0 55 | 56 | 57 | 58 | """ 59 | ) 60 | ); 61 | } 62 | 63 | @Test 64 | void findMavenDependencyDoesNotFindWrongVersion() { 65 | rewriteRun( 66 | //language=xml 67 | pomXml( 68 | """ 69 | 70 | com.mycompany.app 71 | my-app 72 | 1 73 | 74 | 75 | org.openrewrite 76 | rewrite-core 77 | 8.1.0 78 | 79 | 80 | 81 | """ 82 | ) 83 | ); 84 | } 85 | 86 | @Test 87 | void findGradleDependency() { 88 | rewriteRun( 89 | spec -> spec.beforeRecipe(withToolingApi()), 90 | //language=groovy 91 | buildGradle( 92 | """ 93 | plugins { 94 | id 'java-library' 95 | } 96 | 97 | repositories { 98 | mavenCentral() 99 | } 100 | 101 | dependencies { 102 | api "org.openrewrite:rewrite-core:8.0.0" 103 | } 104 | """, 105 | """ 106 | plugins { 107 | id 'java-library' 108 | } 109 | 110 | repositories { 111 | mavenCentral() 112 | } 113 | 114 | dependencies { 115 | /*~~>*/api "org.openrewrite:rewrite-core:8.0.0" 116 | } 117 | """ 118 | ) 119 | ); 120 | } 121 | 122 | @Test 123 | void findGradleDependencyDoesntFindWrongVersion() { 124 | rewriteRun( 125 | spec -> spec.beforeRecipe(withToolingApi()), 126 | //language=groovy 127 | buildGradle( 128 | """ 129 | plugins { 130 | id 'java-library' 131 | } 132 | 133 | repositories { 134 | mavenCentral() 135 | } 136 | 137 | dependencies { 138 | api "org.openrewrite:rewrite-core:8.1.0" 139 | } 140 | """ 141 | ) 142 | ); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/dependencies/search/DoesNotIncludeDependency.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies.search; 17 | 18 | import lombok.EqualsAndHashCode; 19 | import lombok.Value; 20 | import org.jspecify.annotations.Nullable; 21 | import org.openrewrite.*; 22 | 23 | @EqualsAndHashCode(callSuper = false) 24 | @Value 25 | public class DoesNotIncludeDependency extends Recipe { 26 | @Override 27 | public String getDisplayName() { 28 | return "Does not include dependency for Gradle and Maven"; 29 | } 30 | 31 | @Override 32 | public String getDescription() { 33 | return "A precondition which returns false if visiting a Gradle file / Maven pom which includes the specified dependency in the classpath of some Gradle configuration / Maven scope. " + 34 | "For compatibility with multimodule projects, this should most often be applied as a precondition."; 35 | } 36 | 37 | @Option(displayName = "Group", 38 | description = "The first part of a dependency coordinate `com.google.guava:guava:VERSION`. Supports glob.", 39 | example = "com.google.guava") 40 | String groupId; 41 | 42 | @Option(displayName = "Artifact", 43 | description = "The second part of a dependency coordinate `com.google.guava:guava:VERSION`. Supports glob.", 44 | example = "guava") 45 | String artifactId; 46 | 47 | @Option(displayName = "Version", 48 | description = "Match only dependencies with the specified resolved version. " + 49 | "Node-style [version selectors](https://docs.openrewrite.org/reference/dependency-version-selectors) may be used. " + 50 | "All versions are searched by default.", 51 | example = "1.x", 52 | required = false) 53 | @Nullable 54 | String version; 55 | 56 | @Option(displayName = "Only direct dependencies", 57 | description = "Default false. If enabled, transitive dependencies will not be considered.", 58 | required = false, 59 | example = "true") 60 | @Nullable 61 | Boolean onlyDirect; 62 | 63 | @Option(displayName = "Maven scope", 64 | description = "Default any. If specified, only the requested scope's classpaths will be checked.", 65 | required = false, 66 | valid = {"compile", "test", "runtime", "provided"}, 67 | example = "compile") 68 | @Nullable 69 | String scope; 70 | 71 | @Option(displayName = "Gradle configuration", 72 | description = "Match dependencies with the specified configuration. If not specified, all configurations will be searched.", 73 | example = "compileClasspath", 74 | required = false) 75 | @Nullable 76 | String configuration; 77 | 78 | @Override 79 | public TreeVisitor getVisitor() { 80 | return new TreeVisitor() { 81 | final TreeVisitor gdnid = new org.openrewrite.gradle.search.DoesNotIncludeDependency( 82 | groupId, artifactId, version, configuration) 83 | .getVisitor(); 84 | final TreeVisitor mdnid = new org.openrewrite.maven.search.DoesNotIncludeDependency( 85 | groupId, artifactId, version, onlyDirect, scope) 86 | .getVisitor(); 87 | 88 | @Override 89 | public boolean isAcceptable(SourceFile sourceFile, ExecutionContext ctx) { 90 | return gdnid.isAcceptable(sourceFile, ctx) || mdnid.isAcceptable(sourceFile, ctx); 91 | } 92 | 93 | @Override 94 | public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { 95 | if (!(tree instanceof SourceFile)) { 96 | return tree; 97 | } 98 | SourceFile s = (SourceFile) tree; 99 | if (gdnid.isAcceptable(s, ctx)) { 100 | s = (SourceFile) gdnid.visitNonNull(s, ctx); 101 | } else if (mdnid.isAcceptable(s, ctx)) { 102 | s = (SourceFile) mdnid.visitNonNull(s, ctx); 103 | } 104 | return s; 105 | } 106 | }; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/dependencies/search/RepositoryHasDependency.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies.search; 17 | 18 | import lombok.EqualsAndHashCode; 19 | import lombok.Value; 20 | import org.jspecify.annotations.Nullable; 21 | import org.openrewrite.*; 22 | import org.openrewrite.java.dependencies.DependencyInsight; 23 | import org.openrewrite.java.marker.JavaProject; 24 | import org.openrewrite.marker.SearchResult; 25 | 26 | import java.util.concurrent.atomic.AtomicBoolean; 27 | 28 | @EqualsAndHashCode(callSuper = false) 29 | @Value 30 | public class RepositoryHasDependency extends ScanningRecipe { 31 | 32 | @Override 33 | public String getDisplayName() { 34 | return "Repository has dependency"; 35 | } 36 | 37 | @Override 38 | public String getDescription() { 39 | return "Searches for both Gradle and Maven modules that have a dependency matching the specified groupId and artifactId. " + 40 | "Places a `SearchResult` marker on all sources within a repository with a matching dependency. " + 41 | "This recipe is intended to be used as a precondition for other recipes. " + 42 | "For example this could be used to limit the application of a spring boot migration to only projects " + 43 | "that use a springframework dependency, limiting unnecessary upgrading. " + 44 | "If the search result you want is instead just the build.gradle(.kts) or pom.xml file applying the plugin, use the `FindDependency` recipe instead."; 45 | } 46 | 47 | @Option(displayName = "Group pattern", 48 | description = "Group glob pattern used to match dependencies.", 49 | example = "com.fasterxml.jackson.module") 50 | String groupIdPattern; 51 | 52 | @Option(displayName = "Artifact pattern", 53 | description = "Artifact glob pattern used to match dependencies.", 54 | example = "jackson-module-*") 55 | String artifactIdPattern; 56 | 57 | @Option(displayName = "Scope", 58 | description = "Match dependencies with the specified scope. All scopes are searched by default.", 59 | valid = {"compile", "test", "runtime", "provided", "system"}, 60 | example = "compile", 61 | required = false) 62 | @Nullable 63 | String scope; 64 | 65 | @Option(displayName = "Version", 66 | description = "Match only dependencies with the specified version. " + 67 | "Node-style [version selectors](https://docs.openrewrite.org/reference/dependency-version-selectors) may be used." + 68 | "All versions are searched by default.", 69 | example = "1.x", 70 | required = false) 71 | @Nullable 72 | String version; 73 | 74 | @Override 75 | public AtomicBoolean getInitialValue(ExecutionContext ctx) { 76 | return new AtomicBoolean(); 77 | } 78 | 79 | @Override 80 | public TreeVisitor getScanner(AtomicBoolean acc) { 81 | return new TreeVisitor() { 82 | @Override 83 | public Tree visit(@Nullable Tree tree, ExecutionContext ctx) { 84 | if(acc.get()) { 85 | assert tree != null; 86 | return tree; 87 | } 88 | assert tree != null; 89 | tree.getMarkers() 90 | .findFirst(JavaProject.class) 91 | .ifPresent(jp -> { 92 | Tree t = new DependencyInsight(groupIdPattern, artifactIdPattern, scope, version) 93 | .getVisitor() 94 | .visit(tree, ctx); 95 | if (t != tree) { 96 | acc.set(true); 97 | } 98 | }); 99 | return tree; 100 | } 101 | }; 102 | } 103 | 104 | @Override 105 | public TreeVisitor getVisitor(AtomicBoolean acc) { 106 | if (acc.get()) { 107 | return new TreeVisitor() { 108 | @Override 109 | public Tree visit(@Nullable Tree tree, ExecutionContext ctx) { 110 | assert tree != null; 111 | return SearchResult.found(tree, "Repository has dependency: " + groupIdPattern + ":" + artifactIdPattern + (version == null ? "" : ":" + version)); 112 | } 113 | }; 114 | } 115 | return TreeVisitor.noop(); 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/test/java/org/openrewrite/java/dependencies/AddDependencyTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies; 17 | 18 | import org.intellij.lang.annotations.Language; 19 | import org.jspecify.annotations.Nullable; 20 | import org.junit.jupiter.api.Test; 21 | import org.junit.jupiter.params.ParameterizedTest; 22 | import org.junit.jupiter.params.provider.ValueSource; 23 | import org.openrewrite.DocumentExample; 24 | import org.openrewrite.java.JavaParser; 25 | import org.openrewrite.test.RecipeSpec; 26 | import org.openrewrite.test.RewriteTest; 27 | 28 | import static org.openrewrite.gradle.Assertions.buildGradle; 29 | import static org.openrewrite.gradle.toolingapi.Assertions.withToolingApi; 30 | import static org.openrewrite.java.Assertions.*; 31 | import static org.openrewrite.maven.Assertions.pomXml; 32 | 33 | class AddDependencyTest implements RewriteTest { 34 | 35 | @Override 36 | public void defaults(RecipeSpec spec) { 37 | spec.parser(JavaParser.fromJavaVersion().classpath("guava")); 38 | } 39 | 40 | @Language("java") 41 | private final String usingGuavaIntMath = """ 42 | import com.google.common.math.IntMath; 43 | public class A { 44 | boolean getMap() { 45 | return IntMath.isPrime(5); 46 | } 47 | } 48 | """; 49 | 50 | @DocumentExample 51 | @Test 52 | void addMavenDependencyWithSystemScope() { 53 | rewriteRun( 54 | spec -> spec 55 | .recipe(addDependency("doesnotexist:doesnotexist:1", "com.google.common.math.IntMath", "system")), 56 | mavenProject("project", 57 | srcMainJava( 58 | java(usingGuavaIntMath) 59 | ), 60 | //language=xml 61 | pomXml( 62 | """ 63 | 64 | com.mycompany.app 65 | my-app 66 | 1 67 | 68 | """, 69 | """ 70 | 71 | com.mycompany.app 72 | my-app 73 | 1 74 | 75 | 76 | doesnotexist 77 | doesnotexist 78 | 1 79 | system 80 | 81 | 82 | 83 | """ 84 | ) 85 | ) 86 | ); 87 | } 88 | 89 | @ParameterizedTest 90 | @ValueSource(strings = {"com.google.common.math.*", "com.google.common.math.IntMath"}) 91 | void addGradleDependencyWithOnlyIfUsingTestScope(String onlyIfUsing) { 92 | rewriteRun( 93 | spec -> spec.beforeRecipe(withToolingApi()).recipe(addDependency("com.google.guava:guava:29.0-jre", onlyIfUsing, null)), 94 | mavenProject("project", 95 | srcTestJava( 96 | java(usingGuavaIntMath) 97 | ), 98 | //language=groovy 99 | buildGradle( 100 | """ 101 | plugins { 102 | id "java-library" 103 | } 104 | 105 | repositories { 106 | mavenCentral() 107 | } 108 | """, 109 | """ 110 | plugins { 111 | id "java-library" 112 | } 113 | 114 | repositories { 115 | mavenCentral() 116 | } 117 | 118 | dependencies { 119 | testImplementation "com.google.guava:guava:29.0-jre" 120 | } 121 | """ 122 | ) 123 | ) 124 | ); 125 | } 126 | 127 | 128 | private AddDependency addDependency(String gav, String onlyIfUsing, @Nullable String scope) { 129 | String[] gavParts = gav.split(":"); 130 | return new AddDependency( 131 | gavParts[0], 132 | gavParts[1], 133 | gavParts.length < 3 ? null : gavParts[2], 134 | null, 135 | onlyIfUsing, 136 | gavParts.length < 4 ? null : gavParts[3], 137 | null, 138 | null, 139 | null, 140 | scope, 141 | null, 142 | null, 143 | null, 144 | null 145 | ); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/test/java/org/openrewrite/java/dependencies/search/DoesNotIncludeDependencyTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies.search; 17 | 18 | import org.junit.jupiter.api.Test; 19 | import org.openrewrite.DocumentExample; 20 | import org.openrewrite.test.RecipeSpec; 21 | import org.openrewrite.test.RewriteTest; 22 | 23 | import static org.openrewrite.gradle.Assertions.buildGradle; 24 | import static org.openrewrite.gradle.toolingapi.Assertions.withToolingApi; 25 | import static org.openrewrite.java.Assertions.java; 26 | import static org.openrewrite.maven.Assertions.pomXml; 27 | 28 | class DoesNotIncludeDependencyTest implements RewriteTest { 29 | @Override 30 | public void defaults(RecipeSpec spec) { 31 | spec 32 | .beforeRecipe(withToolingApi()) 33 | .recipe(new DoesNotIncludeDependency("org.springframework", "spring-beans", null, false, "compile", "compileClasspath")); 34 | } 35 | 36 | @DocumentExample 37 | @Test 38 | void whenDoesNotIncludeDependencyInCorrectScopeOrConfigurationMarks() { 39 | rewriteRun( 40 | //language=xml 41 | pomXml( 42 | """ 43 | 44 | com.example 45 | foo 46 | 1.0.0 47 | 48 | 49 | org.springframework 50 | spring-beans 51 | 6.0.0 52 | test 53 | 54 | 55 | 56 | """, 57 | """ 58 | 59 | com.example 60 | foo 61 | 1.0.0 62 | 63 | 64 | org.springframework 65 | spring-beans 66 | 6.0.0 67 | test 68 | 69 | 70 | 71 | """ 72 | ), 73 | //language=groovy 74 | buildGradle( 75 | """ 76 | plugins { 77 | id 'java-library' 78 | } 79 | repositories { 80 | mavenCentral() 81 | } 82 | dependencies { 83 | testImplementation 'org.springframework:spring-beans:6.0.0' 84 | } 85 | """, 86 | """ 87 | /*~~>*/plugins { 88 | id 'java-library' 89 | } 90 | repositories { 91 | mavenCentral() 92 | } 93 | dependencies { 94 | testImplementation 'org.springframework:spring-beans:6.0.0' 95 | } 96 | """ 97 | ) 98 | ); 99 | } 100 | 101 | @Test 102 | void whenIncludesDependencyInCorrectScopeOrConfigurationDoesNotMark() { 103 | rewriteRun( 104 | //language=xml 105 | pomXml( 106 | """ 107 | 108 | com.example 109 | foo 110 | 1.0.0 111 | 112 | 113 | org.springframework 114 | spring-beans 115 | 6.0.0 116 | compile 117 | 118 | 119 | 120 | """ 121 | ), 122 | //language=groovy 123 | buildGradle( 124 | """ 125 | plugins { 126 | id 'java-library' 127 | } 128 | repositories { 129 | mavenCentral() 130 | } 131 | dependencies { 132 | implementation 'org.springframework:spring-beans:6.0.0' 133 | } 134 | """ 135 | ) 136 | ); 137 | } 138 | 139 | @Test 140 | void whenInapplicableFileTypeDoesNotMark() { 141 | rewriteRun( 142 | //language=java 143 | java( 144 | """ 145 | class SomeClass {} 146 | """ 147 | ) 148 | ); 149 | } 150 | } 151 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/dependencies/oldgroupids/ParseDefinitionMigrations.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies.oldgroupids; 17 | 18 | import com.fasterxml.jackson.annotation.JsonProperty; 19 | import com.fasterxml.jackson.core.JsonGenerator; 20 | import com.fasterxml.jackson.databind.DeserializationFeature; 21 | import com.fasterxml.jackson.databind.MapperFeature; 22 | import com.fasterxml.jackson.databind.ObjectMapper; 23 | import com.fasterxml.jackson.databind.ObjectWriter; 24 | import com.fasterxml.jackson.dataformat.csv.CsvFactory; 25 | import com.fasterxml.jackson.dataformat.csv.CsvMapper; 26 | import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; 27 | import lombok.Data; 28 | import org.jspecify.annotations.Nullable; 29 | 30 | import java.io.File; 31 | import java.io.IOException; 32 | import java.nio.file.Path; 33 | import java.util.ArrayList; 34 | import java.util.List; 35 | 36 | public class ParseDefinitionMigrations { 37 | public static void main(String[] args) throws IOException { 38 | if (args.length != 2) { 39 | System.err.println("Usage: ParseDefinitionMigrations "); 40 | System.exit(1); 41 | } 42 | File repo = new File(args[0]); 43 | if (!repo.isDirectory() || !repo.canRead()) { 44 | System.err.println("oga-maven-plugin repo " + repo + " not readable"); 45 | System.exit(1); 46 | } 47 | File csv = new File(args[1]); 48 | if (!csv.createNewFile() && !csv.canWrite()) { 49 | System.err.println("CSV " + csv + " not writable"); 50 | System.exit(1); 51 | } 52 | 53 | parseDefinitionMigrations(repo, csv); 54 | } 55 | 56 | static void parseDefinitionMigrations(File repo, File csv) throws IOException { 57 | ObjectMapper objectMapper = getObjectMapper(); 58 | 59 | Path uc = repo.toPath().resolve("uc"); 60 | File official = uc.resolve("og-definitions.json").toFile(); 61 | File unofficial = uc.resolve("og-unofficial-definitions.json").toFile(); 62 | 63 | List definitions = objectMapper.readValue(official, Definitions.class).getMigration(); 64 | List proposed = objectMapper.readValue(unofficial, UnofficialDefinitions.class).getMigration(); 65 | 66 | List migrations = new ArrayList<>(definitions.size() + proposed.size()); 67 | for (DefinitionMigration d : definitions) { 68 | migrations.add(getMigration(d.getOldGav(), d.getNewGav(), d.getContext())); 69 | } 70 | for (ProposedMigration p : proposed) { 71 | migrations.add(getMigration(p.getOldGav(), p.getProposal().get(0), p.getContext())); 72 | } 73 | 74 | ObjectWriter objectWriter = getObjectWriter(); 75 | objectWriter.writeValue(csv, migrations); 76 | } 77 | 78 | private static Migration getMigration(String oldGav1, String newGav1, String context) { 79 | String[] oldGav = oldGav1.split(":"); 80 | String[] newGav = newGav1.split(":"); 81 | return new Migration( 82 | oldGav[0], oldGav.length > 1 ? oldGav[1] : null, 83 | newGav[0], newGav.length > 1 ? newGav[1] : null, 84 | context); 85 | } 86 | 87 | private static ObjectMapper getObjectMapper() { 88 | return new ObjectMapper() 89 | .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) 90 | .registerModule(new JavaTimeModule()); 91 | } 92 | 93 | private static ObjectWriter getObjectWriter() { 94 | CsvFactory factory = new CsvFactory(); 95 | factory.configure(JsonGenerator.Feature.AUTO_CLOSE_TARGET, false); 96 | CsvMapper csvMapper = CsvMapper.builder(factory) 97 | .disable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY) 98 | .build(); 99 | return csvMapper.writer(csvMapper.schemaFor(Migration.class)); 100 | } 101 | } 102 | 103 | /** 104 | * Mirrors Definitions.kt 105 | */ 106 | @Data 107 | class Definitions { 108 | List migration; 109 | } 110 | 111 | @Data 112 | class DefinitionMigration { 113 | @JsonProperty("old") 114 | String oldGav; 115 | 116 | @JsonProperty("new") 117 | String newGav; 118 | 119 | @Nullable 120 | String context; 121 | } 122 | 123 | @Data 124 | class UnofficialDefinitions { 125 | List migration; 126 | } 127 | 128 | @Data 129 | class ProposedMigration { 130 | @JsonProperty("old") 131 | String oldGav; 132 | 133 | List proposal; 134 | 135 | @Nullable 136 | String context; 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/dependencies/RemoveDependency.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies; 17 | 18 | import lombok.EqualsAndHashCode; 19 | import lombok.Value; 20 | import org.jspecify.annotations.Nullable; 21 | import org.openrewrite.*; 22 | import org.openrewrite.java.marker.JavaProject; 23 | import org.openrewrite.java.search.UsesType; 24 | 25 | import java.util.HashMap; 26 | import java.util.Map; 27 | 28 | @EqualsAndHashCode(callSuper = false) 29 | @Value 30 | public class RemoveDependency extends ScanningRecipe> { 31 | @Option(displayName = "Group ID", 32 | description = "The first part of a dependency coordinate `com.google.guava:guava:VERSION`. This can be a glob expression.", 33 | example = "com.fasterxml.jackson*") 34 | String groupId; 35 | 36 | @Option(displayName = "Artifact ID", 37 | description = "The second part of a dependency coordinate `com.google.guava:guava:VERSION`. This can be a glob expression.", 38 | example = "jackson-module*") 39 | String artifactId; 40 | 41 | @Option(displayName = "Unless using", 42 | description = "Do not remove if type is in use. Supports glob expressions.", 43 | example = "org.aspectj.lang.*", 44 | required = false) 45 | @Nullable 46 | String unlessUsing; 47 | 48 | // Gradle only parameter 49 | @Option(displayName = "The dependency configuration", 50 | description = "The dependency configuration to remove from.", 51 | example = "api", 52 | required = false) 53 | @Nullable 54 | String configuration; 55 | 56 | // Maven only parameter 57 | @Option(displayName = "Scope", 58 | description = "Only remove dependencies if they are in this scope. If 'runtime', this will" + 59 | "also remove dependencies in the 'compile' scope because 'compile' dependencies are part of the runtime dependency set", 60 | valid = {"compile", "test", "runtime", "provided"}, 61 | example = "compile", 62 | required = false) 63 | @Nullable 64 | String scope; 65 | 66 | @Override 67 | public String getDisplayName() { 68 | return "Remove a Gradle or Maven dependency"; 69 | } 70 | 71 | @Override 72 | public String getDescription() { 73 | return "For Gradle project, removes a single dependency from the dependencies section of the `build.gradle`.\n" + 74 | "For Maven project, removes a single dependency from the `` section of the pom.xml."; 75 | } 76 | 77 | @Override 78 | public Map getInitialValue(ExecutionContext ctx) { 79 | return new HashMap<>(); 80 | } 81 | 82 | @Override 83 | public TreeVisitor getScanner(Map projectToInUse) { 84 | if (unlessUsing == null) { 85 | return TreeVisitor.noop(); 86 | } 87 | UsesType usesType = new UsesType<>(unlessUsing, true); 88 | return new TreeVisitor() { 89 | @Override 90 | public Tree preVisit(Tree tree, ExecutionContext ctx) { 91 | stopAfterPreVisit(); 92 | tree.getMarkers().findFirst(JavaProject.class).ifPresent(javaProject -> 93 | projectToInUse.compute(javaProject, (jp, foundSoFar) -> Boolean.TRUE.equals(foundSoFar) || tree != usesType.visit(tree, ctx))); 94 | return tree; 95 | } 96 | }; 97 | } 98 | 99 | @Override 100 | public TreeVisitor getVisitor(Map projectToInUse) { 101 | return new TreeVisitor() { 102 | final TreeVisitor gradleRemoveDep = new org.openrewrite.gradle.RemoveDependency(groupId, artifactId, configuration).getVisitor(); 103 | final TreeVisitor mavenRemoveDep = new org.openrewrite.maven.RemoveDependency(groupId, artifactId, scope).getVisitor(); 104 | 105 | @Override 106 | public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { 107 | if (!(tree instanceof SourceFile)) { 108 | return tree; 109 | } 110 | if (unlessUsing != null) { 111 | JavaProject jp = tree.getMarkers().findFirst(JavaProject.class).orElse(null); 112 | if (jp == null || Boolean.TRUE.equals(projectToInUse.get(jp))) { 113 | return tree; 114 | } 115 | } 116 | SourceFile sf = (SourceFile) tree; 117 | if (gradleRemoveDep.isAcceptable(sf, ctx)) { 118 | return gradleRemoveDep.visitNonNull(tree, ctx); 119 | } 120 | if (mavenRemoveDep.isAcceptable(sf, ctx)) { 121 | return mavenRemoveDep.visitNonNull(tree, ctx); 122 | } 123 | return tree; 124 | } 125 | }; 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/test/java/org/openrewrite/java/dependencies/UpgradeDependencyVersionTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies; 17 | 18 | 19 | import org.junit.jupiter.api.Test; 20 | import org.openrewrite.DocumentExample; 21 | import org.openrewrite.gradle.marker.GradleDependencyConfiguration; 22 | import org.openrewrite.gradle.marker.GradleProject; 23 | import org.openrewrite.test.RewriteTest; 24 | 25 | import java.util.Optional; 26 | 27 | import static org.assertj.core.api.Assertions.assertThat; 28 | import static org.openrewrite.gradle.Assertions.buildGradle; 29 | import static org.openrewrite.gradle.toolingapi.Assertions.withToolingApi; 30 | import static org.openrewrite.maven.Assertions.pomXml; 31 | 32 | class UpgradeDependencyVersionTest implements RewriteTest { 33 | 34 | @DocumentExample("Upgrade gradle dependency") 35 | @Test 36 | void upgradeGuavaInGradleProject() { 37 | rewriteRun( 38 | spec -> spec.beforeRecipe(withToolingApi()) 39 | .recipe(new UpgradeDependencyVersion("com.google.guava", "guava", "30.x", "-jre", null, null)), 40 | buildGradle( 41 | //language=groovy 42 | """ 43 | plugins { 44 | id 'java-library' 45 | } 46 | 47 | repositories { 48 | mavenCentral() 49 | } 50 | 51 | dependencies { 52 | compileOnly 'com.google.guava:guava:29.0-jre' 53 | runtimeOnly ('com.google.guava:guava:29.0-jre') 54 | } 55 | """, 56 | //language=groovy 57 | """ 58 | plugins { 59 | id 'java-library' 60 | } 61 | 62 | repositories { 63 | mavenCentral() 64 | } 65 | 66 | dependencies { 67 | compileOnly 'com.google.guava:guava:30.1.1-jre' 68 | runtimeOnly ('com.google.guava:guava:30.1.1-jre') 69 | } 70 | """, 71 | spec -> spec.afterRecipe(after -> { 72 | Optional maybeGp = after.getMarkers().findFirst(GradleProject.class); 73 | assertThat(maybeGp).isPresent(); 74 | GradleProject gp = maybeGp.get(); 75 | GradleDependencyConfiguration compileClasspath = gp.getConfiguration("compileClasspath"); 76 | assertThat(compileClasspath).isNotNull(); 77 | assertThat( 78 | compileClasspath.getRequested().stream() 79 | .filter(dep -> "com.google.guava".equals(dep.getGroupId()) && "guava".equals(dep.getArtifactId()) && "30.1.1-jre".equals(dep.getVersion())) 80 | .findAny()) 81 | .as("GradleProject requested dependencies should have been updated with the new version of guava") 82 | .isPresent(); 83 | assertThat( 84 | compileClasspath.getResolved().stream() 85 | .filter(dep -> "com.google.guava".equals(dep.getGroupId()) && "guava".equals(dep.getArtifactId()) && "30.1.1-jre".equals(dep.getVersion())) 86 | .findAny()) 87 | .as("GradleProject requested dependencies should have been updated with the new version of guava") 88 | .isPresent(); 89 | }) 90 | ) 91 | ); 92 | } 93 | 94 | @Test 95 | void updateManagedDependencyVersion() { 96 | rewriteRun( 97 | spec -> spec.recipe(new UpgradeDependencyVersion("org.junit.jupiter", "junit-jupiter-api", "5.7.2", null, 98 | null, null)), 99 | //language=xml 100 | pomXml( 101 | """ 102 | 103 | com.mycompany.app 104 | my-app 105 | 1 106 | 107 | 108 | 109 | org.junit.jupiter 110 | junit-jupiter-api 111 | 5.6.2 112 | test 113 | 114 | 115 | 116 | 117 | """, 118 | """ 119 | 120 | com.mycompany.app 121 | my-app 122 | 1 123 | 124 | 125 | 126 | org.junit.jupiter 127 | junit-jupiter-api 128 | 5.7.2 129 | test 130 | 131 | 132 | 133 | 134 | """ 135 | ) 136 | ); 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/dependencies/search/ModuleHasDependency.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies.search; 17 | 18 | import lombok.EqualsAndHashCode; 19 | import lombok.Value; 20 | import org.jspecify.annotations.Nullable; 21 | import org.openrewrite.*; 22 | import org.openrewrite.java.dependencies.DependencyInsight; 23 | import org.openrewrite.java.marker.JavaProject; 24 | import org.openrewrite.marker.SearchResult; 25 | 26 | import java.util.HashSet; 27 | import java.util.Optional; 28 | import java.util.Set; 29 | 30 | @EqualsAndHashCode(callSuper = false) 31 | @Value 32 | public class ModuleHasDependency extends ScanningRecipe> { 33 | 34 | @Override 35 | public String getDisplayName() { 36 | return "Module has dependency"; 37 | } 38 | 39 | @Override 40 | public String getDescription() { 41 | return "Searches for both Gradle and Maven modules that have a dependency matching the specified groupId and artifactId. " + 42 | "Places a `SearchResult` marker on all sources within a module with a matching dependency. " + 43 | "This recipe is intended to be used as a precondition for other recipes. " + 44 | "For example this could be used to limit the application of a spring boot migration to only projects " + 45 | "that use spring-boot-starter, limiting unnecessary upgrading. " + 46 | "If the search result you want is instead just the build.gradle(.kts) or pom.xml file applying the plugin, use the `FindDependency` recipe instead."; 47 | } 48 | 49 | @Option(displayName = "Group pattern", 50 | description = "Group glob pattern used to match dependencies.", 51 | example = "com.fasterxml.jackson.module") 52 | String groupIdPattern; 53 | 54 | @Option(displayName = "Artifact pattern", 55 | description = "Artifact glob pattern used to match dependencies.", 56 | example = "jackson-module-*") 57 | String artifactIdPattern; 58 | 59 | @Option(displayName = "Scope", 60 | description = "Match dependencies with the specified scope. All scopes are searched by default.", 61 | valid = {"compile", "test", "runtime", "provided", "system"}, 62 | example = "compile", 63 | required = false) 64 | @Nullable 65 | String scope; 66 | 67 | @Option(displayName = "Version", 68 | description = "Match only dependencies with the specified version. " + 69 | "Node-style [version selectors](https://docs.openrewrite.org/reference/dependency-version-selectors) may be used." + 70 | "All versions are searched by default.", 71 | example = "1.x", 72 | required = false) 73 | @Nullable 74 | String version; 75 | 76 | @Option(displayName = "Invert marking", 77 | description = "If `true`, will invert the check for whether to mark a file. Defaults to `false`.", 78 | required = false) 79 | @Nullable 80 | Boolean invertMarking; 81 | 82 | @Override 83 | public Set getInitialValue(ExecutionContext ctx) { 84 | return new HashSet<>(); 85 | } 86 | 87 | @Override 88 | public TreeVisitor getScanner(Set acc) { 89 | return new TreeVisitor() { 90 | @Override 91 | public Tree visit(@Nullable Tree tree, ExecutionContext ctx) { 92 | assert tree != null; 93 | tree.getMarkers() 94 | .findFirst(JavaProject.class) 95 | .ifPresent(jp -> { 96 | Tree t = new DependencyInsight(groupIdPattern, artifactIdPattern, version, scope) 97 | .getVisitor() 98 | .visit(tree, ctx); 99 | if (t != tree) { 100 | acc.add(jp); 101 | } 102 | }); 103 | return tree; 104 | } 105 | }; 106 | } 107 | 108 | @Override 109 | public TreeVisitor getVisitor(Set acc) { 110 | return new TreeVisitor() { 111 | @Override 112 | public Tree visit(@Nullable Tree tree, ExecutionContext ctx) { 113 | assert tree != null; 114 | boolean shouldInvert = invertMarking != null && invertMarking; 115 | String dependencyGav = groupIdPattern + ":" + artifactIdPattern + (version == null ? "" : ":" + version); 116 | Optional maybeJp = tree.getMarkers().findFirst(JavaProject.class); 117 | if (!maybeJp.isPresent()) { 118 | if (shouldInvert) { 119 | return SearchResult.found(tree, "No module, so vacuously does not have dependency: " + dependencyGav); 120 | } 121 | return tree; 122 | } 123 | JavaProject jp = maybeJp.get(); 124 | if (shouldInvert && !acc.contains(jp)) { 125 | return SearchResult.found(tree, "Module does not have dependency: " + dependencyGav); 126 | } 127 | if (!shouldInvert && acc.contains(jp)) { 128 | return SearchResult.found(tree, "Module has dependency: " + dependencyGav); 129 | } 130 | return tree; 131 | } 132 | }; 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/dependencies/ChangeDependency.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies; 17 | 18 | import lombok.AllArgsConstructor; 19 | import lombok.EqualsAndHashCode; 20 | import lombok.Getter; 21 | import org.jspecify.annotations.Nullable; 22 | import org.openrewrite.*; 23 | 24 | @AllArgsConstructor 25 | @EqualsAndHashCode(callSuper = false) 26 | @Getter 27 | public class ChangeDependency extends Recipe { 28 | // Gradle and Maven shared parameters 29 | @Option(displayName = "Old group ID", 30 | description = "The old group ID to replace. The group ID is the first part of a dependency coordinate 'com.google.guava:guava:VERSION'. Supports glob expressions.", 31 | example = "org.openrewrite.recipe") 32 | String oldGroupId; 33 | 34 | @Option(displayName = "Old artifact ID", 35 | description = "The old artifact ID to replace. The artifact ID is the second part of a dependency coordinate 'com.google.guava:guava:VERSION'. Supports glob expressions.", 36 | example = "rewrite-testing-frameworks") 37 | String oldArtifactId; 38 | 39 | @Option(displayName = "New group ID", 40 | description = "The new group ID to use. Defaults to the existing group ID.", 41 | example = "corp.internal.openrewrite.recipe", 42 | required = false) 43 | @Nullable 44 | String newGroupId; 45 | 46 | @Option(displayName = "New artifact ID", 47 | description = "The new artifact ID to use. Defaults to the existing artifact ID.", 48 | example = "rewrite-testing-frameworks", 49 | required = false) 50 | @Nullable 51 | String newArtifactId; 52 | 53 | @Option(displayName = "New version", 54 | description = "An exact version number or node-style semver selector used to select the version number.", 55 | example = "29.X", 56 | required = false) 57 | @Nullable 58 | String newVersion; 59 | 60 | @Option(displayName = "Version pattern", 61 | description = "Allows version selection to be extended beyond the original Node Semver semantics. So for example," + 62 | "Setting 'version' to \"25-29\" can be paired with a metadata pattern of \"-jre\" to select Guava 29.0-jre", 63 | example = "-jre", 64 | required = false) 65 | @Nullable 66 | String versionPattern; 67 | 68 | @Option(displayName = "Override managed version", 69 | description = "If the new dependency has a managed version, this flag can be used to explicitly set the version on the dependency. The default for this flag is `false`.", 70 | required = false) 71 | @Nullable 72 | Boolean overrideManagedVersion; 73 | 74 | @Option(displayName = "Update dependency management", 75 | description = "Also update the dependency management section. The default for this flag is `true`.", 76 | required = false) 77 | @Nullable 78 | Boolean changeManagedDependency; 79 | 80 | @Override 81 | public String getDisplayName() { 82 | return "Change Gradle or Maven dependency"; 83 | } 84 | 85 | @Override 86 | public String getDescription() { 87 | return "Change the group ID, artifact ID, and/or the version of a specified Gradle or Maven dependency."; 88 | } 89 | 90 | @Override 91 | public Validated validate(ExecutionContext ctx) { 92 | return super.validate(ctx) 93 | .and(((Recipe) new org.openrewrite.gradle.ChangeDependency( 94 | oldGroupId, oldArtifactId, newGroupId, newArtifactId, newVersion, versionPattern, overrideManagedVersion, changeManagedDependency)).validate()) 95 | .and(((Recipe) new org.openrewrite.maven.ChangeDependencyGroupIdAndArtifactId( 96 | oldGroupId, oldArtifactId, newGroupId, newArtifactId, newVersion, versionPattern, overrideManagedVersion, changeManagedDependency)).validate()); 97 | } 98 | 99 | @Override 100 | public TreeVisitor getVisitor() { 101 | return new TreeVisitor() { 102 | final TreeVisitor mavenVisitor = new org.openrewrite.maven.ChangeDependencyGroupIdAndArtifactId( 103 | oldGroupId, oldArtifactId, 104 | newGroupId, newArtifactId, 105 | newVersion, versionPattern, 106 | overrideManagedVersion, changeManagedDependency).getVisitor(); 107 | final TreeVisitor gradleVisitor = new org.openrewrite.gradle.ChangeDependency( 108 | oldGroupId, oldArtifactId, 109 | newGroupId, newArtifactId, 110 | newVersion, versionPattern, 111 | overrideManagedVersion, true).getVisitor(); 112 | 113 | @Override 114 | public boolean isAcceptable(SourceFile sourceFile, ExecutionContext ctx) { 115 | return mavenVisitor.isAcceptable(sourceFile, ctx) || gradleVisitor.isAcceptable(sourceFile, ctx); 116 | } 117 | 118 | @Override 119 | public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { 120 | if (!(tree instanceof SourceFile)) { 121 | return tree; 122 | } 123 | SourceFile s = (SourceFile) tree; 124 | if (gradleVisitor.isAcceptable(s, ctx)) { 125 | s = (SourceFile) gradleVisitor.visitNonNull(s, ctx); 126 | } else if (mavenVisitor.isAcceptable(s, ctx)) { 127 | s = (SourceFile) mavenVisitor.visitNonNull(s, ctx); 128 | } 129 | return s; 130 | } 131 | }; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/dependencies/UpgradeTransitiveDependencyVersion.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies; 17 | 18 | import lombok.EqualsAndHashCode; 19 | import lombok.Value; 20 | import org.jspecify.annotations.Nullable; 21 | import org.openrewrite.*; 22 | import org.openrewrite.maven.AddManagedDependency; 23 | 24 | @EqualsAndHashCode(callSuper = false) 25 | @Value 26 | public class UpgradeTransitiveDependencyVersion extends ScanningRecipe { 27 | 28 | @Override 29 | public String getDisplayName() { 30 | return "Upgrade transitive Gradle or Maven dependencies"; 31 | } 32 | 33 | @Override 34 | public String getDescription() { 35 | return "Upgrades the version of a transitive dependency in a Maven pom.xml or Gradle build.gradle. " + 36 | "Leaves direct dependencies unmodified. " + 37 | "Can be paired with the regular Upgrade Dependency Version recipe to upgrade a dependency everywhere, " + 38 | "regardless of whether it is direct or transitive."; 39 | } 40 | 41 | @Option(displayName = "Group", 42 | description = "The first part of a dependency coordinate 'org.apache.logging.log4j:ARTIFACT_ID:VERSION'.", 43 | example = "org.apache.logging.log4j") 44 | String groupId; 45 | 46 | @Option(displayName = "Artifact", 47 | description = "The second part of a dependency coordinate 'org.apache.logging.log4j:log4j-bom:VERSION'.", 48 | example = "log4j-bom") 49 | String artifactId; 50 | 51 | @Option(displayName = "Version", 52 | description = "An exact version number or node-style semver selector used to select the version number.", 53 | example = "latest.release") 54 | String version; 55 | 56 | @Option(displayName = "Scope", 57 | description = "An optional scope to use for the dependency management tag. Relevant only to Maven.", 58 | example = "import", 59 | valid = {"import", "runtime", "provided", "test"}, 60 | required = false) 61 | @Nullable 62 | String scope; 63 | 64 | @Option(displayName = "Type", 65 | description = "An optional type to use for the dependency management tag. Relevant only to Maven builds.", 66 | valid = {"jar", "pom", "war"}, 67 | example = "pom", 68 | required = false) 69 | @Nullable 70 | String type; 71 | 72 | @Option(displayName = "Classifier", 73 | description = "An optional classifier to use for the dependency management tag. Relevant only to Maven.", 74 | example = "test", 75 | required = false) 76 | @Nullable 77 | String classifier; 78 | 79 | @Option(displayName = "Version pattern", 80 | description = "Allows version selection to be extended beyond the original Node Semver semantics. So for example," + 81 | "Setting 'version' to \"25-29\" can be paired with a metadata pattern of \"-jre\" to select 29.0-jre", 82 | example = "-jre", 83 | required = false) 84 | @Nullable 85 | String versionPattern; 86 | 87 | @Option(displayName = "Because", 88 | description = "The reason for upgrading the transitive dependency. For example, we could be responding to a vulnerability.", 89 | required = false, 90 | example = "CVE-2021-1234") 91 | @Nullable 92 | String because; 93 | 94 | @Option(displayName = "Releases only", 95 | description = "Whether to exclude snapshots from consideration when using a semver selector", 96 | required = false) 97 | @Nullable 98 | Boolean releasesOnly; 99 | 100 | @Option(displayName = "Only if using glob expression for group:artifact", 101 | description = "Only add managed dependencies to projects having a dependency matching the expression.", 102 | example = "org.apache.logging.log4j:log4j*", 103 | required = false) 104 | @Nullable 105 | String onlyIfUsing; 106 | 107 | @Option(displayName = "Add to the root pom", 108 | description = "Add to the root pom where root is the eldest parent of the pom within the source set.", 109 | required = false) 110 | @Nullable 111 | Boolean addToRootPom; 112 | 113 | @Value 114 | public static class Accumulator { 115 | AddManagedDependency.Scanned mavenAccumulator; 116 | org.openrewrite.gradle.UpgradeTransitiveDependencyVersion.DependencyVersionState gradleAccumulator; 117 | } 118 | 119 | @Override 120 | public Accumulator getInitialValue(ExecutionContext ctx) { 121 | return new Accumulator(getMavenUpgradeTransitive().getInitialValue(ctx), getGradleUpgradeTransitive().getInitialValue(ctx)); 122 | } 123 | 124 | @Override 125 | public TreeVisitor getScanner(Accumulator acc) { 126 | TreeVisitor gradleUTDV = getGradleUpgradeTransitive().getScanner(acc.gradleAccumulator); 127 | TreeVisitor mavenUTDV = getMavenUpgradeTransitive().getScanner(acc.mavenAccumulator); 128 | 129 | return delegate(gradleUTDV, mavenUTDV); 130 | } 131 | 132 | @Override 133 | public TreeVisitor getVisitor(Accumulator acc) { 134 | TreeVisitor gradleUTDV = getGradleUpgradeTransitive().getVisitor(acc.gradleAccumulator); 135 | TreeVisitor mavenUTDV = getMavenUpgradeTransitive().getVisitor(acc.mavenAccumulator); 136 | 137 | return delegate(gradleUTDV, mavenUTDV); 138 | } 139 | 140 | private TreeVisitor delegate(TreeVisitor gradle, TreeVisitor maven) { 141 | return new TreeVisitor() { 142 | @Override 143 | public boolean isAcceptable(SourceFile sourceFile, ExecutionContext ctx) { 144 | return gradle.isAcceptable(sourceFile, ctx) || maven.isAcceptable(sourceFile, ctx); 145 | } 146 | 147 | @Override 148 | public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { 149 | if (!(tree instanceof SourceFile)) { 150 | return tree; 151 | } 152 | SourceFile t = (SourceFile) tree; 153 | if (gradle.isAcceptable(t, ctx)) { 154 | t = (SourceFile) gradle.visitNonNull(t, ctx); 155 | } else if (maven.isAcceptable(t, ctx)) { 156 | t = (SourceFile) maven.visitNonNull(t, ctx); 157 | } 158 | return t; 159 | } 160 | }; 161 | } 162 | 163 | private org.openrewrite.gradle.UpgradeTransitiveDependencyVersion getGradleUpgradeTransitive() { 164 | return new org.openrewrite.gradle.UpgradeTransitiveDependencyVersion(groupId, artifactId, version, versionPattern, because, null); 165 | } 166 | 167 | private org.openrewrite.maven.UpgradeTransitiveDependencyVersion getMavenUpgradeTransitive() { 168 | return new org.openrewrite.maven.UpgradeTransitiveDependencyVersion(groupId, artifactId, version, scope, type, classifier, versionPattern, releasesOnly, onlyIfUsing, addToRootPom, because); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/dependencies/UpgradeDependencyVersion.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies; 17 | 18 | import lombok.Data; 19 | import lombok.EqualsAndHashCode; 20 | import lombok.Getter; 21 | import lombok.RequiredArgsConstructor; 22 | import org.jspecify.annotations.Nullable; 23 | import org.openrewrite.*; 24 | 25 | import java.util.List; 26 | 27 | import static java.util.Objects.requireNonNull; 28 | 29 | 30 | @EqualsAndHashCode(callSuper = false) 31 | @Getter 32 | @RequiredArgsConstructor 33 | public class UpgradeDependencyVersion extends ScanningRecipe { 34 | @Option(displayName = "Group ID", 35 | description = "The first part of a dependency coordinate `com.google.guava:guava:VERSION`. This can be a glob expression.", 36 | example = "com.fasterxml.jackson*") 37 | private final String groupId; 38 | 39 | @Option(displayName = "Artifact ID", 40 | description = "The second part of a dependency coordinate `com.google.guava:guava:VERSION`. This can be a glob expression.", 41 | example = "jackson-module*") 42 | private final String artifactId; 43 | 44 | @Option(displayName = "New version", 45 | description = "An exact version number or node-style semver selector used to select the version number. ", 46 | example = "29.X") 47 | private final String newVersion; 48 | 49 | @Option(displayName = "Version pattern", 50 | description = "Allows version selection to be extended beyond the original Node Semver semantics. So for example," + 51 | "Setting 'version' to \"25-29\" can be paired with a metadata pattern of \"-jre\" to select Guava 29.0-jre", 52 | example = "-jre", 53 | required = false) 54 | @Nullable 55 | private final String versionPattern; 56 | 57 | @Option(displayName = "Override managed version", 58 | description = "For Maven project only, This flag can be set to explicitly override a managed " + 59 | "dependency's version. The default for this flag is `false`.", 60 | required = false) 61 | @Nullable 62 | private final Boolean overrideManagedVersion; 63 | 64 | @Option(displayName = "Retain versions", 65 | description = "For Maven project only, accepts a list of GAVs. For each GAV, if it is a project direct dependency, and it is removed " + 66 | "from dependency management after the changes from this recipe, then it will be retained with an explicit version. " + 67 | "The version can be omitted from the GAV to use the old value from dependency management.", 68 | example = "com.jcraft:jsch", 69 | required = false) 70 | @Nullable 71 | private final List retainVersions; 72 | 73 | @Override 74 | public String getDisplayName() { 75 | return "Upgrade Gradle or Maven dependency versions"; 76 | } 77 | 78 | @Override 79 | public String getDescription() { 80 | //language=markdown 81 | return "For Gradle projects, upgrade the version of a dependency in a `build.gradle` file. " + 82 | "Supports updating dependency declarations of various forms:\n" + 83 | " * `String` notation: `\"group:artifact:version\"` \n" + 84 | " * `Map` notation: `group: 'group', name: 'artifact', version: 'version'`\n" + 85 | "It is possible to update version numbers which are defined earlier in the same file in variable declarations.\n\n" + 86 | "For Maven projects, upgrade the version of a dependency by specifying a group ID and (optionally) an " + 87 | "artifact ID using Node Semver advanced range selectors, allowing more precise control over version " + 88 | "updates to patch or minor releases."; 89 | } 90 | 91 | @Override 92 | public Accumulator getInitialValue(ExecutionContext ctx) { 93 | return new Accumulator( 94 | getUpgradeMavenDependencyVersion().getInitialValue(ctx), 95 | getUpgradeGradleDependencyVersion().getInitialValue(ctx) 96 | ); 97 | } 98 | 99 | @Override 100 | public TreeVisitor getScanner(Accumulator acc) { 101 | TreeVisitor mavenScanner = getUpgradeMavenDependencyVersion().getScanner(acc.mavenAccumulator); 102 | TreeVisitor gradleScanner = getUpgradeGradleDependencyVersion().getScanner(acc.gradleAccumulator); 103 | return new TreeVisitor() { 104 | @Override 105 | public boolean isAcceptable(SourceFile sourceFile, ExecutionContext ctx) { 106 | return mavenScanner.isAcceptable(sourceFile, ctx) || gradleScanner.isAcceptable(sourceFile, ctx); 107 | } 108 | 109 | @Override 110 | public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { 111 | if (mavenScanner.isAcceptable((SourceFile) requireNonNull(tree), ctx)) { 112 | return mavenScanner.visit(tree, ctx); 113 | } 114 | if (gradleScanner.isAcceptable((SourceFile) tree, ctx)) { 115 | return gradleScanner.visit(tree, ctx); 116 | } 117 | return tree; 118 | } 119 | }; 120 | } 121 | 122 | @Override 123 | public TreeVisitor getVisitor(Accumulator acc) { 124 | TreeVisitor mavenVisitor = getUpgradeMavenDependencyVersion().getVisitor(acc.mavenAccumulator); 125 | TreeVisitor gradleVisitor = getUpgradeGradleDependencyVersion().getVisitor(acc.gradleAccumulator); 126 | return new TreeVisitor() { 127 | 128 | @Override 129 | public boolean isAcceptable(SourceFile sourceFile, ExecutionContext ctx) { 130 | return mavenVisitor.isAcceptable(sourceFile, ctx) || gradleVisitor.isAcceptable(sourceFile, ctx); 131 | } 132 | 133 | @Override 134 | public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { 135 | Tree t = tree; 136 | assert tree != null; 137 | if (mavenVisitor.isAcceptable((SourceFile) tree, ctx)) { 138 | t = mavenVisitor.visit(tree, ctx); 139 | } else if (gradleVisitor.isAcceptable((SourceFile) tree, ctx)) { 140 | t = gradleVisitor.visit(t, ctx); 141 | } 142 | return t; 143 | } 144 | }; 145 | } 146 | 147 | org.openrewrite.maven.UpgradeDependencyVersion getUpgradeMavenDependencyVersion() { 148 | return new org.openrewrite.maven.UpgradeDependencyVersion(groupId, artifactId, newVersion, versionPattern, overrideManagedVersion, retainVersions); 149 | } 150 | 151 | public org.openrewrite.gradle.UpgradeDependencyVersion getUpgradeGradleDependencyVersion() { 152 | return new org.openrewrite.gradle.UpgradeDependencyVersion(groupId, artifactId, newVersion, versionPattern); 153 | } 154 | 155 | @Data 156 | public static final class Accumulator { 157 | private final org.openrewrite.maven.UpgradeDependencyVersion.Accumulator mavenAccumulator; 158 | private final org.openrewrite.gradle.UpgradeDependencyVersion.DependencyVersionState gradleAccumulator; 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/test/java/org/openrewrite/java/dependencies/DependencyListTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies; 17 | 18 | import org.junit.jupiter.api.Test; 19 | import org.openrewrite.java.dependencies.table.DependencyListReport; 20 | import org.openrewrite.maven.tree.*; 21 | import org.openrewrite.test.RecipeSpec; 22 | import org.openrewrite.test.RewriteTest; 23 | 24 | import java.nio.file.Path; 25 | 26 | import static java.util.Collections.*; 27 | import static org.assertj.core.api.Assertions.assertThat; 28 | import static org.openrewrite.Tree.randomId; 29 | import static org.openrewrite.gradle.Assertions.buildGradle; 30 | import static org.openrewrite.gradle.Assertions.settingsGradle; 31 | import static org.openrewrite.gradle.toolingapi.Assertions.withToolingApi; 32 | import static org.openrewrite.maven.Assertions.pomXml; 33 | import static org.openrewrite.xml.Assertions.xml; 34 | 35 | @SuppressWarnings("GroovyUnusedAssignment") 36 | class DependencyListTest implements RewriteTest { 37 | 38 | @Override 39 | public void defaults(RecipeSpec spec) { 40 | spec.recipe(new DependencyList(DependencyList.Scope.Compile, true, false)); 41 | } 42 | 43 | @Test 44 | void basic() { 45 | rewriteRun( 46 | spec -> spec 47 | .beforeRecipe(withToolingApi()) 48 | .dataTable(DependencyListReport.Row.class, rows -> { 49 | assertThat(rows) 50 | .filteredOn(it -> "Maven".equals(it.getBuildTool()) && "rewrite-core".equals(it.getDependencyArtifactId())) 51 | .hasSize(1); 52 | assertThat(rows) 53 | .filteredOn(it -> "Gradle".equals(it.getBuildTool()) && "rewrite-core".equals(it.getDependencyArtifactId())) 54 | .hasSize(1); 55 | }), 56 | //language=groovy 57 | buildGradle( 58 | """ 59 | plugins { 60 | id 'java' 61 | } 62 | repositories { 63 | mavenCentral() 64 | } 65 | dependencies { 66 | implementation('org.openrewrite:rewrite-core:7.39.0') 67 | } 68 | """ 69 | ), 70 | //language=xml 71 | pomXml( 72 | """ 73 | 74 | com.mycompany.app 75 | my-app 76 | 1 77 | 78 | 79 | org.openrewrite 80 | rewrite-core 81 | 7.39.0 82 | 83 | 84 | 85 | """ 86 | ) 87 | ); 88 | } 89 | 90 | @Test 91 | void directOnly() { 92 | rewriteRun( 93 | spec -> spec.recipe(new DependencyList(DependencyList.Scope.Compile, false, false)) 94 | .beforeRecipe(withToolingApi()) 95 | .dataTable(DependencyListReport.Row.class, rows -> { 96 | assertThat(rows) 97 | .containsExactlyInAnyOrder( 98 | new DependencyListReport.Row("Gradle", "com.test", "test", "1.0.0", "io.micrometer.prometheus", "prometheus-rsocket-client", "1.5.3", true, ""), 99 | new DependencyListReport.Row("Maven", "com.test", "test", "1.0.0", "io.micrometer.prometheus", "prometheus-rsocket-client", "1.5.3", true, "")); 100 | }), 101 | settingsGradle("rootProject.name = 'test'"), 102 | buildGradle( 103 | //language=groovy 104 | """ 105 | plugins { 106 | id 'java' 107 | } 108 | group = 'com.test' 109 | version = '1.0.0' 110 | repositories { 111 | mavenCentral() 112 | } 113 | dependencies { 114 | implementation('io.micrometer.prometheus:prometheus-rsocket-client:1.5.3') 115 | } 116 | """ 117 | ), 118 | pomXml( 119 | //language=xml 120 | """ 121 | 122 | com.test 123 | test 124 | 1.0.0 125 | 126 | 127 | io.micrometer.prometheus 128 | prometheus-rsocket-client 129 | 1.5.3 130 | 131 | 132 | 133 | """ 134 | ) 135 | ); 136 | } 137 | 138 | @Test 139 | void validateResolvable() { 140 | rewriteRun( 141 | spec -> spec.recipe(new DependencyList(DependencyList.Scope.Compile, false, true)) 142 | .dataTable(DependencyListReport.Row.class, rows -> assertThat(rows) 143 | .singleElement() 144 | .extracting(DependencyListReport.Row::getResolutionFailure) 145 | .matches(it -> it.startsWith("org.openrewrite.maven.MavenDownloadingException"))), 146 | xml( 147 | //language=xml 148 | """ 149 | 150 | com.test 151 | test 152 | 1.0.0 153 | 154 | 155 | com.test 156 | doesnotexist 157 | 1.0.0 158 | 159 | 160 | 161 | """, 162 | spec -> { 163 | // Manually construct a resolution result since, obviously, the above cannot be resolved 164 | MavenRepository pretendRepo = MavenRepository.builder() 165 | .id("nonexistent") 166 | .uri("https://nonexistent") 167 | .build(); 168 | Dependency requested = Dependency.builder() 169 | .gav(new GroupArtifactVersion("com.test", "doesnotexist", "1.0.0")) 170 | .build(); 171 | ResolvedGroupArtifactVersion rgav = new ResolvedGroupArtifactVersion(pretendRepo.getId(), "com.test", "doesnotexist", "1.0.0", null); 172 | spec.path(Path.of("pom.xml")) 173 | .markers(new MavenResolutionResult( 174 | randomId(), 175 | null, 176 | ResolvedPom.builder() 177 | .requested(Pom.builder() 178 | .gav(rgav) 179 | .build()) 180 | .repositories(singletonList(pretendRepo)) 181 | .build(), 182 | emptyList(), 183 | null, 184 | singletonMap(Scope.Compile, singletonList(new ResolvedDependency( 185 | pretendRepo, 186 | rgav, requested, emptyList(), emptyList(), null, null, null, 0, null) 187 | )), 188 | null, 189 | emptyList(), 190 | emptyMap() 191 | )); 192 | } 193 | ) 194 | ); 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/test/java/org/openrewrite/java/dependencies/RemoveDependencyTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies; 17 | 18 | import org.junit.jupiter.api.Test; 19 | import org.openrewrite.DocumentExample; 20 | import org.openrewrite.Issue; 21 | import org.openrewrite.java.JavaParser; 22 | import org.openrewrite.test.RewriteTest; 23 | 24 | import static org.openrewrite.gradle.Assertions.buildGradle; 25 | import static org.openrewrite.gradle.toolingapi.Assertions.withToolingApi; 26 | import static org.openrewrite.java.Assertions.*; 27 | import static org.openrewrite.maven.Assertions.pomXml; 28 | 29 | class RemoveDependencyTest implements RewriteTest { 30 | 31 | @DocumentExample("Remove a Gradle dependency") 32 | @Test 33 | void removeGradleDependencyUsingStringNotationWithExclusion() { 34 | rewriteRun( 35 | spec -> spec.beforeRecipe(withToolingApi()) 36 | .recipe(new RemoveDependency("org.springframework.boot", "spring-boot*", null, null, null)), 37 | //language=groovy 38 | buildGradle( 39 | """ 40 | plugins { 41 | id 'java-library' 42 | } 43 | 44 | repositories { 45 | mavenCentral() 46 | } 47 | 48 | dependencies { 49 | implementation("org.springframework.boot:spring-boot-starter-web:2.7.0") { 50 | exclude group: "junit" 51 | } 52 | testImplementation "org.junit.vintage:junit-vintage-engine:5.6.2" 53 | } 54 | """, 55 | """ 56 | plugins { 57 | id 'java-library' 58 | } 59 | 60 | repositories { 61 | mavenCentral() 62 | } 63 | 64 | dependencies { 65 | testImplementation "org.junit.vintage:junit-vintage-engine:5.6.2" 66 | } 67 | """ 68 | ) 69 | ); 70 | } 71 | 72 | @Test 73 | void removeMavenDependency() { 74 | rewriteRun( 75 | spec -> spec.recipe(new RemoveDependency("junit", "junit", null, null, null)), 76 | //language=xml 77 | pomXml( 78 | """ 79 | 80 | 4.0.0 81 | 82 | com.mycompany.app 83 | my-app 84 | 1 85 | 86 | 87 | 88 | com.google.guava 89 | guava 90 | 29.0-jre 91 | 92 | 93 | junit 94 | junit 95 | 4.13.1 96 | test 97 | 98 | 99 | 100 | """, 101 | """ 102 | 103 | 4.0.0 104 | 105 | com.mycompany.app 106 | my-app 107 | 1 108 | 109 | 110 | 111 | com.google.guava 112 | guava 113 | 29.0-jre 114 | 115 | 116 | 117 | """ 118 | ) 119 | ); 120 | } 121 | 122 | @Issue("https://github.com/openrewrite/rewrite-java-dependencies/issues/11") 123 | @Test 124 | void doNotRemoveIfInUse() { 125 | rewriteRun( 126 | spec -> spec 127 | .parser(JavaParser.fromJavaVersion().dependsOn( 128 | //language=java 129 | """ 130 | package org.aspectj.lang.annotation; 131 | 132 | import java.lang.annotation.Target; 133 | import java.lang.annotation.ElementType; 134 | import java.lang.annotation.Retention; 135 | import java.lang.annotation.RetentionPolicy; 136 | 137 | @Retention(RetentionPolicy.RUNTIME) 138 | @Target(ElementType.TYPE) 139 | public @interface Aspect { 140 | } 141 | """ 142 | )) 143 | .recipe(new RemoveDependency("org.aspectj", "aspectjrt", "org.aspectj.lang.annotation.*", null, null)), 144 | mavenProject("example", 145 | //language=java 146 | srcMainJava( 147 | java( 148 | """ 149 | import org.aspectj.lang.annotation.Aspect; 150 | @Aspect 151 | class MyLoggingInterceptor { 152 | } 153 | """ 154 | ) 155 | ), 156 | //language=xml 157 | pomXml( 158 | """ 159 | 160 | 4.0.0 161 | 162 | com.mycompany.app 163 | my-app 164 | 1 165 | 166 | 167 | 168 | org.aspectj 169 | aspectjrt 170 | 1.9.22.1 171 | 172 | 173 | 174 | """ 175 | ) 176 | ) 177 | ); 178 | } 179 | 180 | @Issue("https://github.com/openrewrite/rewrite-java-dependencies/issues/11") 181 | @Test 182 | void doRemoveIfNotInUse() { 183 | rewriteRun( 184 | spec -> spec.recipe(new RemoveDependency("org.aspectj", "aspectjrt", "java.lang.String", null, null)), 185 | mavenProject("example", 186 | //language=java 187 | srcMainJava( 188 | java( 189 | """ 190 | class MyLoggingInterceptor { 191 | // Not using String anywhere here; the dependency should be removed 192 | } 193 | """ 194 | ) 195 | ), 196 | //language=xml 197 | pomXml( 198 | """ 199 | 200 | 4.0.0 201 | 202 | com.mycompany.app 203 | my-app 204 | 1 205 | 206 | 207 | 208 | org.aspectj 209 | aspectjrt 210 | 1.9.22.1 211 | 212 | 213 | 214 | """, 215 | """ 216 | 217 | 4.0.0 218 | 219 | com.mycompany.app 220 | my-app 221 | 1 222 | 223 | """ 224 | ) 225 | ) 226 | ); 227 | } 228 | } 229 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/dependencies/search/FindMinimumJUnitVersion.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies.search; 17 | 18 | import lombok.EqualsAndHashCode; 19 | import lombok.Value; 20 | import org.jspecify.annotations.Nullable; 21 | import org.openrewrite.*; 22 | import org.openrewrite.gradle.marker.GradleDependencyConfiguration; 23 | import org.openrewrite.gradle.marker.GradleProject; 24 | import org.openrewrite.internal.StringUtils; 25 | import org.openrewrite.java.dependencies.internal.StaticVersionComparator; 26 | import org.openrewrite.java.dependencies.internal.VersionParser; 27 | import org.openrewrite.marker.Markers; 28 | import org.openrewrite.maven.table.DependenciesInUse; 29 | import org.openrewrite.maven.tree.GroupArtifact; 30 | import org.openrewrite.maven.tree.MavenResolutionResult; 31 | import org.openrewrite.maven.tree.ResolvedDependency; 32 | import org.openrewrite.maven.tree.ResolvedGroupArtifactVersion; 33 | 34 | import java.util.HashMap; 35 | import java.util.List; 36 | import java.util.Map; 37 | import java.util.Objects; 38 | 39 | import static org.openrewrite.java.dependencies.search.FindMinimumDependencyVersion.applyMarkersForLocatedGavs; 40 | 41 | @EqualsAndHashCode(callSuper = false) 42 | @Value 43 | public class FindMinimumJUnitVersion extends ScanningRecipe> { 44 | transient DependenciesInUse dependenciesInUse = new DependenciesInUse(this); 45 | 46 | @Option(displayName = "Version", 47 | description = "Determine if the provided version is the minimum JUnit version. " + 48 | "If both JUnit 4 and JUnit 5 are present, the minimum version is JUnit 4. " + 49 | "If only one version is present, that version is the minimum version.", 50 | example = "4", 51 | valid = {"4", "5"}, 52 | required = false) 53 | @Nullable 54 | String minimumVersion; 55 | 56 | 57 | @Override 58 | public String getDisplayName() { 59 | return "Find minimum JUnit version"; 60 | } 61 | 62 | @Override 63 | public String getDescription() { 64 | return "A recipe to find the minimum version of JUnit dependencies. " + 65 | "This recipe is designed to return the minimum version of JUnit in a project. " + 66 | "It will search for JUnit 4 and JUnit 5 dependencies in the project. " + 67 | "If both versions are found, it will return the minimum version of JUnit 4.\n" + 68 | "If a minimumVersion is provided, the recipe will search to see if " + 69 | "the minimum version of JUnit used by the project is no lower than the minimumVersion.\n" + 70 | "For example: if the minimumVersion is 4, and the project has JUnit 4.12 and JUnit 5.7, " + 71 | "the recipe will return JUnit 4.12. If the project has only JUnit 5.7, the recipe will return JUnit 5.7.\n" + 72 | "Another example: if the minimumVersion is 5, and the project has JUnit 4.12 and JUnit 5.7, " + 73 | "the recipe will not return any results."; 74 | } 75 | 76 | @Override 77 | public Map getInitialValue(ExecutionContext ctx) { 78 | return new HashMap<>(); 79 | } 80 | 81 | @Override 82 | public TreeVisitor getScanner(Map acc) { 83 | return new TreeVisitor() { 84 | @Override 85 | public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { 86 | if (tree == null) { 87 | return null; 88 | } 89 | VersionParser versionParser = new VersionParser(); 90 | Markers m = tree.getMarkers(); 91 | m.findFirst(GradleProject.class).ifPresent(gradle -> { 92 | for (GradleDependencyConfiguration conf : gradle.getConfigurations()) { 93 | collectionJUnit4(versionParser, conf.getResolved(), acc); 94 | collectionJUnit5(versionParser, conf.getResolved(), acc); 95 | } 96 | }); 97 | m.findFirst(MavenResolutionResult.class).ifPresent(maven -> { 98 | for (List resolved : maven.getDependencies().values()) { 99 | collectionJUnit4(versionParser, resolved, acc); 100 | collectionJUnit5(versionParser, resolved, acc); 101 | } 102 | }); 103 | return tree; 104 | } 105 | }; 106 | } 107 | 108 | private boolean isResolvedGroupArtifactVersion(ResolvedGroupArtifactVersion resolvedGroupArtifactVersion, String groupName, String artifactName) { 109 | return resolvedGroupArtifactVersion.getArtifactId().equals(artifactName) && 110 | resolvedGroupArtifactVersion.getGroupId().equals(groupName); 111 | } 112 | 113 | @Override 114 | public TreeVisitor getVisitor(Map acc) { 115 | boolean hasJUnit4 = acc.values().stream().anyMatch(resolvedGroupArtifactVersion -> isResolvedGroupArtifactVersion(resolvedGroupArtifactVersion, "junit", "junit")); 116 | boolean hasJUnit5 = acc.values().stream().anyMatch(resolvedGroupArtifactVersion -> isResolvedGroupArtifactVersion(resolvedGroupArtifactVersion, "org.junit.jupiter", "junit-jupiter-api")); 117 | if (Objects.equals(minimumVersion, "4")) { 118 | if (hasJUnit4) { 119 | acc.entrySet().removeIf(e -> !isResolvedGroupArtifactVersion(e.getValue(), "junit", "junit")); 120 | } else { 121 | return TreeVisitor.noop(); 122 | } 123 | } else if (Objects.equals(minimumVersion, "5")) { 124 | if (hasJUnit4) { 125 | return TreeVisitor.noop(); 126 | } 127 | acc.entrySet().removeIf(e -> !isResolvedGroupArtifactVersion(e.getValue(), "org.junit.jupiter", "junit-jupiter-api")); 128 | } else { 129 | if (hasJUnit4 && hasJUnit5) { 130 | acc.entrySet().removeIf(e -> !isResolvedGroupArtifactVersion(e.getValue(), "junit", "junit")); 131 | } 132 | } 133 | 134 | return applyMarkersForLocatedGavs(acc, dependenciesInUse); 135 | } 136 | 137 | private void collectionJUnit4(VersionParser versionParser, List resolved, 138 | Map acc) { 139 | collectVersion(versionParser, resolved, "junit", "junit", acc); 140 | } 141 | 142 | private void collectionJUnit5(VersionParser versionParser, List resolved, 143 | Map acc) { 144 | collectVersion(versionParser, resolved, "org.junit.jupiter", "junit-jupiter-api", acc); 145 | } 146 | 147 | private static void collectVersion(VersionParser versionParser, List resolved, String groupIdPattern, String artifactIdPattern, Map acc) { 148 | StaticVersionComparator versionComparator = new StaticVersionComparator(); 149 | for (ResolvedDependency dep : resolved) { 150 | if (StringUtils.matchesGlob(dep.getGroupId(), groupIdPattern) && 151 | StringUtils.matchesGlob(dep.getArtifactId(), artifactIdPattern)) { 152 | acc.merge(new GroupArtifact(dep.getGroupId(), dep.getArtifactId()), 153 | dep.getGav(), (d1, d2) -> versionComparator.compare( 154 | versionParser.transform(d1.getVersion()), 155 | versionParser.transform(d2.getVersion())) < 0 ? 156 | d1 : d2); 157 | } 158 | } 159 | } 160 | } 161 | -------------------------------------------------------------------------------- /src/test/java/org/openrewrite/java/dependencies/search/FindMinimumDependencyVersionTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies.search; 17 | 18 | import org.junit.jupiter.api.Test; 19 | import org.openrewrite.DocumentExample; 20 | import org.openrewrite.test.RecipeSpec; 21 | import org.openrewrite.test.RewriteTest; 22 | 23 | import static org.openrewrite.gradle.Assertions.buildGradle; 24 | import static org.openrewrite.gradle.toolingapi.Assertions.withToolingApi; 25 | import static org.openrewrite.java.Assertions.mavenProject; 26 | import static org.openrewrite.maven.Assertions.pomXml; 27 | 28 | class FindMinimumDependencyVersionTest implements RewriteTest { 29 | 30 | @Override 31 | public void defaults(RecipeSpec spec) { 32 | spec.recipe(new FindMinimumDependencyVersion("com.fasterxml.jackson*", "jackson-core", "2.14-2.16")); 33 | } 34 | 35 | @DocumentExample 36 | @Test 37 | void minimumMaven() { 38 | rewriteRun( 39 | mavenProject( 40 | "core", 41 | //language=xml 42 | pomXml( 43 | """ 44 | 45 | org.openrewrite 46 | core 47 | 0.1.0-SNAPSHOT 48 | 49 | 50 | com.fasterxml.jackson.core 51 | jackson-core 52 | 2.14.0 53 | 54 | 55 | com.fasterxml.jackson.core 56 | jackson-databind 57 | 2.15.0 58 | 59 | 60 | 61 | """, 62 | """ 63 | 64 | org.openrewrite 65 | core 66 | 0.1.0-SNAPSHOT 67 | 68 | 69 | com.fasterxml.jackson.core 70 | jackson-core 71 | 2.14.0 72 | 73 | 74 | com.fasterxml.jackson.core 75 | jackson-databind 76 | 2.15.0 77 | 78 | 79 | 80 | """ 81 | ) 82 | ), 83 | mavenProject( 84 | "server", 85 | //language=xml 86 | pomXml( 87 | """ 88 | 89 | org.openrewrite 90 | server 91 | 0.1.0-SNAPSHOT 92 | 93 | 94 | com.fasterxml.jackson.core 95 | jackson-core 96 | 2.15.0 97 | 98 | 99 | 100 | """ 101 | ) 102 | ) 103 | ); 104 | } 105 | 106 | @Test 107 | void minimumGradle() { 108 | rewriteRun( 109 | spec -> spec.beforeRecipe(withToolingApi()), 110 | mavenProject( 111 | "core", 112 | //language=groovy 113 | buildGradle( 114 | """ 115 | plugins { id 'java' } 116 | repositories { mavenCentral() } 117 | dependencies { 118 | implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.0' 119 | } 120 | """, 121 | """ 122 | /*~~(com.fasterxml.jackson.core:jackson-core:2.14.0)~~>*/plugins { id 'java' } 123 | repositories { mavenCentral() } 124 | dependencies { 125 | implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.0' 126 | } 127 | """ 128 | ) 129 | ), 130 | mavenProject( 131 | "server", 132 | //language=groovy 133 | buildGradle( 134 | """ 135 | plugins { id 'java' } 136 | repositories { mavenCentral() } 137 | dependencies { 138 | implementation 'com.fasterxml.jackson.core:jackson-core:2.16.1' 139 | } 140 | """ 141 | ) 142 | ) 143 | ); 144 | } 145 | 146 | @Test 147 | void noMatchBecauseVersionIsOutsideRange() { 148 | rewriteRun( 149 | //language=xml 150 | pomXml( 151 | """ 152 | 153 | org.openrewrite 154 | core 155 | 0.1.0-SNAPSHOT 156 | 157 | 158 | com.fasterxml.jackson.core 159 | jackson-core 160 | 2.11.0 161 | 162 | 163 | 164 | """ 165 | ) 166 | ); 167 | } 168 | 169 | @Test 170 | void findMultiple() { 171 | rewriteRun( 172 | //language=yaml 173 | spec -> spec.recipeFromYaml(""" 174 | type: specs.openrewrite.org/v1beta/recipe 175 | name: org.openrewrite.MyRecipe 176 | description: composite recipe finding 2 versions. 177 | recipeList: 178 | - org.openrewrite.java.dependencies.search.FindMinimumDependencyVersion: 179 | groupIdPattern: com.fasterxml.jackson.core 180 | artifactIdPattern: jackson-core 181 | version: 2.14-2.16 182 | - org.openrewrite.java.dependencies.search.FindMinimumDependencyVersion: 183 | groupIdPattern: commons-lang 184 | artifactIdPattern: commons-lang 185 | version: 2.5-2.7 186 | """, "org.openrewrite.MyRecipe"), 187 | //language=xml 188 | pomXml( 189 | """ 190 | 191 | org.openrewrite 192 | core 193 | 0.1.0-SNAPSHOT 194 | 195 | 196 | com.fasterxml.jackson.core 197 | jackson-core 198 | 2.15.0 199 | 200 | 201 | commons-lang 202 | commons-lang 203 | 2.6 204 | 205 | 206 | 207 | """, 208 | """ 209 | 210 | org.openrewrite 211 | core 212 | 0.1.0-SNAPSHOT 213 | 214 | 215 | com.fasterxml.jackson.core 216 | jackson-core 217 | 2.15.0 218 | 219 | 220 | commons-lang 221 | commons-lang 222 | 2.6 223 | 224 | 225 | 226 | """ 227 | ) 228 | ); 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/dependencies/AddDependency.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies; 17 | 18 | import lombok.EqualsAndHashCode; 19 | import lombok.Value; 20 | import org.jspecify.annotations.Nullable; 21 | import org.openrewrite.*; 22 | import org.openrewrite.maven.tree.Scope; 23 | 24 | @EqualsAndHashCode(callSuper = false) 25 | @Value 26 | public class AddDependency extends ScanningRecipe { 27 | // Gradle and Maven shared parameters 28 | @Option(displayName = "Group ID", 29 | description = "The first part of a dependency coordinate `com.google.guava:guava:VERSION`.", 30 | example = "com.google.guava") 31 | String groupId; 32 | 33 | @Option(displayName = "Artifact ID", 34 | description = "The second part of a dependency coordinate `com.google.guava:guava:VERSION`", 35 | example = "guava") 36 | String artifactId; 37 | 38 | @Option(displayName = "Version", 39 | description = "An exact version number or node-style semver selector used to select the version number.", 40 | example = "29.X", 41 | required = false) 42 | @Nullable 43 | String version; 44 | 45 | @Option(displayName = "Version pattern", 46 | description = "Allows version selection to be extended beyond the original Node Semver semantics. So for example, " + 47 | "Setting 'version' to \"25-29\" can be paired with a metadata pattern of \"-jre\" to select Guava 29.0-jre", 48 | example = "-jre", 49 | required = false) 50 | @Nullable 51 | String versionPattern; 52 | 53 | @Option(displayName = "Only if using", 54 | description = "Used to determine if the dependency will be added and in which scope it should be placed.", 55 | example = "org.junit.jupiter.api.*", 56 | required = false) 57 | @Nullable 58 | String onlyIfUsing; 59 | 60 | @Option(displayName = "Classifier", 61 | description = "A classifier to add. Commonly used to select variants of a library.", 62 | example = "test", 63 | required = false) 64 | @Nullable 65 | String classifier; 66 | 67 | @Option(displayName = "Family pattern", 68 | description = "A pattern, applied to groupIds, used to determine which other dependencies should have aligned version numbers. " + 69 | "Accepts '*' as a wildcard character.", 70 | example = "com.fasterxml.jackson*", 71 | required = false) 72 | @Nullable 73 | String familyPattern; 74 | 75 | // Gradle only parameters 76 | @Option(displayName = "Extension", 77 | description = "For Gradle only, The extension of the dependency to add. If omitted Gradle defaults to assuming the type is \"jar\".", 78 | example = "jar", 79 | required = false) 80 | @Nullable 81 | String extension; 82 | 83 | @Option(displayName = "Gradle configuration", 84 | description = "The Gradle dependency configuration name within which to place the dependency. " + 85 | "When omitted the configuration will be determined by the Maven scope parameter. " + 86 | "If that parameter is also omitted, configuration will be determined based on where types " + 87 | "matching `onlyIfUsing` appear in source code.", 88 | example = "implementation", 89 | required = false) 90 | @Nullable 91 | String configuration; 92 | 93 | // Maven only parameters 94 | @Option(displayName = "Maven scope", 95 | description = "The Maven scope within which to place the dependency. " + 96 | "When omitted scope will be determined based on where types matching `onlyIfUsing` appear in source code.", 97 | example = "runtime", 98 | valid = {"compile", "provided", "runtime", "test"}, 99 | required = false) 100 | @Nullable 101 | String scope; 102 | 103 | @Option(displayName = "Releases only", 104 | description = "For Maven only, Whether to exclude snapshots from consideration when using a semver selector", 105 | required = false) 106 | @Nullable 107 | Boolean releasesOnly; 108 | 109 | @Option(displayName = "Type", 110 | description = "For Maven only, The type of dependency to add. If omitted Maven defaults to assuming the type is \"jar\".", 111 | valid = {"jar", "pom", "war"}, 112 | example = "jar", 113 | required = false) 114 | @Nullable 115 | String type; 116 | 117 | @Option(displayName = "Optional", 118 | description = "Set the value of the `` tag. No `` tag will be added when this is `null`.", 119 | required = false) 120 | @Nullable 121 | Boolean optional; 122 | 123 | @Option(displayName = "Accept transitive", 124 | description = "Default false. If enabled, the dependency will not be added if it is already on the classpath as a transitive dependency.", 125 | example = "true", 126 | required = false) 127 | @Nullable 128 | Boolean acceptTransitive; 129 | 130 | @Override 131 | public String getDisplayName() { 132 | return "Add Gradle or Maven dependency"; 133 | } 134 | 135 | @Override 136 | public String getDescription() { 137 | return "For a Gradle project, add a gradle dependency to a `build.gradle` file in the correct configuration " + 138 | "based on where it is used. Or For a maven project, Add a Maven dependency to a `pom.xml` file in the " + 139 | "correct scope based on where it is used."; 140 | } 141 | 142 | @Override 143 | public Accumulator getInitialValue(ExecutionContext ctx) { 144 | return new Accumulator(gradleAddDep().getInitialValue(ctx), mavenAddDep().getInitialValue(ctx)); 145 | } 146 | 147 | @Override 148 | public TreeVisitor getScanner(Accumulator acc) { 149 | return new TreeVisitor() { 150 | @Override 151 | public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { 152 | gradleAddDep().getScanner(acc.gradleAccumulator).visit(tree, ctx); 153 | mavenAddDep().getScanner(acc.mavenAccumulator).visit(tree, ctx); 154 | return tree; 155 | } 156 | }; 157 | } 158 | 159 | @Override 160 | public TreeVisitor getVisitor(Accumulator acc) { 161 | return new TreeVisitor() { 162 | final TreeVisitor gradleAddDep = gradleAddDep().getVisitor(acc.gradleAccumulator); 163 | final TreeVisitor mavenAddDep = mavenAddDep().getVisitor(acc.mavenAccumulator); 164 | 165 | @Override 166 | public boolean isAcceptable(SourceFile sourceFile, ExecutionContext ctx) { 167 | return gradleAddDep.isAcceptable(sourceFile, ctx) || mavenAddDep.isAcceptable(sourceFile, ctx); 168 | } 169 | 170 | @Override 171 | public Tree visit(Tree tree, ExecutionContext ctx) { 172 | if (!(tree instanceof SourceFile)) { 173 | return tree; 174 | } 175 | Tree t = tree; 176 | if (gradleAddDep.isAcceptable((SourceFile) t, ctx)) { 177 | t = gradleAddDep.visitNonNull(tree, ctx); 178 | } 179 | if (mavenAddDep.isAcceptable((SourceFile) t, ctx)) { 180 | t = mavenAddDep.visitNonNull(tree, ctx); 181 | } 182 | return t; 183 | } 184 | }; 185 | } 186 | 187 | @Value 188 | public static class Accumulator { 189 | org.openrewrite.gradle.AddDependency.Scanned gradleAccumulator; 190 | org.openrewrite.maven.AddDependency.Scanned mavenAccumulator; 191 | } 192 | 193 | private org.openrewrite.gradle.AddDependency gradleAddDep() { 194 | String configurationName = null; 195 | if(configuration != null) { 196 | configurationName = configuration; 197 | } else if(scope != null) { 198 | configurationName = Scope.asGradleConfigurationName(Scope.fromName(scope)); 199 | } 200 | return new org.openrewrite.gradle.AddDependency(groupId, artifactId, version, versionPattern, 201 | configurationName, onlyIfUsing, classifier, extension, familyPattern, acceptTransitive); 202 | } 203 | 204 | private org.openrewrite.maven.AddDependency mavenAddDep() { 205 | return new org.openrewrite.maven.AddDependency(groupId, artifactId, version != null ? version : "latest.release", 206 | versionPattern, scope, releasesOnly, onlyIfUsing, type, classifier, optional, familyPattern, 207 | acceptTransitive); 208 | } 209 | } 210 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | 118 | 119 | # Determine the Java command to use to start the JVM. 120 | if [ -n "$JAVA_HOME" ] ; then 121 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 122 | # IBM's JDK on AIX uses strange locations for the executables 123 | JAVACMD=$JAVA_HOME/jre/sh/java 124 | else 125 | JAVACMD=$JAVA_HOME/bin/java 126 | fi 127 | if [ ! -x "$JAVACMD" ] ; then 128 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 129 | 130 | Please set the JAVA_HOME variable in your environment to match the 131 | location of your Java installation." 132 | fi 133 | else 134 | JAVACMD=java 135 | if ! command -v java >/dev/null 2>&1 136 | then 137 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 138 | 139 | Please set the JAVA_HOME variable in your environment to match the 140 | location of your Java installation." 141 | fi 142 | fi 143 | 144 | # Increase the maximum file descriptors if we can. 145 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 146 | case $MAX_FD in #( 147 | max*) 148 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 149 | # shellcheck disable=SC2039,SC3045 150 | MAX_FD=$( ulimit -H -n ) || 151 | warn "Could not query maximum file descriptor limit" 152 | esac 153 | case $MAX_FD in #( 154 | '' | soft) :;; #( 155 | *) 156 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 157 | # shellcheck disable=SC2039,SC3045 158 | ulimit -n "$MAX_FD" || 159 | warn "Could not set maximum file descriptor limit to $MAX_FD" 160 | esac 161 | fi 162 | 163 | # Collect all arguments for the java command, stacking in reverse order: 164 | # * args from the command line 165 | # * the main class name 166 | # * -classpath 167 | # * -D...appname settings 168 | # * --module-path (only if needed) 169 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 170 | 171 | # For Cygwin or MSYS, switch paths to Windows format before running java 172 | if "$cygwin" || "$msys" ; then 173 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 174 | 175 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 176 | 177 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 178 | for arg do 179 | if 180 | case $arg in #( 181 | -*) false ;; # don't mess with options #( 182 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 183 | [ -e "$t" ] ;; #( 184 | *) false ;; 185 | esac 186 | then 187 | arg=$( cygpath --path --ignore --mixed "$arg" ) 188 | fi 189 | # Roll the args list around exactly as many times as the number of 190 | # args, so each arg winds up back in the position where it started, but 191 | # possibly modified. 192 | # 193 | # NB: a `for` loop captures its iteration list before it begins, so 194 | # changing the positional parameters here affects neither the number of 195 | # iterations, nor the values presented in `arg`. 196 | shift # remove old arg 197 | set -- "$@" "$arg" # push replacement arg 198 | done 199 | fi 200 | 201 | 202 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 203 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 204 | 205 | # Collect all arguments for the java command: 206 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 207 | # and any embedded shellness will be escaped. 208 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 209 | # treated as '${Hostname}' itself on the command line. 210 | 211 | set -- \ 212 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 213 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 214 | "$@" 215 | 216 | # Stop when "xargs" is not available. 217 | if ! command -v xargs >/dev/null 2>&1 218 | then 219 | die "xargs is not available" 220 | fi 221 | 222 | # Use "xargs" to parse quoted args. 223 | # 224 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 225 | # 226 | # In Bash we could simply go: 227 | # 228 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 229 | # set -- "${ARGS[@]}" "$@" 230 | # 231 | # but POSIX shell has neither arrays nor command substitution, so instead we 232 | # post-process each arg (as a line of input to sed) to backslash-escape any 233 | # character that might be a shell metacharacter, then use eval to reverse 234 | # that process (while maintaining the separation between arguments), and wrap 235 | # the whole thing up as a single "set" statement. 236 | # 237 | # This will of course break if any of these variables contains a newline or 238 | # an unmatched quote. 239 | # 240 | 241 | eval "set -- $( 242 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 243 | xargs -n1 | 244 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 245 | tr '\n' ' ' 246 | )" '"$@"' 247 | 248 | exec "$JAVACMD" "$@" 249 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/dependencies/search/FindMinimumDependencyVersion.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies.search; 17 | 18 | import lombok.EqualsAndHashCode; 19 | import lombok.Value; 20 | import org.jspecify.annotations.Nullable; 21 | import org.openrewrite.*; 22 | import org.openrewrite.gradle.IsBuildGradle; 23 | import org.openrewrite.gradle.marker.GradleDependencyConfiguration; 24 | import org.openrewrite.gradle.marker.GradleProject; 25 | import org.openrewrite.internal.StringUtils; 26 | import org.openrewrite.java.dependencies.internal.StaticVersionComparator; 27 | import org.openrewrite.java.dependencies.internal.VersionParser; 28 | import org.openrewrite.java.marker.JavaSourceSet; 29 | import org.openrewrite.marker.Markers; 30 | import org.openrewrite.marker.SearchResult; 31 | import org.openrewrite.maven.search.FindMavenProject; 32 | import org.openrewrite.maven.table.DependenciesInUse; 33 | import org.openrewrite.maven.tree.*; 34 | import org.openrewrite.semver.Semver; 35 | import org.openrewrite.semver.VersionComparator; 36 | 37 | import java.util.*; 38 | 39 | import static java.util.Objects.requireNonNull; 40 | 41 | @EqualsAndHashCode(callSuper = false) 42 | @Value 43 | public class FindMinimumDependencyVersion extends ScanningRecipe> { 44 | transient DependenciesInUse dependenciesInUse = new DependenciesInUse(this); 45 | 46 | @Option(displayName = "Group pattern", 47 | description = "Group ID glob pattern used to match dependencies.", 48 | example = "com.fasterxml.jackson.module") 49 | String groupIdPattern; 50 | 51 | @Option(displayName = "Artifact pattern", 52 | description = "Artifact ID glob pattern used to match dependencies.", 53 | example = "jackson-module-*") 54 | String artifactIdPattern; 55 | 56 | @Option(displayName = "Version", 57 | description = "Match only dependencies with the specified version. " + 58 | "Node-style [version selectors](https://docs.openrewrite.org/reference/dependency-version-selectors) may be used. " + 59 | "All versions are searched by default.", 60 | example = "1.x", 61 | required = false) 62 | @Nullable 63 | String version; 64 | 65 | @Override 66 | public String getDisplayName() { 67 | return "Find the oldest matching dependency version in use"; 68 | } 69 | 70 | @Override 71 | public String getDescription() { 72 | return "The oldest dependency version in use is the lowest dependency " + 73 | "version in use in any source set of any subproject of " + 74 | "a repository. It is possible that, for example, the main " + 75 | "source set of a project uses Jackson 2.11, but a test source set " + 76 | "uses Jackson 2.16. In this case, the oldest Jackson version in use is " + 77 | "Java 2.11."; 78 | } 79 | 80 | @Override 81 | public Map getInitialValue(ExecutionContext ctx) { 82 | return new HashMap<>(); 83 | } 84 | 85 | @Override 86 | public TreeVisitor getScanner(Map acc) { 87 | return new TreeVisitor() { 88 | @Override 89 | public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { 90 | if (tree == null) { 91 | return null; 92 | } 93 | VersionParser versionParser = new VersionParser(); 94 | Markers m = tree.getMarkers(); 95 | m.findFirst(GradleProject.class).ifPresent(gradle -> { 96 | for (GradleDependencyConfiguration conf : gradle.getConfigurations()) { 97 | collectMinimumVersions(versionParser, conf.getResolved(), acc); 98 | } 99 | }); 100 | m.findFirst(MavenResolutionResult.class).ifPresent(maven -> { 101 | for (List resolved : maven.getDependencies().values()) { 102 | collectMinimumVersions(versionParser, resolved, acc); 103 | } 104 | }); 105 | return tree; 106 | } 107 | }; 108 | } 109 | 110 | @Override 111 | public TreeVisitor getVisitor(Map acc) { 112 | VersionComparator versionComparator = version == null ? null : 113 | requireNonNull(Semver.validate(version, null).getValue()); 114 | StaticVersionComparator staticVersionComparator = new StaticVersionComparator(); 115 | VersionParser versionParser = new VersionParser(); 116 | String minimumVersion = acc.values().stream().map(ResolvedGroupArtifactVersion::getVersion) 117 | .min((d1, d2) -> staticVersionComparator.compare( 118 | versionParser.transform(d1), 119 | versionParser.transform(d2))) 120 | .filter(min -> versionComparator == null || versionComparator.isValid(null, min)) 121 | .orElse(null); 122 | if (minimumVersion == null) { 123 | return TreeVisitor.noop(); 124 | } 125 | 126 | acc.entrySet().removeIf(e -> !e.getValue().getVersion().equals(minimumVersion)); 127 | 128 | return applyMarkersForLocatedGavs(acc, dependenciesInUse); 129 | } 130 | 131 | static TreeVisitor applyMarkersForLocatedGavs(Map acc, DependenciesInUse dependenciesInUse) { 132 | return Preconditions.check(Preconditions.or(new IsBuildGradle<>(), new FindMavenProject().getVisitor()), new TreeVisitor() { 133 | @Override 134 | public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { 135 | if (tree == null) { 136 | return null; 137 | } 138 | Markers m = tree.getMarkers(); 139 | Tree t = tree; 140 | t = m.findFirst(GradleProject.class).map(gradle -> { 141 | Tree t2 = tree; 142 | for (GradleDependencyConfiguration conf : gradle.getConfigurations()) { 143 | List resolved = conf.getResolved(); 144 | t2 = recordMinimumDependencyUse(ctx, gradle.getName(), conf.getName(), resolved, t2); 145 | } 146 | return t2; 147 | }).orElse(t); 148 | 149 | return m.findFirst(MavenResolutionResult.class).map(maven -> { 150 | Tree t2 = tree; 151 | for (Map.Entry> resolved : maven.getDependencies().entrySet()) { 152 | t2 = recordMinimumDependencyUse(ctx, maven.getPom().getArtifactId(), 153 | resolved.getKey().toString().toLowerCase(), resolved.getValue(), t2); 154 | } 155 | return t2; 156 | }).orElse(t); 157 | } 158 | 159 | private Tree recordMinimumDependencyUse(ExecutionContext ctx, String projectName, String scope, List resolved, Tree t) { 160 | Set minimums = new TreeSet<>(); 161 | for (ResolvedDependency dep : resolved) { 162 | for (ResolvedGroupArtifactVersion min : acc.values()) { 163 | if (dep.getGav().equals(min)) { 164 | minimums.add(dep.getGav().toString()); 165 | dependenciesInUse.insertRow(ctx, new DependenciesInUse.Row( 166 | projectName, 167 | t.getMarkers().findFirst(JavaSourceSet.class).map(JavaSourceSet::getName).orElse("unknown"), 168 | dep.getGroupId(), 169 | dep.getArtifactId(), 170 | dep.getVersion(), 171 | dep.getGav().getDatedSnapshotVersion(), 172 | scope, 173 | 1)); 174 | } 175 | } 176 | } 177 | if (!minimums.isEmpty()) { 178 | return SearchResult.found(t, String.join("\n", minimums)); 179 | } 180 | return t; 181 | } 182 | }); 183 | } 184 | 185 | private void collectMinimumVersions(VersionParser versionParser, List resolved, 186 | Map acc) { 187 | StaticVersionComparator versionComparator = new StaticVersionComparator(); 188 | for (ResolvedDependency dep : resolved) { 189 | if (StringUtils.matchesGlob(dep.getGroupId(), groupIdPattern) && 190 | StringUtils.matchesGlob(dep.getArtifactId(), artifactIdPattern)) { 191 | acc.merge(new GroupArtifact(dep.getGroupId(), dep.getArtifactId()), 192 | dep.getGav(), (d1, d2) -> versionComparator.compare( 193 | versionParser.transform(d1.getVersion()), 194 | versionParser.transform(d2.getVersion())) < 0 ? 195 | d1 : d2); 196 | } 197 | } 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/test/java/org/openrewrite/java/dependencies/DependencyResolutionDiagnosticTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies; 17 | 18 | import org.junit.jupiter.api.Test; 19 | import org.openrewrite.DocumentExample; 20 | import org.openrewrite.InMemoryExecutionContext; 21 | import org.openrewrite.Parser; 22 | import org.openrewrite.java.dependencies.table.RepositoryAccessibilityReport; 23 | import org.openrewrite.maven.MavenExecutionContextView; 24 | import org.openrewrite.maven.MavenSettings; 25 | import org.openrewrite.test.RecipeSpec; 26 | import org.openrewrite.test.RewriteTest; 27 | 28 | import java.io.ByteArrayInputStream; 29 | import java.nio.file.Path; 30 | 31 | import static org.assertj.core.api.Assertions.assertThat; 32 | import static org.openrewrite.gradle.Assertions.buildGradle; 33 | import static org.openrewrite.gradle.toolingapi.Assertions.withToolingApi; 34 | import static org.openrewrite.maven.Assertions.pomXml; 35 | 36 | @SuppressWarnings("GroovyAssignabilityCheck") 37 | class DependencyResolutionDiagnosticTest implements RewriteTest { 38 | 39 | @Override 40 | public void defaults(RecipeSpec spec) { 41 | spec.recipe(new DependencyResolutionDiagnostic(null, null, null)); 42 | } 43 | 44 | @DocumentExample 45 | @Test 46 | void gradleNoMarker() { 47 | rewriteRun( 48 | //language=groovy 49 | buildGradle( 50 | """ 51 | plugins { 52 | id("java") 53 | } 54 | """, 55 | """ 56 | /*~~(build.gradle is a Gradle build file, but it is missing a GradleProject marker.)~~>*/plugins { 57 | id("java") 58 | } 59 | """ 60 | ) 61 | ); 62 | } 63 | 64 | @Test 65 | void gradle() { 66 | rewriteRun( 67 | spec -> spec.beforeRecipe(withToolingApi()) 68 | // It is a limitation of the tooling API which prevents configuration-granularity error information from being collected. 69 | // So the GradleDependencyConfigurationErrors table will never be populated in unit tests. 70 | .dataTable(RepositoryAccessibilityReport.Row.class, rows -> { 71 | assertThat(rows) 72 | .hasSize(4) 73 | .contains( 74 | new RepositoryAccessibilityReport.Row("https://repo.maven.apache.org/maven2", "", "", 200, "", "")); 75 | assertThat(rows).filteredOn(row -> row.getUri().startsWith("file:/") && "".equals(row.getPingExceptionMessage())).hasSize(1); 76 | assertThat(rows).contains( 77 | new RepositoryAccessibilityReport.Row("https://plugins.gradle.org/m2", "", "", 200, "", "")); 78 | assertThat(rows) 79 | .filteredOn(row -> "https://nonexistent.moderne.io/maven2".equals(row.getUri()) && row.getPingHttpCode() == null).hasSize(1); 80 | }), 81 | //language=groovy 82 | buildGradle( 83 | """ 84 | plugins { 85 | id("java") 86 | } 87 | repositories { 88 | mavenLocal() 89 | mavenCentral() 90 | maven { 91 | url "https://nonexistent.moderne.io/maven2" 92 | } 93 | } 94 | 95 | dependencies { 96 | implementation("org.openrewrite.nonexistent:nonexistent:0.0.0") 97 | } 98 | """ 99 | ) 100 | ); 101 | } 102 | 103 | 104 | @Test 105 | void gradleNoDefaultRepos() { 106 | rewriteRun( 107 | spec -> spec.beforeRecipe(withToolingApi()) 108 | // It is a limitation of the tooling API which prevents configuration-granularity error information from being collected. 109 | // So the GradleDependencyConfigurationErrors table will never be populated in unit tests. 110 | .dataTable(RepositoryAccessibilityReport.Row.class, rows -> { 111 | assertThat(rows) 112 | .hasSize(2) 113 | .contains( 114 | new RepositoryAccessibilityReport.Row("https://plugins.gradle.org/m2", "", "", 200, "", "")) 115 | .contains( 116 | new RepositoryAccessibilityReport.Row("https://nonexistent.moderne.io/maven2", "java.net.UnknownHostException", "nonexistent.moderne.io", null, "", "")); 117 | }), 118 | //language=groovy 119 | buildGradle( 120 | """ 121 | plugins { 122 | id("java") 123 | } 124 | repositories { 125 | maven { 126 | url "https://nonexistent.moderne.io/maven2" 127 | } 128 | } 129 | 130 | dependencies { 131 | implementation("org.openrewrite.nonexistent:nonexistent:0.0.0") 132 | } 133 | """ 134 | ) 135 | ); 136 | } 137 | 138 | @Test 139 | void mavenSettingsWithMirrors() { 140 | rewriteRun( 141 | spec -> { 142 | MavenExecutionContextView ctx = MavenExecutionContextView.view(new InMemoryExecutionContext()); 143 | MavenSettings settings = MavenSettings.parse(new Parser.Input(Path.of("settings.xml"), () -> new ByteArrayInputStream( 144 | //language=xml 145 | """ 146 | 149 | 150 | 151 | * 152 | mirrored-repo 153 | https://nonexistent.moderne.io/maven2 154 | repo 155 | 156 | 157 | 158 | """.getBytes())), ctx); 159 | ctx.setMavenSettings(settings); 160 | spec.beforeRecipe(withToolingApi()) 161 | .dataTable(RepositoryAccessibilityReport.Row.class, rows -> { 162 | assertThat(rows) 163 | .contains( 164 | new RepositoryAccessibilityReport.Row("https://nonexistent.moderne.io/maven2", "java.net.UnknownHostException", "nonexistent.moderne.io", null, "", "") 165 | ) 166 | .noneMatch(repo -> repo.getUri().contains("https://repo.maven.apache.org/maven2")); 167 | }) 168 | .executionContext(ctx); 169 | }, 170 | //language=xml 171 | pomXml( 172 | """ 173 | 174 | com.example 175 | test 176 | 0.1.0 177 | 178 | """ 179 | ) 180 | ); 181 | } 182 | 183 | @Test 184 | void maven() { 185 | rewriteRun( 186 | spec -> spec.beforeRecipe(withToolingApi()) 187 | .dataTable(RepositoryAccessibilityReport.Row.class, rows -> { 188 | assertThat(rows).contains( 189 | new RepositoryAccessibilityReport.Row("https://repo.maven.apache.org/maven2", "", "", 200, "", "")); 190 | assertThat(rows).filteredOn(row -> row.getUri().startsWith("file:/") && "".equals(row.getPingExceptionMessage())).hasSize(1); 191 | assertThat(rows).contains( 192 | new RepositoryAccessibilityReport.Row("https://nonexistent.moderne.io/maven2", "java.net.UnknownHostException", "nonexistent.moderne.io", null, "", "") 193 | ); 194 | }), 195 | //language=xml 196 | pomXml( 197 | """ 198 | 199 | com.example 200 | test 201 | 0.1.0 202 | 203 | 204 | 205 | nonexistent 206 | https://nonexistent.moderne.io/maven2 207 | 208 | 209 | 210 | """ 211 | ) 212 | ); 213 | } 214 | 215 | @Test 216 | void dependencyNotFound() { 217 | rewriteRun( 218 | spec -> spec.recipe(new DependencyResolutionDiagnostic("org.nonexistent", "nonexistent", "0")) 219 | .beforeRecipe(withToolingApi()) 220 | .dataTable(RepositoryAccessibilityReport.Row.class, rows -> 221 | assertThat(rows).contains( 222 | new RepositoryAccessibilityReport.Row("https://repo.maven.apache.org/maven2", 223 | "", "", 200, "org.openrewrite.maven.MavenDownloadingException", 224 | "org.nonexistent:nonexistent:0 failed. Unable to download POM: org.nonexistent:nonexistent:0. Tried repositories:\nhttps://repo.maven.apache.org/maven2/: HTTP 404") 225 | ) 226 | ), 227 | //language=groovy 228 | buildGradle( 229 | """ 230 | plugins { 231 | id("java") 232 | } 233 | repositories { 234 | mavenCentral() 235 | } 236 | """ 237 | ) 238 | ); 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/main/java/org/openrewrite/java/dependencies/DependencyList.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies; 17 | 18 | import lombok.EqualsAndHashCode; 19 | import lombok.Value; 20 | import org.jspecify.annotations.Nullable; 21 | import org.openrewrite.*; 22 | import org.openrewrite.gradle.marker.GradleDependencyConfiguration; 23 | import org.openrewrite.gradle.marker.GradleProject; 24 | import org.openrewrite.internal.ExceptionUtils; 25 | import org.openrewrite.java.dependencies.table.DependencyListReport; 26 | import org.openrewrite.marker.Markers; 27 | import org.openrewrite.maven.MavenDownloadingException; 28 | import org.openrewrite.maven.MavenExecutionContextView; 29 | import org.openrewrite.maven.MavenSettings; 30 | import org.openrewrite.maven.internal.MavenPomDownloader; 31 | import org.openrewrite.maven.table.MavenMetadataFailures; 32 | import org.openrewrite.maven.tree.*; 33 | 34 | import java.util.HashSet; 35 | import java.util.Optional; 36 | import java.util.Set; 37 | 38 | import static java.util.Collections.emptyMap; 39 | 40 | @EqualsAndHashCode(callSuper = false) 41 | @Value 42 | public class DependencyList extends Recipe { 43 | 44 | transient DependencyListReport report = new DependencyListReport(this); 45 | transient MavenMetadataFailures metadataFailures = new MavenMetadataFailures(this); 46 | 47 | @Option(displayName = "Scope", 48 | description = "The scope of the dependencies to include in the report." + 49 | "Defaults to \"Compile\"", 50 | valid = {"Compile", "Runtime", "TestRuntime"}, 51 | required = false, 52 | example = "Compile") 53 | @Nullable 54 | Scope scope; 55 | 56 | @Option(displayName = "Include transitive dependencies", 57 | description = "Whether or not to include transitive dependencies in the report. " + 58 | "Defaults to including only direct dependencies." + 59 | "Defaults to false.", 60 | required = false, 61 | example = "true") 62 | boolean includeTransitive; 63 | 64 | @Option(displayName = "Validate dependencies are resolvable", 65 | description = "When enabled the recipe will attempt to download every dependency it encounters, reporting on any failures. " + 66 | "This can be useful for identifying dependencies that have become unavailable since an LST was produced." + 67 | "Defaults to false.", 68 | valid = {"true", "false"}, 69 | required = false, 70 | example = "true") 71 | boolean validateResolvable; 72 | 73 | /** 74 | * Freestanding gradle script plugins get assigned the same GradleProject marker with the build script in the project. 75 | * Keep track of the ones which have been seen to minimize duplicate entries in the report. 76 | */ 77 | transient Set seenGradleProjects = new HashSet<>(); 78 | 79 | @Override 80 | public String getDisplayName() { 81 | return "Dependency report"; 82 | } 83 | 84 | @Override 85 | public String getDescription() { 86 | return "Emits a data table detailing all Gradle and Maven dependencies. " + 87 | "This recipe makes no changes to any source file."; 88 | } 89 | 90 | @Override 91 | public TreeVisitor getVisitor() { 92 | return new TreeVisitor() { 93 | @Override 94 | public @Nullable Tree visit(@Nullable Tree tree, ExecutionContext ctx) { 95 | if (tree == null) { 96 | return null; 97 | } 98 | Markers m = tree.getMarkers(); 99 | Set seen = new HashSet<>(); 100 | m.findFirst(GradleProject.class) 101 | .filter(gradle -> seenGradleProjects.add(new GroupArtifactVersion(gradle.getGroup(), gradle.getName(), gradle.getVersion()))) 102 | .ifPresent(gradle -> { 103 | GradleDependencyConfiguration conf = gradle.getConfiguration(scope().asGradleConfigurationName()); 104 | if (conf != null) { 105 | for (ResolvedDependency dep : conf.getResolved()) { 106 | if (dep.getDepth() > 0) { 107 | continue; 108 | } 109 | insertDependency(ctx, gradle, seen, dep, true); 110 | } 111 | } 112 | }); 113 | m.findFirst(MavenResolutionResult.class).ifPresent(maven -> { 114 | for (ResolvedDependency dep : maven.getDependencies().get(scope().asMavenScope())) { 115 | if (dep.getDepth() > 0) { 116 | continue; 117 | } 118 | insertDependency(ctx, maven, seen, dep, true); 119 | } 120 | }); 121 | return tree; 122 | } 123 | }; 124 | } 125 | 126 | private Scope scope() { 127 | return scope == null ? Scope.Compile : scope; 128 | } 129 | 130 | private void insertDependency(ExecutionContext ctx, GradleProject gradle, Set seen, ResolvedDependency dep, boolean direct) { 131 | if (!seen.add(dep.getGav())) { 132 | return; 133 | } 134 | String resolutionFailure = ""; 135 | if (validateResolvable) { 136 | try { 137 | //noinspection DataFlowIssue 138 | metadataFailures.insertRows(ctx, () -> new MavenPomDownloader( 139 | emptyMap(), ctx, 140 | null, 141 | null) 142 | .downloadMetadata(new GroupArtifact(gradle.getGroup(), gradle.getName()), null, gradle.getMavenRepositories())); 143 | } catch (MavenDownloadingException e) { 144 | resolutionFailure = ExceptionUtils.sanitizeStackTrace(e, RecipeScheduler.class); 145 | } 146 | } 147 | report.insertRow(ctx, new DependencyListReport.Row( 148 | "Gradle", 149 | gradle.getGroup(), 150 | gradle.getName(), 151 | gradle.getVersion(), 152 | dep.getGroupId(), 153 | dep.getArtifactId(), 154 | dep.getVersion(), 155 | direct, 156 | resolutionFailure 157 | )); 158 | if (includeTransitive) { 159 | for (ResolvedDependency transitive : dep.getDependencies()) { 160 | insertDependency(ctx, gradle, seen, transitive, false); 161 | } 162 | } 163 | } 164 | 165 | private void insertDependency(ExecutionContext ctx, MavenResolutionResult maven, Set seen, ResolvedDependency dep, boolean direct) { 166 | if (!seen.add(dep.getGav())) { 167 | return; 168 | } 169 | String resolutionFailure = ""; 170 | if (validateResolvable) { 171 | try { 172 | MavenExecutionContextView mctx = MavenExecutionContextView.view(ctx); 173 | metadataFailures.insertRows(ctx, () -> new MavenPomDownloader( 174 | emptyMap(), ctx, 175 | mctx.getSettings() == null ? maven.getMavenSettings() : 176 | maven.getMavenSettings() == null ? mctx.getSettings() : 177 | mctx.getSettings().merge(maven.getMavenSettings()), 178 | Optional.ofNullable(mctx.getSettings()) 179 | .map(MavenSettings::getActiveProfiles) 180 | .map(MavenSettings.ActiveProfiles::getActiveProfiles) 181 | .orElse(maven.getActiveProfiles())) 182 | .downloadMetadata(new GroupArtifact(maven.getPom().getGroupId(), maven.getPom().getArtifactId()), null, maven.getPom().getRepositories())); 183 | } catch (MavenDownloadingException e) { 184 | resolutionFailure = ExceptionUtils.sanitizeStackTrace(e, RecipeScheduler.class); 185 | } 186 | } 187 | report.insertRow(ctx, new DependencyListReport.Row( 188 | "Maven", 189 | maven.getPom().getGroupId(), 190 | maven.getPom().getArtifactId(), 191 | maven.getPom().getVersion(), 192 | dep.getGroupId(), 193 | dep.getArtifactId(), 194 | dep.getVersion(), 195 | direct, 196 | resolutionFailure 197 | )); 198 | if (includeTransitive) { 199 | for (ResolvedDependency transitive : dep.getDependencies()) { 200 | insertDependency(ctx, maven, seen, transitive, false); 201 | } 202 | } 203 | } 204 | 205 | public enum Scope { 206 | Compile, 207 | Runtime, 208 | TestRuntime; 209 | 210 | public org.openrewrite.maven.tree.Scope asMavenScope() { 211 | switch (this) { 212 | case Compile: 213 | return org.openrewrite.maven.tree.Scope.Compile; 214 | case Runtime: 215 | return org.openrewrite.maven.tree.Scope.Runtime; 216 | case TestRuntime: 217 | return org.openrewrite.maven.tree.Scope.Test; 218 | default: 219 | throw new IllegalStateException("Unexpected value: " + this); 220 | } 221 | } 222 | 223 | public String asGradleConfigurationName() { 224 | switch (this) { 225 | case Compile: 226 | return "compileClasspath"; 227 | case Runtime: 228 | return "runtimeClasspath"; 229 | case TestRuntime: 230 | return "testRuntimeClasspath"; 231 | default: 232 | throw new IllegalStateException("Unexpected value: " + this); 233 | } 234 | } 235 | } 236 | } 237 | -------------------------------------------------------------------------------- /src/test/java/org/openrewrite/java/dependencies/ChangeDependencyTest.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2023 the original author or authors. 3 | *

4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | *

8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | *

10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package org.openrewrite.java.dependencies; 17 | 18 | import org.junit.jupiter.api.Test; 19 | import org.openrewrite.DocumentExample; 20 | import org.openrewrite.InMemoryExecutionContext; 21 | import org.openrewrite.Validated; 22 | import org.openrewrite.test.RewriteTest; 23 | 24 | import static org.assertj.core.api.Assertions.assertThat; 25 | import static org.openrewrite.gradle.Assertions.buildGradle; 26 | import static org.openrewrite.gradle.toolingapi.Assertions.withToolingApi; 27 | import static org.openrewrite.maven.Assertions.pomXml; 28 | 29 | class ChangeDependencyTest implements RewriteTest { 30 | 31 | @DocumentExample("Change Gradle dependency") 32 | @Test 33 | void changeGradleDependency() { 34 | rewriteRun( 35 | spec -> spec.beforeRecipe(withToolingApi()) 36 | .recipe(new ChangeDependency("commons-lang", "commons-lang", "org.apache.commons", "commons-lang3", "3.11.x", null, null, null)), 37 | //language=groovy 38 | buildGradle( 39 | """ 40 | plugins { 41 | id "java-library" 42 | } 43 | 44 | repositories { 45 | mavenCentral() 46 | } 47 | 48 | dependencies { 49 | implementation "commons-lang:commons-lang:2.6" 50 | } 51 | """, 52 | """ 53 | plugins { 54 | id "java-library" 55 | } 56 | 57 | repositories { 58 | mavenCentral() 59 | } 60 | 61 | dependencies { 62 | implementation "org.apache.commons:commons-lang3:3.11" 63 | } 64 | """ 65 | ) 66 | ); 67 | } 68 | 69 | @Test 70 | void changeMavenDependency() { 71 | rewriteRun( 72 | spec -> spec.recipe(new ChangeDependency("commons-lang", "commons-lang", "org.apache.commons", "commons-lang3", "3.11.x", null, null, null)), 73 | //language=xml 74 | pomXml( 75 | """ 76 | 77 | com.example.app 78 | my-app 79 | 1 80 | 81 | 82 | commons-lang 83 | commons-lang 84 | 2.6 85 | 86 | 87 | 88 | """, 89 | """ 90 | 91 | com.example.app 92 | my-app 93 | 1 94 | 95 | 96 | org.apache.commons 97 | commons-lang3 98 | 3.11 99 | 100 | 101 | 102 | """ 103 | ) 104 | ); 105 | } 106 | 107 | @Test 108 | void changeMavenDependencyToAlreadyPresent() { 109 | rewriteRun( 110 | spec -> spec.recipe(new ChangeDependency("commons-lang", "commons-lang", "org.apache.commons", "commons-lang3", "3.11.x", null, null, null)), 111 | //language=xml 112 | pomXml( 113 | """ 114 | 115 | com.example.app 116 | my-app 117 | 1 118 | 119 | 120 | org.apache.commons 121 | commons-lang3 122 | 3.11 123 | 124 | 125 | commons-lang 126 | commons-lang 127 | 2.6 128 | 129 | 130 | 131 | """, 132 | """ 133 | 134 | com.example.app 135 | my-app 136 | 1 137 | 138 | 139 | org.apache.commons 140 | commons-lang3 141 | 3.11 142 | 143 | 144 | 145 | """ 146 | ) 147 | ); 148 | } 149 | 150 | @Test 151 | void changeMavenDependencyManagementPomImport() { 152 | rewriteRun( 153 | spec -> spec.recipe(new ChangeDependency( 154 | "org.springframework.boot", "spring-boot-dependencies", 155 | "io.micronaut.platform", "micronaut-parent", 156 | "4.3.4", null, null, null 157 | )), 158 | //language=xml 159 | pomXml( 160 | """ 161 | 162 | 4.0.0 163 | com.mycompany.app 164 | my-app 165 | 1 166 | 167 | 168 | 169 | org.springframework.boot 170 | spring-boot-dependencies 171 | 2.5.0 172 | pom 173 | import 174 | 175 | 176 | 177 | 178 | """, 179 | """ 180 | 181 | 4.0.0 182 | com.mycompany.app 183 | my-app 184 | 1 185 | 186 | 187 | 188 | io.micronaut.platform 189 | micronaut-parent 190 | 4.3.4 191 | pom 192 | import 193 | 194 | 195 | 196 | 197 | """ 198 | ) 199 | ); 200 | } 201 | 202 | @Test 203 | void validateCascadesToRecipes() { 204 | Validated validate = new ChangeDependency( 205 | "org.springframework.boot", "spring-boot-dependencies", 206 | "org.springframework.boot", "spring-boot-dependencies", 207 | "3.2.2", null, null, null 208 | ).validate(new InMemoryExecutionContext()); 209 | assertThat(validate.failures()) 210 | .hasSize(2) 211 | .allMatch(failure -> failure.getMessage().contains("must be different")); 212 | } 213 | 214 | @Test 215 | void doNotPinWhenNotVersionedGradle() { 216 | rewriteRun( 217 | spec -> spec 218 | .beforeRecipe(withToolingApi()) 219 | .recipe(new ChangeDependency("mysql", "mysql-connector-java", "com.mysql", "mysql-connector-j", "8.0.x", null, null, null)), 220 | //language=groovy 221 | buildGradle( 222 | """ 223 | plugins { 224 | id 'java' 225 | id 'org.springframework.boot' version '2.6.1' 226 | id 'io.spring.dependency-management' version '1.0.11.RELEASE' 227 | } 228 | 229 | repositories { 230 | mavenCentral() 231 | } 232 | 233 | dependencies { 234 | runtimeOnly 'mysql:mysql-connector-java' 235 | } 236 | """, 237 | """ 238 | plugins { 239 | id 'java' 240 | id 'org.springframework.boot' version '2.6.1' 241 | id 'io.spring.dependency-management' version '1.0.11.RELEASE' 242 | } 243 | 244 | repositories { 245 | mavenCentral() 246 | } 247 | 248 | dependencies { 249 | runtimeOnly 'com.mysql:mysql-connector-j' 250 | } 251 | """ 252 | ) 253 | ); 254 | } 255 | 256 | @Test 257 | void pinWhenOverrideManagedVersionGradle() { 258 | rewriteRun( 259 | spec -> spec 260 | .beforeRecipe(withToolingApi()) 261 | .recipe(new ChangeDependency("mysql", "mysql-connector-java", "com.mysql", "mysql-connector-j", "8.0.x", null, true, null)), 262 | //language=groovy 263 | buildGradle( 264 | """ 265 | plugins { 266 | id 'java' 267 | id 'org.springframework.boot' version '2.6.1' 268 | id 'io.spring.dependency-management' version '1.0.11.RELEASE' 269 | } 270 | 271 | repositories { 272 | mavenCentral() 273 | } 274 | 275 | dependencies { 276 | runtimeOnly 'mysql:mysql-connector-java' 277 | } 278 | """, 279 | """ 280 | plugins { 281 | id 'java' 282 | id 'org.springframework.boot' version '2.6.1' 283 | id 'io.spring.dependency-management' version '1.0.11.RELEASE' 284 | } 285 | 286 | repositories { 287 | mavenCentral() 288 | } 289 | 290 | dependencies { 291 | runtimeOnly 'com.mysql:mysql-connector-j:8.0.33' 292 | } 293 | """ 294 | ) 295 | ); 296 | } 297 | } 298 | --------------------------------------------------------------------------------