├── .editorconfig ├── .github ├── dependabot.yml └── workflows │ ├── ci.yml │ ├── dependency-submission.yml │ ├── integration-tests.yml │ ├── publish.yml │ ├── repository-backup.yml │ └── stale.yml ├── .gitignore ├── LICENSE └── apache-license-v2.txt ├── README.md ├── build.gradle.kts ├── buildSrc └── src │ └── main │ └── groovy │ └── org │ └── gradle │ └── rewrite │ └── build │ ├── GradleVersionData.groovy │ └── GradleVersionsCommandLineArgumentProvider.groovy ├── gradle.properties ├── gradle ├── licenseHeader.txt └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── plugin ├── build.gradle.kts └── src │ ├── main │ ├── java │ │ └── org │ │ │ └── openrewrite │ │ │ └── gradle │ │ │ ├── AbstractRewriteTask.java │ │ │ ├── DelegatingProjectParser.java │ │ │ ├── GradleProjectParser.java │ │ │ ├── RewriteClassLoader.java │ │ │ ├── RewriteDiscoverTask.java │ │ │ ├── RewriteDryRunTask.java │ │ │ ├── RewriteExtension.java │ │ │ ├── RewritePlugin.java │ │ │ ├── RewriteRunTask.java │ │ │ ├── SanitizedMarkerPrinter.java │ │ │ ├── isolated │ │ │ ├── AndroidProjectCompileOptions.java │ │ │ ├── AndroidProjectParser.java │ │ │ ├── AndroidProjectVariant.java │ │ │ ├── DefaultProjectParser.java │ │ │ ├── ResultsContainer.java │ │ │ └── package-info.java │ │ │ └── package-info.java │ └── kotlin │ │ └── org │ │ └── openrewrite │ │ └── gradle │ │ └── GradleProjectSpec.kt │ └── test │ ├── kotlin │ └── org │ │ └── openrewrite │ │ └── gradle │ │ ├── GradleRunnerTest.kt │ │ ├── RewriteDiscoverTest.kt │ │ ├── RewriteDryRunSourceSetTest.kt │ │ ├── RewriteDryRunTest.kt │ │ ├── RewritePluginTest.kt │ │ ├── RewriteRunTest.kt │ │ └── fixtures │ │ ├── GradleFixtures.kt │ │ └── JavaFixtures.kt │ └── samples │ └── resourceParserTest │ └── root │ └── project │ ├── file2.txt │ └── subproject │ └── file.txt ├── scripts └── spring-petclinic-test.init.gradle ├── settings.gradle.kts └── suppressions.xml /.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/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/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 | env: 22 | GRADLE_SWITCHES: --console=plain --info --stacktrace --warning-mode=all --no-daemon 23 | GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} 24 | 25 | jobs: 26 | build: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - uses: actions/checkout@v4 30 | with: 31 | fetch-depth: 0 32 | - uses: actions/setup-java@v4 33 | with: 34 | distribution: temurin 35 | java-version: 11 36 | - name: build 37 | uses: gradle/actions/setup-gradle@v3 38 | with: 39 | arguments: ${{ env.GRADLE_SWITCHES }} build 40 | - name: publish-snapshots 41 | if: github.event_name != 'pull_request' 42 | uses: gradle/actions/setup-gradle@v3 43 | with: 44 | arguments: ${{ env.GRADLE_SWITCHES }} snapshot :plugin:publishPluginMavenPublicationToSonatypeRepository -x test -x publishPlugins 45 | env: 46 | ORG_GRADLE_PROJECT_sonatypeUsername: ${{ secrets.OSSRH_USERNAME }} 47 | ORG_GRADLE_PROJECT_sonatypePassword: ${{ secrets.OSSRH_TOKEN }} 48 | ORG_GRADLE_PROJECT_signingKey: ${{ secrets.OSSRH_SIGNING_KEY }} 49 | ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.OSSRH_SIGNING_PASSWORD }} 50 | -------------------------------------------------------------------------------- /.github/workflows/dependency-submission.yml: -------------------------------------------------------------------------------- 1 | name: Dependency Submission 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["publish"] 6 | types: 7 | - completed 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | dependency-submission: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout sources 17 | uses: actions/checkout@v4 18 | - name: Generate and submit dependency graph 19 | uses: gradle/actions/dependency-submission@v3 20 | env: 21 | # Exclude all dependencies that originate solely in the 'buildSrc' project 22 | DEPENDENCY_GRAPH_EXCLUDE_PROJECTS: ':buildSrc' 23 | # Exclude dependencies that are only resolved in test classpaths 24 | DEPENDENCY_GRAPH_EXCLUDE_CONFIGURATIONS: '.*[Tt]est(Compile|Runtime)Classpath' 25 | -------------------------------------------------------------------------------- /.github/workflows/integration-tests.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: integration-tests 3 | 4 | on: 5 | workflow_dispatch: {} 6 | 7 | env: 8 | GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} 9 | 10 | jobs: 11 | integration-tests: 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | java: ["8"] 16 | os: ["ubuntu-latest"] 17 | runs-on: ${{ matrix.os }} 18 | name: itest-${{ matrix.java }} 19 | steps: 20 | - name: checkout-openrewrite/spring-petclinic-migration 21 | uses: actions/checkout@v4 22 | with: 23 | repository: openrewrite/spring-petclinic-migration 24 | - name: setup-plugin-checkout-source 25 | uses: actions/checkout@v4 26 | with: 27 | path: rewrite-gradle-plugin 28 | - name: setup-plugin-setup-java-11 29 | uses: actions/setup-java@v4 30 | with: 31 | distribution: temurin 32 | java-version: 11 33 | - name: setup-plugin-pTML 34 | run: ./rewrite-gradle-plugin/gradlew --project-dir rewrite-gradle-plugin pTML 35 | - name: setup-java-${{ matrix.java }} 36 | uses: actions/setup-java@v4 37 | with: 38 | distribution: temurin 39 | java-version: ${{ matrix.java }} 40 | - name: run-validateRewrite 41 | uses: gradle/actions/setup-gradle@v3 42 | with: 43 | arguments: | 44 | --info 45 | --stacktrace 46 | --no-daemon 47 | --init-script /home/runner/work/rewrite-gradle-plugin/rewrite-gradle-plugin/rewrite-gradle-plugin/scripts/spring-petclinic-test.init.gradle 48 | validateRewrite 49 | gradle-version: current 50 | -------------------------------------------------------------------------------- /.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 | env: 11 | GRADLE_ENTERPRISE_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY }} 12 | 13 | jobs: 14 | release: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | - uses: actions/setup-java@v4 21 | with: 22 | distribution: temurin 23 | java-version: 11 24 | 25 | - name: publish-candidate 26 | if: contains(github.ref, '-rc.') 27 | uses: gradle/actions/setup-gradle@v3 28 | with: 29 | arguments: | 30 | -Preleasing 31 | -Prelease.disableGitChecks=true 32 | -Prelease.useLastTag=true 33 | -Dgradle.publish.key=${{ secrets.GRADLE_PUBLISH_KEY }} 34 | -Dgradle.publish.secret=${{ secrets.GRADLE_PUBLISH_SECRET }} 35 | candidate 36 | publishPlugins 37 | 38 | - name: publish-release 39 | if: (!contains(github.ref, '-rc.')) 40 | uses: gradle/actions/setup-gradle@v3 41 | with: 42 | arguments: | 43 | -Preleasing 44 | -Prelease.disableGitChecks=true 45 | -Prelease.useLastTag=true 46 | -Dgradle.publish.key=${{ secrets.GRADLE_PUBLISH_KEY }} 47 | -Dgradle.publish.secret=${{ secrets.GRADLE_PUBLISH_SECRET }} 48 | final 49 | publishPlugins 50 | -------------------------------------------------------------------------------- /.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/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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | build/ 3 | out/ 4 | !*/src/**/build 5 | .gradle/ 6 | .idea/ 7 | -------------------------------------------------------------------------------- /LICENSE/apache-license-v2.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

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

10 | 11 |
12 |

rewrite-gradle-plugin

13 |
14 | 15 |
16 | 17 | 18 | [![ci](https://github.com/openrewrite/rewrite-gradle-plugin/actions/workflows/ci.yml/badge.svg)](https://github.com/openrewrite/rewrite-gradle-plugin/actions/workflows/ci.yml) 19 | [![Gradle Plugin Portal](https://img.shields.io/maven-metadata/v/https/plugins.gradle.org/m2/org.openrewrite/plugin/maven-metadata.xml.svg?label=gradlePluginPortal)](https://plugins.gradle.org/plugin/org.openrewrite.rewrite) 20 | [![Revved up by Develocity](https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A)](https://ge.openrewrite.org/scans) 21 | [![Contributing Guide](https://img.shields.io/badge/Contributing-Guide-informational)](https://github.com/openrewrite/.github/blob/main/CONTRIBUTING.md) 22 |
23 | 24 | ## What is this? 25 | 26 | This project provides a Gradle plugin that applies [Rewrite](https://github.com/openrewrite/rewrite) checking and fixing tasks as build tasks, one of several possible workflows for propagating change across an organization's source code. 27 | 28 | ```groovy 29 | plugins { 30 | id("java") 31 | id("org.openrewrite.rewrite").version("latest_version_here") 32 | } 33 | 34 | rewrite { 35 | // Reformats Java Code 36 | activeRecipe("org.openrewrite.java.format.AutoFormat") 37 | } 38 | ``` 39 | 40 | ### Consuming latest snapshots from OSSRH 41 | 42 | To use the latest `-SNAPSHOT` of the `rewrite-gradle-plugin`, update your project's `settings.gradle.kts`: 43 | 44 | ```kts 45 | pluginManagement { 46 | resolutionStrategy { 47 | eachPlugin { 48 | if (requested.id.namespace == "org.openrewrite") { 49 | useModule("org.openrewrite:plugin:${requested.version}") 50 | } 51 | } 52 | } 53 | 54 | repositories { 55 | // ... 56 | maven { 57 | url = uri("https://oss.sonatype.org/content/repositories/snapshots/") 58 | } 59 | // ... 60 | // you'll likely also need this if you don't have a pluginManagement section already: 61 | gradlePluginPortal() 62 | // ... 63 | } 64 | } 65 | ``` 66 | 67 | The plugin can be consumed in your `build.gradle.kts`: 68 | 69 | ```kts 70 | plugins { 71 | id("org.openrewrite.rewrite") version "X.Y.Z-SNAPSHOT" 72 | // or resolved dynamically to absolute latest: 73 | id("org.openrewrite.rewrite") version "latest.integration" 74 | } 75 | ``` 76 | 77 | ## Contributing 78 | 79 | 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. 80 | 81 | ## Documentation 82 | 83 | - [OpenRewrite Quickstart Guide](https://docs.openrewrite.org/running-recipes/getting-started) 84 | - [Gradle Plugin Reference](https://docs.openrewrite.org/reference/gradle-plugin-configuration) 85 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("nebula.release") version "17.1.0" 3 | id("io.github.gradle-nexus.publish-plugin") version "1.1.0" 4 | id("org.owasp.dependencycheck") version "latest.release" apply false 5 | id("nebula.maven-resolved-dependencies") version "18.4.0" apply false 6 | id("nebula.maven-apache-license") version "18.4.0" apply false 7 | } 8 | 9 | configure { 10 | defaultVersionStrategy = nebula.plugin.release.NetflixOssStrategies.SNAPSHOT(project) 11 | } 12 | 13 | allprojects { 14 | apply(plugin = "org.owasp.dependencycheck") 15 | apply(plugin = "nebula.maven-resolved-dependencies") 16 | 17 | group = "org.openrewrite" 18 | description = "Eliminate Tech-Debt. At build time." 19 | 20 | configure { 21 | analyzers.assemblyEnabled = false 22 | failBuildOnCVSS = 9.0F 23 | suppressionFile = "suppressions.xml" 24 | nvd.apiKey = System.getenv("NVD_API_KEY") 25 | } 26 | 27 | dependencies{ 28 | modules { 29 | module("com.google.guava:listenablefuture") { 30 | replacedBy("com.google.guava:guava", "listenablefuture is part of guava") 31 | } 32 | module("com.google.collections:google-collections") { 33 | replacedBy("com.google.guava:guava", "google-collections is part of guava") 34 | } 35 | } 36 | } 37 | } 38 | 39 | nexusPublishing { 40 | repositories { 41 | sonatype() 42 | } 43 | } 44 | 45 | evaluationDependsOn(":plugin") 46 | -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/org/gradle/rewrite/build/GradleVersionData.groovy: -------------------------------------------------------------------------------- 1 | package org.gradle.rewrite.build 2 | 3 | import groovy.json.JsonSlurper 4 | import org.gradle.util.VersionNumber 5 | 6 | class GradleVersionData { 7 | 8 | static List getNightlyVersions() { 9 | def releaseNightly = getLatestReleaseNightly() 10 | releaseNightly ? [releaseNightly] + getLatestNightly() : [getLatestNightly()] 11 | } 12 | 13 | private static String getLatestNightly() { 14 | new JsonSlurper().parse(new URL("https://services.gradle.org/versions/nightly")).version 15 | } 16 | 17 | private static String getLatestReleaseNightly() { 18 | new JsonSlurper().parse(new URL("https://services.gradle.org/versions/release-nightly")).version 19 | } 20 | 21 | static List getReleasedVersions() { 22 | new JsonSlurper().parse(new URL("https://services.gradle.org/versions/all")) 23 | .findAll { !it.nightly && !it.snapshot } // filter out snapshots and nightlies 24 | .findAll { !it.rcFor || it.activeRc } // filter out inactive rcs 25 | .findAll { !it.milestoneFor } // filter out milestones 26 | . collectEntries { [(it.version): VersionNumber.parse(it.version as String)] } 27 | .findAll { it.value.major >= 5 } // only 5.0 and above 28 | .inject([] as List>) { releasesToTest, version -> // only test against latest patch versions 29 | if (!releasesToTest.any { it.value.major == version.value.major && it.value.minor == version.value.minor }) { 30 | releasesToTest + version 31 | } else { 32 | releasesToTest 33 | } 34 | } 35 | .collect { it.key.toString() } 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /buildSrc/src/main/groovy/org/gradle/rewrite/build/GradleVersionsCommandLineArgumentProvider.groovy: -------------------------------------------------------------------------------- 1 | package org.gradle.rewrite.build 2 | 3 | import groovy.transform.CompileStatic 4 | import org.gradle.api.tasks.Input 5 | import org.gradle.process.CommandLineArgumentProvider 6 | 7 | import java.util.function.Supplier 8 | 9 | @CompileStatic 10 | class GradleVersionsCommandLineArgumentProvider implements CommandLineArgumentProvider { 11 | 12 | public static final String PROPERTY_NAME = "org.gradle.test.gradleVersions" 13 | 14 | final Supplier> versions 15 | 16 | GradleVersionsCommandLineArgumentProvider(Supplier> versions) { 17 | this.versions = { versions.get() }.memoize() as Supplier> 18 | } 19 | 20 | @Input 21 | List getVersions() { 22 | versions.get() 23 | } 24 | 25 | @Override 26 | Iterable asArguments() { 27 | ["-D${PROPERTY_NAME}=${getVersions().join("|")}".toString()] 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # 2 | -------------------------------------------------------------------------------- /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. 14 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/openrewrite/rewrite-gradle-plugin/e973f1ccc7d87e60c08f32c82be052ba26e11167/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip 4 | distributionSha256Sum=7197a12f450794931532469d4ff21a59ea2c1cd59a3ec3f89c035c3c420a6999 5 | networkTimeout=10000 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 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 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | org.gradle.wrapper.GradleWrapperMain \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /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 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /plugin/build.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | import nl.javadude.gradle.plugins.license.LicenseExtension 4 | import org.gradle.rewrite.build.GradleVersionData 5 | import org.gradle.rewrite.build.GradleVersionsCommandLineArgumentProvider 6 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 7 | import java.util.* 8 | 9 | plugins { 10 | id("org.jetbrains.kotlin.jvm") version "1.9.0" 11 | id("com.gradle.plugin-publish") version "1.1.0" 12 | id("com.github.hierynomus.license") version "0.16.1" 13 | id("nebula.maven-apache-license") 14 | } 15 | 16 | gradlePlugin { 17 | website.set("https://github.com/openrewrite/rewrite-gradle-plugin") 18 | vcsUrl.set("https://github.com/openrewrite/rewrite-gradle-plugin.git") 19 | 20 | plugins { 21 | create("rewrite") { 22 | id = "org.openrewrite.rewrite" 23 | displayName = "Rewrite" 24 | description = "Automatically eliminate technical debt" 25 | implementationClass = "org.openrewrite.gradle.RewritePlugin" 26 | tags.set(listOf("rewrite", "refactoring", "remediation", "security", "migration", "java", "checkstyle")) 27 | } 28 | } 29 | } 30 | 31 | repositories { 32 | if (!project.hasProperty("releasing")) { 33 | mavenLocal { 34 | mavenContent { 35 | excludeVersionByRegex(".+", ".+", ".+-rc-?[0-9]*") 36 | } 37 | } 38 | 39 | maven { 40 | url = uri("https://oss.sonatype.org/content/repositories/snapshots") 41 | } 42 | } 43 | 44 | mavenCentral { 45 | mavenContent { 46 | excludeVersionByRegex(".+", ".+", ".+-rc-?[0-9]*") 47 | } 48 | } 49 | gradlePluginPortal() 50 | google() 51 | } 52 | 53 | val latest = if (project.hasProperty("releasing")) { 54 | "latest.release" 55 | } else { 56 | "latest.integration" 57 | } 58 | 59 | configurations.all { 60 | resolutionStrategy { 61 | cacheChangingModulesFor(0, TimeUnit.SECONDS) 62 | cacheDynamicVersionsFor(0, TimeUnit.SECONDS) 63 | if (name.startsWith("test")) { 64 | eachDependency { 65 | if (requested.name == "groovy-xml") { 66 | useVersion("3.0.9") 67 | } 68 | } 69 | } 70 | } 71 | } 72 | 73 | java { 74 | toolchain { 75 | languageVersion.set(JavaLanguageVersion.of(17)) 76 | } 77 | } 78 | 79 | tasks.withType().configureEach { 80 | options.release.set(8) 81 | options.encoding = "UTF-8" 82 | options.compilerArgs.add("-parameters") 83 | options.isFork = true 84 | } 85 | 86 | tasks.withType().configureEach { 87 | compilerOptions { 88 | jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8) 89 | } 90 | } 91 | 92 | val rewriteDependencies = configurations.create("rewriteDependencies") 93 | configurations.named("compileOnly").configure { 94 | extendsFrom(rewriteDependencies) 95 | } 96 | 97 | dependencies { 98 | "rewriteDependencies"(platform("org.openrewrite:rewrite-bom:$latest")) 99 | "rewriteDependencies"("org.openrewrite:rewrite-core") 100 | "rewriteDependencies"("org.openrewrite:rewrite-hcl") 101 | "rewriteDependencies"("org.openrewrite:rewrite-java") 102 | "rewriteDependencies"("org.openrewrite:rewrite-java-21") 103 | "rewriteDependencies"("org.openrewrite:rewrite-java-17") 104 | "rewriteDependencies"("org.openrewrite:rewrite-java-11") 105 | "rewriteDependencies"("org.openrewrite:rewrite-java-8") 106 | "rewriteDependencies"("org.openrewrite:rewrite-json") 107 | "rewriteDependencies"("org.openrewrite:rewrite-kotlin") 108 | "rewriteDependencies"("org.openrewrite:rewrite-xml") 109 | "rewriteDependencies"("org.openrewrite:rewrite-yaml") 110 | "rewriteDependencies"("org.openrewrite:rewrite-properties") 111 | "rewriteDependencies"("org.openrewrite:rewrite-protobuf") 112 | "rewriteDependencies"("org.openrewrite:rewrite-groovy") 113 | "rewriteDependencies"("org.openrewrite:rewrite-gradle") 114 | "rewriteDependencies"("org.openrewrite:rewrite-polyglot:$latest") 115 | "rewriteDependencies"("org.openrewrite.gradle.tooling:model:$latest") 116 | "rewriteDependencies"("org.openrewrite:rewrite-maven") 117 | "rewriteDependencies"("com.fasterxml.jackson.module:jackson-module-kotlin:2.17.2") 118 | "rewriteDependencies"("com.google.guava:guava:latest.release") 119 | implementation(platform("org.openrewrite:rewrite-bom:$latest")) 120 | compileOnly("com.android.tools.build:gradle:7.0.4") 121 | compileOnly("org.jetbrains.kotlin:kotlin-gradle-plugin:latest.release") 122 | compileOnly("com.google.guava:guava:latest.release") 123 | 124 | testImplementation(platform("org.junit:junit-bom:latest.release")) 125 | testImplementation("org.junit.jupiter:junit-jupiter-api") 126 | testImplementation("org.junit.jupiter:junit-jupiter-params") 127 | testImplementation("org.openrewrite.tools:jgit:latest.release") 128 | testImplementation("org.openrewrite:rewrite-test") 129 | testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine") 130 | testImplementation("org.assertj:assertj-core:3.+") 131 | 132 | modules { 133 | module("com.google.guava:listenablefuture") { 134 | replacedBy("com.google.guava:guava", "listenablefuture is part of guava") 135 | } 136 | module("com.google.collections:google-collections") { 137 | replacedBy("com.google.guava:guava", "google-collections is part of guava") 138 | } 139 | } 140 | } 141 | 142 | project.rootProject.tasks.getByName("postRelease").dependsOn(project.tasks.getByName("publishPlugins")) 143 | 144 | tasks.register("testGradleReleases") { 145 | jvmArgumentProviders.add(GradleVersionsCommandLineArgumentProvider(GradleVersionData::getReleasedVersions)) 146 | } 147 | 148 | tasks.register("testGradleNightlies") { 149 | jvmArgumentProviders.add(GradleVersionsCommandLineArgumentProvider(GradleVersionData::getNightlyVersions)) 150 | } 151 | 152 | tasks.withType { 153 | useJUnitPlatform() 154 | // Remove this once we've fixed https://github.com/openrewrite/rewrite-gradle-plugin/issues/132 155 | setForkEvery(1) 156 | } 157 | 158 | val gVP = tasks.register("generateVersionsProperties") { 159 | val outputFile = file("${project.layout.buildDirectory.get().asFile}/rewrite/versions.properties") 160 | description = "Creates a versions.properties in $outputFile" 161 | group = "Build" 162 | 163 | inputs.files(rewriteDependencies) 164 | inputs.property("releasing", project.hasProperty("releasing")) 165 | 166 | outputs.file(outputFile) 167 | 168 | doLast { 169 | if (outputFile.exists()) { 170 | outputFile.delete() 171 | } else { 172 | outputFile.parentFile.mkdirs() 173 | } 174 | val resolvedModules = rewriteDependencies.resolvedConfiguration.firstLevelModuleDependencies 175 | val props = Properties() 176 | for (module in resolvedModules) { 177 | props["${module.moduleGroup}:${module.moduleName}"] = module.moduleVersion 178 | } 179 | outputFile.outputStream().use { 180 | props.store(it, null) 181 | } 182 | } 183 | } 184 | 185 | tasks.named("processResources") { 186 | into("rewrite/") { 187 | from(gVP) 188 | } 189 | } 190 | 191 | tasks.named("test") { 192 | systemProperty( 193 | "org.openrewrite.test.gradleVersion", project.findProperty("testedGradleVersion") ?: gradle.gradleVersion 194 | ) 195 | } 196 | 197 | val testGradle4 = tasks.register("testGradle4") { 198 | systemProperty("org.openrewrite.test.gradleVersion", "4.10") 199 | systemProperty("jarLocationForTest", tasks.named("jar").get().archiveFile.get().asFile.absolutePath) 200 | // Gradle 4 predates support for Java 11 201 | javaLauncher.set(javaToolchains.launcherFor { 202 | languageVersion.set(JavaLanguageVersion.of(8)) 203 | }) 204 | } 205 | tasks.named("check").configure { 206 | dependsOn(testGradle4) 207 | } 208 | 209 | configure { 210 | ext.set("year", Calendar.getInstance().get(Calendar.YEAR)) 211 | skipExistingHeaders = true 212 | header = project.rootProject.file("gradle/licenseHeader.txt") 213 | mapping(mapOf("kt" to "SLASHSTAR_STYLE", "java" to "SLASHSTAR_STYLE")) 214 | strictCheck = true 215 | exclude("**/versions.properties") 216 | exclude("**/*.txt") 217 | } 218 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/openrewrite/gradle/AbstractRewriteTask.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.gradle; 17 | 18 | import org.gradle.api.DefaultTask; 19 | import org.gradle.api.file.ProjectLayout; 20 | import org.gradle.api.provider.Provider; 21 | import org.gradle.api.tasks.Input; 22 | import org.gradle.api.tasks.Internal; 23 | import org.gradle.api.tasks.options.Option; 24 | import org.gradle.util.GradleVersion; 25 | 26 | import javax.inject.Inject; 27 | import java.io.File; 28 | import java.nio.file.Path; 29 | import java.util.Collections; 30 | import java.util.List; 31 | import java.util.Set; 32 | import java.util.stream.Collectors; 33 | 34 | public abstract class AbstractRewriteTask extends DefaultTask { 35 | protected Provider> resolvedDependencies; 36 | protected boolean dumpGcActivity; 37 | protected GradleProjectParser gpp; 38 | protected RewriteExtension extension; 39 | 40 | protected AbstractRewriteTask() { 41 | if (GradleVersion.current().compareTo(GradleVersion.version("7.4")) >= 0) { 42 | notCompatibleWithConfigurationCache("org.openrewrite.rewrite needs to parse the whole project"); 43 | } 44 | } 45 | 46 | public T setExtension(RewriteExtension extension) { 47 | this.extension = extension; 48 | //noinspection unchecked 49 | return (T) this; 50 | } 51 | 52 | public T setResolvedDependencies(Provider> resolvedDependencies) { 53 | this.resolvedDependencies = resolvedDependencies; 54 | //noinspection unchecked 55 | return (T) this; 56 | } 57 | 58 | @Option(description = "Dump GC activity related to parsing.", option = "dumpGcActivity") 59 | public void setDumpGcActivity(boolean dumpGcActivity) { 60 | this.dumpGcActivity = dumpGcActivity; 61 | } 62 | 63 | @Input 64 | public boolean isDumpGcActivity() { 65 | return dumpGcActivity; 66 | } 67 | 68 | @Inject 69 | public ProjectLayout getProjectLayout() { 70 | throw new AssertionError("unexpected; getProjectLayout() should be overridden by Gradle"); 71 | } 72 | 73 | @Internal 74 | protected T getProjectParser() { 75 | if (gpp == null) { 76 | if (extension == null) { 77 | throw new IllegalArgumentException("Must configure extension"); 78 | } 79 | if (resolvedDependencies == null) { 80 | throw new IllegalArgumentException("Must configure resolvedDependencies"); 81 | } 82 | Set deps = resolvedDependencies.getOrNull(); 83 | if (deps == null) { 84 | deps = Collections.emptySet(); 85 | } 86 | Set classpath = deps.stream() 87 | .map(File::toPath) 88 | .collect(Collectors.toSet()); 89 | gpp = new DelegatingProjectParser(getProject(), extension, classpath); 90 | } 91 | //noinspection unchecked 92 | return (T) gpp; 93 | } 94 | 95 | @Input 96 | public List getActiveRecipes() { 97 | return getProjectParser().getActiveRecipes(); 98 | } 99 | 100 | @Input 101 | public List getActiveStyles() { 102 | return getProjectParser().getActiveStyles(); 103 | } 104 | 105 | protected void shutdownRewrite() { 106 | getProjectParser().shutdownRewrite(); 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/openrewrite/gradle/DelegatingProjectParser.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.gradle; 17 | 18 | import org.gradle.api.Project; 19 | import org.gradle.internal.service.ServiceRegistry; 20 | import org.jspecify.annotations.Nullable; 21 | 22 | import java.lang.reflect.InvocationTargetException; 23 | import java.net.MalformedURLException; 24 | import java.net.URI; 25 | import java.net.URL; 26 | import java.nio.file.Path; 27 | import java.nio.file.Paths; 28 | import java.util.Arrays; 29 | import java.util.Collection; 30 | import java.util.List; 31 | import java.util.Set; 32 | import java.util.concurrent.Callable; 33 | import java.util.function.Consumer; 34 | import java.util.stream.Collectors; 35 | 36 | public class DelegatingProjectParser implements GradleProjectParser { 37 | @Nullable 38 | protected static List rewriteClasspath; 39 | @Nullable 40 | protected static RewriteClassLoader rewriteClassLoader; 41 | protected final GradleProjectParser gpp; 42 | 43 | public DelegatingProjectParser(Project project, RewriteExtension extension, Set classpath) { 44 | try { 45 | List classpathUrls = classpath.stream() 46 | .map(Path::toUri) 47 | .map(uri -> { 48 | try { 49 | return uri.toURL(); 50 | } catch (MalformedURLException e) { 51 | throw new RuntimeException(e); 52 | } 53 | }) 54 | .collect(Collectors.toList()); 55 | 56 | @SuppressWarnings("ConstantConditions") 57 | URL currentJar = jarContainingResource(getClass() 58 | .getResource("/org/openrewrite/gradle/isolated/DefaultProjectParser.class") 59 | .toString()); 60 | classpathUrls.add(currentJar); 61 | 62 | ClassLoader pluginClassLoader = getPluginClassLoader(project); 63 | 64 | if (rewriteClassLoader == null || 65 | !classpathUrls.equals(rewriteClasspath) || 66 | rewriteClassLoader.getPluginClassLoader() != pluginClassLoader) { 67 | if (rewriteClassLoader != null) { 68 | rewriteClassLoader.close(); 69 | } 70 | rewriteClassLoader = new RewriteClassLoader(classpathUrls, pluginClassLoader); 71 | rewriteClasspath = classpathUrls; 72 | } 73 | 74 | Class gppClass = Class.forName("org.openrewrite.gradle.isolated.DefaultProjectParser", true, rewriteClassLoader); 75 | assert (gppClass.getClassLoader() == rewriteClassLoader) : "DefaultProjectParser must be loaded from RewriteClassLoader to be sufficiently isolated from Gradle's classpath"; 76 | gpp = (GradleProjectParser) gppClass.getDeclaredConstructor(Project.class, RewriteExtension.class) 77 | .newInstance(project, extension); 78 | 79 | } catch (Exception e) { 80 | throw new RuntimeException(e); 81 | } 82 | } 83 | 84 | @Override 85 | public List getActiveRecipes() { 86 | return unwrapInvocationException(gpp::getActiveRecipes); 87 | } 88 | 89 | @Override 90 | public List getActiveStyles() { 91 | return unwrapInvocationException(gpp::getActiveStyles); 92 | } 93 | 94 | @Override 95 | public List getAvailableStyles() { 96 | return unwrapInvocationException(gpp::getAvailableStyles); 97 | } 98 | 99 | @Override 100 | public void discoverRecipes(ServiceRegistry serviceRegistry) { 101 | unwrapInvocationException(() -> { 102 | gpp.discoverRecipes(serviceRegistry); 103 | return null; 104 | }); 105 | } 106 | 107 | @Override 108 | public Collection listSources() { 109 | return unwrapInvocationException(gpp::listSources); 110 | } 111 | 112 | @Override 113 | public void run(Consumer onError) { 114 | unwrapInvocationException(() -> { 115 | gpp.run(onError); 116 | return null; 117 | }); 118 | } 119 | 120 | @Override 121 | public void dryRun(Path reportPath, boolean dumpGcActivity, Consumer onError) { 122 | unwrapInvocationException(() -> { 123 | gpp.dryRun(reportPath, dumpGcActivity, onError); 124 | return null; 125 | }); 126 | } 127 | 128 | @Override 129 | public void shutdownRewrite() { 130 | unwrapInvocationException(() -> { 131 | gpp.shutdownRewrite(); 132 | return null; 133 | }); 134 | } 135 | 136 | protected URL jarContainingResource(String resourcePath) { 137 | try { 138 | if (resourcePath.startsWith("jar:")) { 139 | resourcePath = resourcePath.substring(4); 140 | int indexOfBang = resourcePath.indexOf("!"); 141 | if (indexOfBang != -1) { 142 | resourcePath = resourcePath.substring(0, indexOfBang); 143 | } 144 | return new URI(resourcePath).toURL(); 145 | } else if (resourcePath.startsWith("file:")) { 146 | return new URI(resourcePath.substring(0, resourcePath.lastIndexOf("/main/") + 6)).toURL(); 147 | } 148 | // This code path only gets taken when running the tests against older versions of Gradle 149 | // In all other circumstances, "path" will point at a jar file 150 | return Paths.get(System.getProperty("jarLocationForTest")).toUri().toURL(); 151 | } catch (Exception e) { 152 | throw new RuntimeException(e); 153 | } 154 | } 155 | 156 | /** 157 | * Bloating stack traces with reflection errors isn't generally helpful for understanding what went wrong. 158 | *

159 | * This highlights the actual cause of a problem, allowing Gradle's console to display something useful like 160 | * "Recipe validation errors detected ..." rather than only "InvocationTargetException ..." 161 | */ 162 | private T unwrapInvocationException(Callable supplier) { 163 | try { 164 | return supplier.call(); 165 | } catch (InvocationTargetException e) { 166 | if (e.getTargetException() instanceof RuntimeException) { 167 | throw (RuntimeException) e.getTargetException(); 168 | } else { 169 | throw new RuntimeException(e.getTargetException()); 170 | } 171 | } catch (Exception e) { 172 | throw new RuntimeException(e); 173 | } 174 | } 175 | 176 | private ClassLoader getPluginClassLoader(Project project) { 177 | ClassLoader pluginClassLoader = getAndroidPluginClassLoader(project); 178 | if (pluginClassLoader == null) { 179 | pluginClassLoader = getClass().getClassLoader(); 180 | } 181 | return pluginClassLoader; 182 | } 183 | 184 | private @Nullable ClassLoader getAndroidPluginClassLoader(Project project) { 185 | List pluginIds = Arrays.asList( 186 | "com.android.application", 187 | "com.android.library", 188 | "com.android.feature", 189 | "com.android.dynamic-feature", 190 | "com.android.test"); 191 | 192 | for (String pluginId : pluginIds) { 193 | if (project.getPlugins().hasPlugin(pluginId)) { 194 | Object plugin = project.getPlugins().getPlugin(pluginId); 195 | return plugin.getClass().getClassLoader(); 196 | } 197 | } 198 | return null; 199 | } 200 | } 201 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/openrewrite/gradle/GradleProjectParser.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.gradle; 17 | 18 | import org.gradle.internal.service.ServiceRegistry; 19 | 20 | import java.nio.file.Path; 21 | import java.util.Collection; 22 | import java.util.List; 23 | import java.util.function.Consumer; 24 | 25 | public interface GradleProjectParser { 26 | List getActiveRecipes(); 27 | 28 | List getActiveStyles(); 29 | 30 | List getAvailableStyles(); 31 | 32 | Collection listSources(); 33 | 34 | void discoverRecipes(ServiceRegistry serviceRegistry); 35 | 36 | /** 37 | * @deprecated Use {@link #discoverRecipes(ServiceRegistry)} instead. 38 | */ 39 | @Deprecated 40 | default void discoverRecipes(boolean interactive, ServiceRegistry serviceRegistry) { 41 | discoverRecipes(serviceRegistry); 42 | } 43 | 44 | void run(Consumer onError); 45 | 46 | void dryRun(Path reportPath, boolean dumpGcActivity, Consumer onError); 47 | 48 | void shutdownRewrite(); 49 | } 50 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/openrewrite/gradle/RewriteClassLoader.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.gradle; 17 | 18 | import java.net.URL; 19 | import java.net.URLClassLoader; 20 | import java.util.Arrays; 21 | import java.util.Collection; 22 | import java.util.List; 23 | 24 | /** 25 | * Rewrite uses jackson for serialization/deserialization. So do lots of other build plugins. 26 | * Gradle plugins all share the same classpath at runtime. 27 | *

28 | * This classloader exists to isolate rewrite's use of jackson from the rest of the build. 29 | */ 30 | public class RewriteClassLoader extends URLClassLoader { 31 | 32 | private static final List PARENT_LOADED_PACKAGES = Arrays.asList( 33 | "org.openrewrite.gradle.GradleProjectParser", 34 | "org.openrewrite.gradle.DefaultRewriteExtension", 35 | "org.openrewrite.gradle.RewriteExtension", 36 | "org.slf4j", 37 | "org.gradle", 38 | "groovy", 39 | "org.codehaus.groovy"); 40 | private static final List PLUGIN_LOADED_PACKAGES = Arrays.asList("com.android"); 41 | private final ClassLoader pluginClassLoader; 42 | 43 | public RewriteClassLoader(Collection artifacts) { 44 | this(artifacts, RewriteClassLoader.class.getClassLoader()); 45 | } 46 | 47 | public RewriteClassLoader(Collection artifacts, ClassLoader pluginClassLoader) { 48 | super(artifacts.toArray(new URL[0]), RewriteClassLoader.class.getClassLoader()); 49 | this.pluginClassLoader = pluginClassLoader; 50 | setDefaultAssertionStatus(true); 51 | } 52 | 53 | public ClassLoader getPluginClassLoader() { 54 | return pluginClassLoader; 55 | } 56 | 57 | /** 58 | * Load the named class. We want classes that extend org.openrewrite.Recipe to be loaded 59 | * by this ClassLoader only. But we want classes required to run recipes to continue 60 | * to be loaded by their parent ClassLoader to avoid ClassCastExceptions. In the case 61 | * of Android Gradle plugin classes, we use the ClassLoader of the plugin. 62 | */ 63 | protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { 64 | Class foundClass = findLoadedClass(name); 65 | if (foundClass == null) { 66 | try { 67 | if (shouldBeParentLoaded(name)) { 68 | foundClass = super.loadClass(name, resolve); 69 | } else if (shouldBePluginLoaded(name)) { 70 | foundClass = Class.forName(name, resolve, pluginClassLoader); 71 | } else { 72 | foundClass = findClass(name); 73 | } 74 | } catch (ClassNotFoundException e) { 75 | foundClass = super.loadClass(name, resolve); 76 | } 77 | } 78 | if (resolve) { 79 | resolveClass(foundClass); 80 | } 81 | return foundClass; 82 | } 83 | 84 | protected boolean shouldBeParentLoaded(String name) { 85 | return shouldBeLoaded(name, PARENT_LOADED_PACKAGES); 86 | } 87 | 88 | protected boolean shouldBePluginLoaded(String name) { 89 | return shouldBeLoaded(name, PLUGIN_LOADED_PACKAGES); 90 | } 91 | 92 | private boolean shouldBeLoaded(String name, List packagesToLoad) { 93 | for (String pkg : packagesToLoad) { 94 | if (name.startsWith(pkg)) { 95 | return true; 96 | } 97 | } 98 | return false; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/openrewrite/gradle/RewriteDiscoverTask.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.gradle; 17 | 18 | import org.gradle.api.tasks.TaskAction; 19 | 20 | import javax.inject.Inject; 21 | 22 | public class RewriteDiscoverTask extends AbstractRewriteTask { 23 | 24 | @Inject 25 | public RewriteDiscoverTask() { 26 | setGroup("rewrite"); 27 | setDescription("Lists all available recipes and their visitors"); 28 | } 29 | 30 | @TaskAction 31 | public void run() { 32 | getProjectParser().discoverRecipes(getServices()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/openrewrite/gradle/RewriteDryRunTask.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.gradle; 17 | 18 | import org.gradle.api.logging.Logger; 19 | import org.gradle.api.logging.Logging; 20 | import org.gradle.api.specs.Specs; 21 | import org.gradle.api.tasks.OutputFile; 22 | import org.gradle.api.tasks.TaskAction; 23 | 24 | import javax.inject.Inject; 25 | import java.nio.file.Path; 26 | 27 | public class RewriteDryRunTask extends AbstractRewriteTask { 28 | 29 | private static final Logger logger = Logging.getLogger(RewriteDryRunTask.class); 30 | 31 | @OutputFile 32 | public Path getReportPath() { 33 | return getProjectLayout() 34 | .getBuildDirectory() 35 | .get() 36 | .getAsFile() 37 | .toPath() 38 | .resolve("reports") 39 | .resolve("rewrite") 40 | .resolve("rewrite.patch"); 41 | } 42 | 43 | @Inject 44 | public RewriteDryRunTask() { 45 | setGroup("rewrite"); 46 | setDescription("Run the active refactoring recipes, producing a patch file. No source files will be changed."); 47 | getOutputs().upToDateWhen(Specs.SATISFIES_NONE); 48 | } 49 | 50 | @TaskAction 51 | public void run() { 52 | getProjectParser().dryRun(getReportPath(), dumpGcActivity, throwable -> logger.info("Error during rewrite dry run", throwable)); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/openrewrite/gradle/RewriteExtension.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.gradle; 17 | 18 | import org.gradle.api.Project; 19 | 20 | import javax.annotation.Nullable; 21 | import javax.inject.Provider; 22 | import java.io.File; 23 | import java.io.IOException; 24 | import java.io.InputStream; 25 | import java.util.*; 26 | 27 | import static java.util.Arrays.asList; 28 | import static java.util.Collections.emptyMap; 29 | 30 | @SuppressWarnings("unused") 31 | public class RewriteExtension { 32 | 33 | private final List activeRecipes = new ArrayList<>(); 34 | private final List activeStyles = new ArrayList<>(); 35 | private boolean configFileSetDeliberately; 36 | 37 | protected final Project project; 38 | private File configFile; 39 | 40 | @Nullable 41 | private Provider checkstyleConfigProvider; 42 | 43 | @Nullable 44 | private Provider> checkstylePropertiesProvider; 45 | 46 | @Nullable 47 | private File checkstyleConfigFile; 48 | private boolean enableExperimentalGradleBuildScriptParsing = true; 49 | private boolean exportDatatables; 50 | private final List exclusions = new ArrayList<>(); 51 | private final List plainTextMasks = new ArrayList<>(); 52 | 53 | private int sizeThresholdMb = 10; 54 | 55 | @Nullable 56 | private String rewriteVersion; 57 | 58 | @Nullable 59 | private Properties versionProps; 60 | 61 | private boolean logCompilationWarningsAndErrors; 62 | 63 | /** 64 | * Whether to throw an exception if an activeRecipe fails configuration validation. 65 | * This may happen if the activeRecipe is improperly configured, or any downstream recipes are improperly configured. 66 | *

67 | * For the time, this default is "false" to prevent one improperly recipe from failing the build. 68 | * In the future, this default may be changed to "true" to be more restrictive. 69 | */ 70 | private boolean failOnInvalidActiveRecipes; 71 | 72 | private boolean failOnDryRunResults; 73 | 74 | private boolean throwOnParseFailures; 75 | 76 | @SuppressWarnings("unused") 77 | public RewriteExtension(Project project) { 78 | this.project = project; 79 | configFile = project.file("rewrite.yml"); 80 | } 81 | 82 | public void setConfigFile(File configFile) { 83 | configFileSetDeliberately = true; 84 | this.configFile = configFile; 85 | } 86 | 87 | public void setConfigFile(String configFilePath) { 88 | configFileSetDeliberately = true; 89 | configFile = project.file(configFilePath); 90 | } 91 | 92 | public void setCheckstyleConfigFile(File configFile) { 93 | this.checkstyleConfigFile = configFile; 94 | } 95 | 96 | /** 97 | * Will prefer to return an explicitly configured checkstyle configuration file location. 98 | * If none has been specified, will attempt to auto-detect an appropriate file. 99 | */ 100 | public @org.jspecify.annotations.Nullable @Nullable File getCheckstyleConfigFile() { 101 | if (checkstyleConfigFile == null && checkstyleConfigProvider != null) { 102 | try { 103 | return checkstyleConfigProvider.get(); 104 | } catch (Exception e) { 105 | return null; 106 | } 107 | } 108 | return checkstyleConfigFile; 109 | } 110 | 111 | public Map getCheckstyleProperties() { 112 | if (checkstyleConfigProvider == null) { 113 | return emptyMap(); 114 | } 115 | return checkstylePropertiesProvider.get(); 116 | } 117 | 118 | /** 119 | * Supplying a rewrite configuration file is optional, so if it doesn't exist it isn't an error or a warning. 120 | * But if the user has deliberately specified a different location from the default, that seems like a reasonable 121 | * signal that the file should be expected to exist. So this signal can be used to decide if a warning should be 122 | * displayed if the specified file cannot be found. 123 | */ 124 | public boolean getConfigFileSetDeliberately() { 125 | return configFileSetDeliberately; 126 | } 127 | 128 | public File getConfigFile() { 129 | return configFile; 130 | } 131 | 132 | public void activeRecipe(String... recipes) { 133 | activeRecipes.addAll(asList(recipes)); 134 | } 135 | 136 | public void clearActiveRecipes() { 137 | activeRecipes.clear(); 138 | } 139 | 140 | public void setActiveRecipes(List activeRecipes) { 141 | this.activeRecipes.clear(); 142 | this.activeRecipes.addAll(activeRecipes); 143 | } 144 | 145 | public void activeStyle(String... styles) { 146 | activeStyles.addAll(asList(styles)); 147 | } 148 | 149 | public void clearActiveStyles() { 150 | activeStyles.clear(); 151 | } 152 | 153 | public void setActiveStyles(List activeStyles) { 154 | this.activeRecipes.clear(); 155 | this.activeRecipes.addAll(activeStyles); 156 | } 157 | 158 | public List getActiveStyles() { 159 | return activeStyles; 160 | } 161 | 162 | public List getActiveRecipes() { 163 | return activeRecipes; 164 | } 165 | 166 | public Properties getVersionProps() { 167 | if (versionProps == null) { 168 | try (InputStream is = RewriteExtension.class.getResourceAsStream("/rewrite/versions.properties")) { 169 | versionProps = new Properties(); 170 | versionProps.load(is); 171 | } catch (IOException e) { 172 | throw new RuntimeException(e); 173 | } 174 | } 175 | return versionProps; 176 | } 177 | 178 | /** 179 | * Returns the version of rewrite core libraries to be used. 180 | */ 181 | public String getRewriteVersion() { 182 | if (rewriteVersion == null) { 183 | return getVersionProps().getProperty("org.openrewrite:rewrite-core"); 184 | } 185 | return rewriteVersion; 186 | } 187 | 188 | @Nullable 189 | private String rewritePolyglotVersion; 190 | public String getRewritePolyglotVersion() { 191 | if (rewritePolyglotVersion == null) { 192 | return getVersionProps().getProperty("org.openrewrite:rewrite-polyglot"); 193 | } 194 | return rewritePolyglotVersion; 195 | } 196 | 197 | @Nullable 198 | private String rewriteGradleModelVersion; 199 | public String getRewriteGradleModelVersion() { 200 | if (rewriteGradleModelVersion == null) { 201 | rewriteGradleModelVersion = getVersionProps().getProperty("org.openrewrite.gradle.tooling:model"); 202 | } 203 | return rewriteGradleModelVersion; 204 | } 205 | 206 | public void setRewriteVersion(String value) { 207 | rewriteVersion = value; 208 | } 209 | 210 | public boolean getFailOnInvalidActiveRecipes() { 211 | return failOnInvalidActiveRecipes; 212 | } 213 | 214 | public void setFailOnInvalidActiveRecipes(boolean failOnInvalidActiveRecipes) { 215 | this.failOnInvalidActiveRecipes = failOnInvalidActiveRecipes; 216 | } 217 | 218 | public boolean getFailOnDryRunResults() { 219 | return this.failOnDryRunResults; 220 | } 221 | 222 | public void setFailOnDryRunResults(boolean failOnDryRunResults) { 223 | this.failOnDryRunResults = failOnDryRunResults; 224 | } 225 | 226 | public boolean getLogCompilationWarningsAndErrors() { 227 | return logCompilationWarningsAndErrors; 228 | } 229 | 230 | public void setLogCompilationWarningsAndErrors(boolean logCompilationWarningsAndErrors) { 231 | this.logCompilationWarningsAndErrors = logCompilationWarningsAndErrors; 232 | } 233 | 234 | public Provider getCheckstyleConfigProvider() { 235 | return checkstyleConfigProvider; 236 | } 237 | 238 | public void setCheckstyleConfigProvider(Provider checkstyleConfigProvider) { 239 | this.checkstyleConfigProvider = checkstyleConfigProvider; 240 | } 241 | 242 | public Provider> getCheckstylePropertiesProvider() { 243 | return checkstylePropertiesProvider; 244 | } 245 | 246 | public void setCheckstylePropertiesProvider(Provider> checkstylePropertiesProvider) { 247 | this.checkstylePropertiesProvider = checkstylePropertiesProvider; 248 | } 249 | 250 | public boolean isEnableExperimentalGradleBuildScriptParsing() { 251 | return enableExperimentalGradleBuildScriptParsing; 252 | } 253 | 254 | public void setEnableExperimentalGradleBuildScriptParsing(boolean enableExperimentalGradleBuildScriptParsing) { 255 | this.enableExperimentalGradleBuildScriptParsing = enableExperimentalGradleBuildScriptParsing; 256 | } 257 | 258 | public boolean isExportDatatables() { 259 | return exportDatatables; 260 | } 261 | 262 | public void setExportDatatables(boolean exportDatatables) { 263 | this.exportDatatables = exportDatatables; 264 | } 265 | 266 | public List getExclusions() { 267 | return exclusions; 268 | } 269 | 270 | public void exclusion(String... exclusions) { 271 | this.exclusions.addAll(asList(exclusions)); 272 | } 273 | 274 | public void exclusion(Collection exclusions) { 275 | this.exclusions.addAll(exclusions); 276 | } 277 | 278 | public List getPlainTextMasks() { 279 | if (plainTextMasks.isEmpty()) { 280 | plainTextMasks.addAll(Arrays.asList( 281 | "**/*.adoc", 282 | "**/*.aj", 283 | "**/*.bash", 284 | "**/*.bat", 285 | "**/CODEOWNERS", 286 | "**/*.css", 287 | "**/*.config", 288 | "**/Dockerfile*", 289 | "**/*.env", 290 | "**/.gitattributes", 291 | "**/.gitignore", 292 | "**/*.htm*", 293 | "**/gradlew", 294 | "**/.java-version", 295 | "**/*.jelly", 296 | "**/*.jsp", 297 | "**/*.ksh", 298 | "**/*.lock", 299 | "**/lombok.config", 300 | "**/*.md", 301 | "**/*.mf", 302 | "**/META-INF/services/**", 303 | "**/META-INF/spring/**", 304 | "**/META-INF/spring.factories", 305 | "**/mvnw", 306 | "**/*.qute.java", 307 | "**/.sdkmanrc", 308 | "**/*.sh", 309 | "**/*.sql", 310 | "**/*.svg", 311 | "**/*.tsx", 312 | "**/*.txt" 313 | )); 314 | } 315 | return plainTextMasks; 316 | } 317 | 318 | public void plainTextMask(String... masks) { 319 | this.plainTextMasks.addAll(asList(masks)); 320 | } 321 | 322 | public void plainTextMask(Collection masks) { 323 | this.plainTextMasks.addAll(masks); 324 | } 325 | 326 | public int getSizeThresholdMb() { 327 | return sizeThresholdMb; 328 | } 329 | 330 | public void setSizeThresholdMb(int thresholdMb) { 331 | this.sizeThresholdMb = thresholdMb; 332 | } 333 | 334 | public String getJacksonModuleKotlinVersion() { 335 | return getVersionProps().getProperty("com.fasterxml.jackson.module:jackson-module-kotlin"); 336 | } 337 | 338 | public boolean getThrowOnParseFailures() { 339 | if (project.getProperties().containsKey("rewrite.throwOnParseFailures")) { 340 | return true; 341 | } 342 | return throwOnParseFailures; 343 | } 344 | 345 | public void setThrowOnParseFailures(boolean throwOnParseFailures) { 346 | this.throwOnParseFailures = throwOnParseFailures; 347 | } 348 | } 349 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/openrewrite/gradle/RewritePlugin.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.gradle; 17 | 18 | import org.gradle.api.Plugin; 19 | import org.gradle.api.Project; 20 | import org.gradle.api.Task; 21 | import org.gradle.api.artifacts.Configuration; 22 | import org.gradle.api.artifacts.Dependency; 23 | import org.gradle.api.artifacts.dsl.DependencyHandler; 24 | import org.gradle.api.attributes.*; 25 | import org.gradle.api.attributes.java.TargetJvmEnvironment; 26 | import org.gradle.api.model.ObjectFactory; 27 | import org.gradle.api.plugins.JavaBasePlugin; 28 | import org.gradle.api.plugins.JavaPluginConvention; 29 | import org.gradle.api.plugins.JavaPluginExtension; 30 | import org.gradle.api.plugins.quality.CheckstyleExtension; 31 | import org.gradle.api.plugins.quality.CheckstylePlugin; 32 | import org.gradle.api.provider.Provider; 33 | import org.gradle.api.tasks.SourceSetContainer; 34 | import org.gradle.api.tasks.TaskProvider; 35 | import org.jspecify.annotations.Nullable; 36 | 37 | import java.io.File; 38 | import java.util.*; 39 | 40 | import static org.gradle.api.attributes.Bundling.BUNDLING_ATTRIBUTE; 41 | import static org.gradle.api.attributes.java.TargetJvmEnvironment.TARGET_JVM_ENVIRONMENT_ATTRIBUTE; 42 | 43 | /** 44 | * When applied to the root project of a multi-project build, applies to all subprojects. 45 | * When applied to the root project the "rewrite" configuration and "rewrite" DSL created in the root project apply to 46 | * all parts of the build. 47 | * When applied to a subproject of a multi-project build, applies only to that subproject. 48 | * Creates "rewrite" dependency configuration and "rewrite" DSL in and applicable to only that subproject. 49 | */ 50 | @SuppressWarnings("unused") 51 | public class RewritePlugin implements Plugin { 52 | 53 | @Nullable 54 | private Set resolvedDependencies; 55 | 56 | @Override 57 | public void apply(Project project) { 58 | boolean isRootProject = project == project.getRootProject(); 59 | if (!isRootProject && project.getRootProject().getPluginManager().hasPlugin("org.openrewrite.rewrite")) { 60 | return; 61 | } 62 | if (project.getRootProject().getPluginManager().hasPlugin("io.moderne.rewrite")) { 63 | // Moderne plugin provides superset of rewrite plugin functionality, no need to apply both 64 | return; 65 | } 66 | RewriteExtension extension = project.getExtensions().create("rewrite", RewriteExtension.class, project); 67 | 68 | // Rewrite module dependencies put here will be available to all rewrite tasks 69 | Configuration rewriteConf = project.getConfigurations().maybeCreate("rewrite"); 70 | 71 | Provider> resolvedDependenciesProvider = project.provider(() -> getResolvedDependencies(project, extension, rewriteConf)); 72 | 73 | TaskProvider rewriteRun = project.getTasks().register("rewriteRun", RewriteRunTask.class, task -> { 74 | task.setExtension(extension); 75 | task.setResolvedDependencies(resolvedDependenciesProvider); 76 | task.dependsOn(rewriteConf); 77 | }); 78 | 79 | TaskProvider rewriteDryRun = project.getTasks().register("rewriteDryRun", RewriteDryRunTask.class, task -> { 80 | task.setExtension(extension); 81 | task.setResolvedDependencies(resolvedDependenciesProvider); 82 | task.dependsOn(rewriteConf); 83 | }); 84 | 85 | TaskProvider rewriteDiscover = project.getTasks().register("rewriteDiscover", RewriteDiscoverTask.class, task -> { 86 | task.setExtension(extension); 87 | task.setResolvedDependencies(resolvedDependenciesProvider); 88 | task.dependsOn(rewriteConf); 89 | }); 90 | 91 | if (isRootProject) { 92 | project.allprojects(subproject -> configureProject(subproject, extension, rewriteDryRun, rewriteRun)); 93 | } else { 94 | configureProject(project, extension, rewriteDryRun, rewriteRun); 95 | } 96 | } 97 | 98 | private static void configureProject(Project project, RewriteExtension extension, TaskProvider rewriteDryRun, TaskProvider rewriteRun) { 99 | // DomainObjectCollection.all() accepts a function to be applied to both existing and subsequently added members of the collection 100 | // Do not replace all() with any form of collection iteration which does not share this important property 101 | project.getPlugins().all(plugin -> { 102 | if (plugin instanceof CheckstylePlugin) { 103 | // A multi-project build could hypothetically have different checkstyle configuration per-project 104 | // In practice all projects tend to have the same configuration 105 | CheckstyleExtension checkstyleExtension = project.getExtensions().getByType(CheckstyleExtension.class); 106 | extension.setCheckstyleConfigProvider(checkstyleExtension::getConfigFile); 107 | extension.setCheckstylePropertiesProvider(checkstyleExtension::getConfigProperties); 108 | } 109 | if (!(plugin instanceof JavaBasePlugin)) { 110 | return; 111 | } 112 | 113 | //Collect Java metadata for each project (used for Java Provenance) 114 | SourceSetContainer sourceSets; 115 | if (project.getGradle().getGradleVersion().compareTo("7.1") >= 0) { 116 | sourceSets = project.getExtensions().getByType(JavaPluginExtension.class).getSourceSets(); 117 | } else { 118 | //Using the older javaConvention because we need to support older versions of gradle. 119 | @SuppressWarnings("deprecation") 120 | JavaPluginConvention javaConvention = project.getConvention().getPlugin(JavaPluginConvention.class); 121 | sourceSets = javaConvention.getSourceSets(); 122 | } 123 | sourceSets.all(sourceSet -> { 124 | // This is intended to ensure that any Groovy/Kotlin/etc. and dependent project sources are available 125 | TaskProvider compileTask = project.getTasks().named(sourceSet.getCompileJavaTaskName()); 126 | rewriteRun.configure(task -> task.dependsOn(compileTask)); 127 | rewriteDryRun.configure(task -> task.dependsOn(compileTask)); 128 | }); 129 | 130 | // Detect SourceSets which overlap other sourceSets and disable the compilation task of the overlapping 131 | // source set. Some plugins will create source sets not intended to be compiled for their own purposes. 132 | Set sourceDirs = new HashSet<>(); 133 | project.afterEvaluate(unused -> sourceSets.stream() 134 | .sorted(Comparator.comparingInt(sourceSet -> { 135 | if ("main".equals(sourceSet.getName())) { 136 | return 0; 137 | } else if ("test".equals(sourceSet.getName())) { 138 | return 1; 139 | } else { 140 | return 2; 141 | } 142 | })).forEach(sourceSet -> { 143 | for (File file : sourceSet.getAllJava().getSourceDirectories().getFiles()) { 144 | if (!sourceDirs.add(file.getAbsolutePath())) { 145 | TaskProvider compileTask = project.getTasks().named(sourceSet.getCompileJavaTaskName()); 146 | compileTask.configure(task -> task.setEnabled(false)); 147 | } 148 | } 149 | })); 150 | }); 151 | } 152 | 153 | private Set getResolvedDependencies(Project project, RewriteExtension extension, Configuration rewriteConf) { 154 | if (resolvedDependencies == null) { 155 | // Avoid Stream.concat here pending https://github.com/gradle/gradle/issues/33152 156 | List dependencies = new ArrayList<>(); 157 | dependencies.addAll(knownRewriteDependencies(extension, project.getDependencies())); 158 | dependencies.addAll(rewriteConf.getDependencies()); 159 | // By using a detached configuration, we separate this dependency resolution from the rest of the project's 160 | // configuration. This also means that Gradle has no criteria with which to select between variants of 161 | // dependencies which expose differing capabilities. So those must be manually configured 162 | Configuration detachedConf = project.getConfigurations().detachedConfiguration(dependencies.toArray(new Dependency[0])); 163 | 164 | try { 165 | ObjectFactory objectFactory = project.getObjects(); 166 | detachedConf.attributes(attributes -> { 167 | // Adapted from org.gradle.api.plugins.jvm.internal.DefaultJvmEcosystemAttributesDetails 168 | attributes.attribute(Category.CATEGORY_ATTRIBUTE, objectFactory.named(Category.class, Category.LIBRARY)); 169 | attributes.attribute(Usage.USAGE_ATTRIBUTE, objectFactory.named(Usage.class, Usage.JAVA_RUNTIME)); 170 | attributes.attribute(LibraryElements.LIBRARY_ELEMENTS_ATTRIBUTE, objectFactory.named(LibraryElements.class, LibraryElements.JAR)); 171 | attributes.attribute(BUNDLING_ATTRIBUTE, objectFactory.named(Bundling.class, Bundling.EXTERNAL)); 172 | try { 173 | attributes.attribute(TARGET_JVM_ENVIRONMENT_ATTRIBUTE, objectFactory.named(TargetJvmEnvironment.class, TargetJvmEnvironment.STANDARD_JVM)); 174 | } catch (NoClassDefFoundError e) { 175 | // Old versions of Gradle don't have the class TargetJvmEnvironment and that's OK, we can always 176 | // try this attribute instead 177 | attributes.attribute(Attribute.of("org.gradle.jvm.environment", String.class), "standard-jvm"); 178 | } 179 | }); 180 | } catch (NoClassDefFoundError e) { 181 | // Old versions of Gradle don't have all of these attributes and that's OK 182 | } 183 | 184 | resolvedDependencies = detachedConf.resolve(); 185 | } 186 | return resolvedDependencies; 187 | } 188 | 189 | private static Collection knownRewriteDependencies(RewriteExtension extension, DependencyHandler deps) { 190 | String rewriteVersion = extension.getRewriteVersion(); 191 | return Arrays.asList( 192 | deps.create("org.openrewrite:rewrite-core:" + rewriteVersion), 193 | deps.create("org.openrewrite:rewrite-groovy:" + rewriteVersion), 194 | deps.create("org.openrewrite:rewrite-gradle:" + rewriteVersion), 195 | deps.create("org.openrewrite:rewrite-hcl:" + rewriteVersion), 196 | deps.create("org.openrewrite:rewrite-json:" + rewriteVersion), 197 | deps.create("org.openrewrite:rewrite-kotlin:" + rewriteVersion), 198 | deps.create("org.openrewrite:rewrite-java:" + rewriteVersion), 199 | deps.create("org.openrewrite:rewrite-java-21:" + rewriteVersion), 200 | deps.create("org.openrewrite:rewrite-java-17:" + rewriteVersion), 201 | deps.create("org.openrewrite:rewrite-java-11:" + rewriteVersion), 202 | deps.create("org.openrewrite:rewrite-java-8:" + rewriteVersion), 203 | deps.create("org.openrewrite:rewrite-maven:" + rewriteVersion), 204 | deps.create("org.openrewrite:rewrite-properties:" + rewriteVersion), 205 | deps.create("org.openrewrite:rewrite-protobuf:" + rewriteVersion), 206 | deps.create("org.openrewrite:rewrite-toml:" + rewriteVersion), 207 | deps.create("org.openrewrite:rewrite-xml:" + rewriteVersion), 208 | deps.create("org.openrewrite:rewrite-yaml:" + rewriteVersion), 209 | deps.create("org.openrewrite:rewrite-polyglot:" + extension.getRewritePolyglotVersion()), 210 | deps.create("org.openrewrite.gradle.tooling:model:" + extension.getRewriteGradleModelVersion()), 211 | deps.create("com.fasterxml.jackson.module:jackson-module-kotlin:" + extension.getJacksonModuleKotlinVersion()), 212 | deps.create("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:" + extension.getJacksonModuleKotlinVersion()) 213 | ); 214 | } 215 | } 216 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/openrewrite/gradle/RewriteRunTask.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.gradle; 17 | 18 | import org.gradle.api.logging.Logger; 19 | import org.gradle.api.logging.Logging; 20 | import org.gradle.api.tasks.TaskAction; 21 | 22 | import javax.inject.Inject; 23 | 24 | public class RewriteRunTask extends AbstractRewriteTask { 25 | 26 | private static final Logger logger = Logging.getLogger(RewriteRunTask.class); 27 | 28 | @Inject 29 | public RewriteRunTask() { 30 | setGroup("rewrite"); 31 | setDescription("Apply the active refactoring recipes"); 32 | } 33 | 34 | @TaskAction 35 | public void run() { 36 | getProjectParser().run(throwable -> logger.info("Error during rewrite run", throwable)); 37 | } 38 | 39 | } 40 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/openrewrite/gradle/SanitizedMarkerPrinter.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.gradle; 17 | 18 | import org.openrewrite.Cursor; 19 | import org.openrewrite.PrintOutputCapture; 20 | import org.openrewrite.marker.Marker; 21 | import org.openrewrite.marker.SearchResult; 22 | 23 | import java.util.function.UnaryOperator; 24 | 25 | /** 26 | * A {@link PrintOutputCapture} that sanitizes the diff of informational markers, 27 | * so these aren't accidentally committed to source control. 28 | */ 29 | public class SanitizedMarkerPrinter implements PrintOutputCapture.MarkerPrinter { 30 | @Override 31 | public String beforeSyntax(Marker marker, Cursor cursor, UnaryOperator commentWrapper) { 32 | if (marker instanceof SearchResult) { 33 | return DEFAULT.beforeSyntax(marker, cursor, commentWrapper); 34 | } 35 | return ""; 36 | } 37 | 38 | @Override 39 | public String afterSyntax(Marker marker, Cursor cursor, UnaryOperator commentWrapper) { 40 | if (marker instanceof SearchResult) { 41 | return DEFAULT.afterSyntax(marker, cursor, commentWrapper); 42 | } 43 | return ""; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/openrewrite/gradle/isolated/AndroidProjectCompileOptions.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.gradle.isolated; 17 | 18 | import com.android.build.gradle.BaseExtension; 19 | import org.gradle.api.JavaVersion; 20 | 21 | import java.lang.reflect.Method; 22 | import java.nio.charset.Charset; 23 | 24 | /* 25 | * AGP versions less than 4 define CompileOptions in com.android.build.gradle.internal.CompileOptions where as 26 | * versions greater than 4 define it in com.android.build.api.dsl.CompileOptions. This class encapsulates fetching 27 | * CompileOptions using either type. 28 | */ 29 | class AndroidProjectCompileOptions { 30 | private final Charset encoding; 31 | private final String sourceCompatibility; 32 | private final String targetCompatibility; 33 | 34 | AndroidProjectCompileOptions(Charset encoding, String sourceCompatibility, String targetCompatibility) { 35 | this.encoding = encoding; 36 | this.sourceCompatibility = sourceCompatibility; 37 | this.targetCompatibility = targetCompatibility; 38 | } 39 | 40 | static AndroidProjectCompileOptions fromBaseExtension(BaseExtension baseExtension) throws ReflectiveOperationException { 41 | Object compileOptions = callMethod(baseExtension, "getCompileOptions"); 42 | String fileEncoding = callMethod(compileOptions, "getEncoding"); 43 | JavaVersion sourceCompatibilityVersion = callMethod(compileOptions, "getSourceCompatibility"); 44 | JavaVersion targetCompatibilityVersion = callMethod(compileOptions, "getTargetCompatibility"); 45 | return new AndroidProjectCompileOptions( 46 | Charset.forName(fileEncoding), 47 | sourceCompatibilityVersion.toString(), 48 | targetCompatibilityVersion.toString()); 49 | } 50 | 51 | private static T callMethod(Object obj, String methodName) throws ReflectiveOperationException { 52 | Method method = obj.getClass().getMethod(methodName); 53 | return (T) method.invoke(obj); 54 | } 55 | 56 | Charset getEncoding() { 57 | return encoding; 58 | } 59 | 60 | String getSourceCompatibility() { 61 | return sourceCompatibility; 62 | } 63 | 64 | String getTargetCompatibility() { 65 | return targetCompatibility; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/openrewrite/gradle/isolated/AndroidProjectParser.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.gradle.isolated; 17 | 18 | import com.android.build.gradle.BaseExtension; 19 | import com.android.build.gradle.LibraryExtension; 20 | import com.android.build.gradle.api.BaseVariant; 21 | import com.android.build.gradle.internal.dsl.BaseAppModuleExtension; 22 | import org.gradle.api.DomainObjectSet; 23 | import org.gradle.api.Project; 24 | import org.gradle.api.logging.Logger; 25 | import org.gradle.api.logging.Logging; 26 | import org.openrewrite.ExecutionContext; 27 | import org.openrewrite.SourceFile; 28 | import org.openrewrite.Tree; 29 | import org.openrewrite.gradle.RewriteExtension; 30 | import org.openrewrite.java.JavaParser; 31 | import org.openrewrite.java.internal.JavaTypeCache; 32 | import org.openrewrite.java.marker.JavaSourceSet; 33 | import org.openrewrite.java.marker.JavaVersion; 34 | import org.openrewrite.kotlin.KotlinParser; 35 | import org.openrewrite.polyglot.OmniParser; 36 | import org.openrewrite.polyglot.ProgressBar; 37 | import org.openrewrite.polyglot.SourceFileStream; 38 | import org.openrewrite.style.NamedStyles; 39 | import org.openrewrite.tree.ParsingExecutionContextView; 40 | 41 | import java.io.IOException; 42 | import java.io.UncheckedIOException; 43 | import java.nio.charset.Charset; 44 | import java.nio.file.Files; 45 | import java.nio.file.Path; 46 | import java.nio.file.PathMatcher; 47 | import java.util.*; 48 | import java.util.function.Supplier; 49 | import java.util.stream.Collectors; 50 | import java.util.stream.Stream; 51 | 52 | class AndroidProjectParser { 53 | private static final Logger logger = Logging.getLogger(DefaultProjectParser.class); 54 | private final Path baseDir; 55 | private final RewriteExtension rewriteExtension; 56 | private final List styles; 57 | 58 | AndroidProjectParser(Path baseDir, RewriteExtension rewriteExtension, List styles) { 59 | this.baseDir = baseDir; 60 | this.rewriteExtension = rewriteExtension; 61 | this.styles = styles; 62 | } 63 | 64 | SourceFileStream parseProjectSourceSets(Project project, 65 | ProgressBar progressBar, 66 | Path buildDir, 67 | Charset sourceCharset, 68 | Set alreadyParsed, 69 | Collection exclusions, 70 | ExecutionContext ctx, 71 | OmniParser omniParser) { 72 | SourceFileStream sourceFileStream = SourceFileStream.build( 73 | project.getPath(), 74 | projectName -> progressBar.intermediateResult(":" + projectName)); 75 | 76 | for (AndroidProjectVariant variant : findAndroidProjectVariants(project)) { 77 | JavaVersion javaVersion = getJavaVersion(project); 78 | final Charset javaSourceCharset = getSourceFileEncoding(project, sourceCharset); 79 | 80 | for (String sourceSetName : variant.getSourceSetNames()) { 81 | Stream sourceSetSourceFiles = Stream.of(); 82 | int sourceSetSize = 0; 83 | 84 | Set javaAndKotlinDirectories = new HashSet<>(); 85 | javaAndKotlinDirectories.addAll(variant.getJavaDirectories(sourceSetName)); 86 | javaAndKotlinDirectories.addAll(variant.getKotlinDirectories(sourceSetName)); 87 | 88 | Set javaAndKotlinPaths = javaAndKotlinDirectories.stream() 89 | .filter(Files::exists) 90 | .filter(dir -> !alreadyParsed.contains(dir)) 91 | .flatMap(dir -> { 92 | try { 93 | return Files.walk(dir); 94 | } catch (IOException e) { 95 | throw new UncheckedIOException(e); 96 | } 97 | }) 98 | .filter(Files::isRegularFile) 99 | .map(Path::toAbsolutePath) 100 | .map(Path::normalize) 101 | .filter(path -> !alreadyParsed.contains(path)) 102 | .collect(Collectors.toSet()); 103 | 104 | List javaPaths = javaAndKotlinPaths.stream() 105 | .filter(path -> path.toString().endsWith(".java")) 106 | .collect(Collectors.toList()); 107 | List kotlinPaths = javaAndKotlinPaths.stream() 108 | .filter(path -> path.toString().endsWith(".kt")) 109 | .collect(Collectors.toList()); 110 | 111 | JavaTypeCache javaTypeCache = new JavaTypeCache(); 112 | 113 | // The compilation classpath doesn't include the transitive dependencies 114 | // The runtime classpath doesn't include compile only dependencies, e.g.: lombok, servlet-api 115 | // So we use both together to get comprehensive type information. 116 | Set dependencyPaths = new HashSet<>(); 117 | try { 118 | Stream.concat(variant.getCompileClasspath().stream(), variant.getRuntimeClasspath().stream()) 119 | .map(Path::toAbsolutePath) 120 | .map(Path::normalize) 121 | .forEach(dependencyPaths::add); 122 | } catch (Exception e) { 123 | logger.warn("Unable to resolve classpath for variant {} sourceSet {}:{}", 124 | variant.getName(), 125 | project.getPath(), 126 | sourceSetName, 127 | e); 128 | } 129 | 130 | if (!javaPaths.isEmpty()) { 131 | alreadyParsed.addAll(javaPaths); 132 | Stream parsedJavaFiles = parseJavaFiles(javaPaths, 133 | ctx, 134 | buildDir, 135 | exclusions, 136 | javaSourceCharset, 137 | javaVersion, 138 | dependencyPaths, 139 | javaTypeCache); 140 | sourceSetSourceFiles = Stream.concat(sourceSetSourceFiles, parsedJavaFiles); 141 | sourceSetSize += javaPaths.size(); 142 | 143 | logger.info("Scanned {} Java sources in {}/{}", javaPaths.size(), project.getPath(), sourceSetName); 144 | } 145 | 146 | if (!kotlinPaths.isEmpty()) { 147 | alreadyParsed.addAll(kotlinPaths); 148 | Stream parsedKotlinFiles = parseKotlinFiles(kotlinPaths, 149 | ctx, 150 | buildDir, 151 | exclusions, 152 | javaSourceCharset, 153 | javaVersion, 154 | dependencyPaths, 155 | javaTypeCache); 156 | sourceSetSourceFiles = Stream.concat(sourceSetSourceFiles, parsedKotlinFiles); 157 | sourceSetSize += kotlinPaths.size(); 158 | 159 | logger.info("Scanned {} Kotlin sources in {}/{}", 160 | kotlinPaths.size(), 161 | project.getPath(), 162 | sourceSetName); 163 | } 164 | 165 | for (Path resourcesDir : variant.getResourcesDirectories(sourceSetName)) { 166 | if (Files.exists(resourcesDir) && !alreadyParsed.contains(resourcesDir)) { 167 | Set accepted = 168 | omniParser.acceptedPaths(baseDir, resourcesDir) 169 | .stream() 170 | .filter(path -> !alreadyParsed.contains(path)) 171 | .collect(Collectors.toSet()); 172 | sourceSetSourceFiles = Stream.concat( 173 | sourceSetSourceFiles, 174 | omniParser.parse(accepted, baseDir, ctx) 175 | .map(it -> it.withMarkers(it.getMarkers().add(javaVersion)))); 176 | alreadyParsed.addAll(accepted); 177 | sourceSetSize += accepted.size(); 178 | } 179 | } 180 | 181 | JavaSourceSet sourceSetProvenance = JavaSourceSet.build(sourceSetName, dependencyPaths); 182 | sourceFileStream = sourceFileStream.concat(sourceSetSourceFiles.map(DefaultProjectParser.addProvenance( 183 | sourceSetProvenance)), 184 | sourceSetSize); 185 | } 186 | } 187 | return sourceFileStream; 188 | } 189 | 190 | Collection findSourceDirectories(Project project) { 191 | Set sourceDirectories = new HashSet<>(); 192 | for (AndroidProjectVariant variant : findAndroidProjectVariants(project)) { 193 | for (String sourceSetName : variant.getSourceSetNames()) { 194 | sourceDirectories.addAll(variant.getJavaDirectories(sourceSetName)); 195 | sourceDirectories.addAll(variant.getKotlinDirectories(sourceSetName)); 196 | sourceDirectories.addAll(variant.getResourcesDirectories(sourceSetName)); 197 | } 198 | } 199 | return sourceDirectories; 200 | } 201 | 202 | private List findAndroidProjectVariants(Project project) { 203 | List variants = new ArrayList<>(); 204 | Object extension = project.getExtensions().findByName("android"); 205 | if (extension instanceof BaseAppModuleExtension) { 206 | BaseAppModuleExtension appExtension = (BaseAppModuleExtension) extension; 207 | addProjectVariant(variants, appExtension.getApplicationVariants()); 208 | addProjectVariant(variants, appExtension.getTestVariants()); 209 | addProjectVariant(variants, appExtension.getUnitTestVariants()); 210 | } else if (extension instanceof LibraryExtension) { 211 | LibraryExtension libraryExtension = (LibraryExtension) extension; 212 | addProjectVariant(variants, libraryExtension.getLibraryVariants()); 213 | addProjectVariant(variants, libraryExtension.getTestVariants()); 214 | addProjectVariant(variants, libraryExtension.getUnitTestVariants()); 215 | } else if (extension != null) { 216 | throw new UnsupportedOperationException("Unhandled android extension type: " + extension.getClass()); 217 | } 218 | 219 | return variants; 220 | } 221 | 222 | private void addProjectVariant(List projectVariants, 223 | DomainObjectSet variantSet) { 224 | variantSet.stream().map(AndroidProjectVariant::fromBaseVariant).forEach(projectVariants::add); 225 | } 226 | 227 | private JavaVersion getJavaVersion(Project project) { 228 | String sourceCompatibility = ""; 229 | String targetCompatibility = ""; 230 | 231 | Object extension = project.getExtensions().findByName("android"); 232 | if (extension instanceof BaseExtension) { 233 | try { 234 | BaseExtension baseExtension = (BaseExtension) extension; 235 | AndroidProjectCompileOptions compileOptions = AndroidProjectCompileOptions.fromBaseExtension( 236 | baseExtension); 237 | sourceCompatibility = compileOptions.getSourceCompatibility(); 238 | targetCompatibility = compileOptions.getTargetCompatibility(); 239 | } catch (Exception e) { 240 | logger.warn("Unable to determine Java source or target compatibility versions", e); 241 | } 242 | } 243 | return new JavaVersion(Tree.randomId(), 244 | System.getProperty("java.runtime.version"), 245 | System.getProperty("java.vm.vendor"), 246 | sourceCompatibility, 247 | targetCompatibility); 248 | } 249 | 250 | Charset getSourceFileEncoding(Project project, Charset defaultCharset) { 251 | Object extension = project.getExtensions().findByName("android"); 252 | if (extension instanceof BaseExtension) { 253 | try { 254 | BaseExtension baseExtension = (BaseExtension) extension; 255 | AndroidProjectCompileOptions compileOptions = AndroidProjectCompileOptions.fromBaseExtension( 256 | baseExtension); 257 | return compileOptions.getEncoding(); 258 | } catch (Exception e) { 259 | logger.warn("Unable to determine Java source file encoding", e); 260 | } 261 | } 262 | return defaultCharset; 263 | } 264 | 265 | private Stream parseJavaFiles(List javaPaths, 266 | ExecutionContext ctx, 267 | Path buildDir, 268 | Collection exclusions, 269 | Charset javaSourceCharset, 270 | JavaVersion javaVersion, 271 | Set dependencyPaths, 272 | JavaTypeCache javaTypeCache) { 273 | ParsingExecutionContextView.view(ctx).setCharset(javaSourceCharset); 274 | 275 | return Stream.of((Supplier) () -> JavaParser.fromJavaVersion() 276 | .classpath(dependencyPaths) 277 | .styles(styles) 278 | .typeCache(javaTypeCache) 279 | .logCompilationWarningsAndErrors(rewriteExtension.getLogCompilationWarningsAndErrors()) 280 | .build()).map(Supplier::get).flatMap(jp -> jp.parse(javaPaths, baseDir, ctx)).map(cu -> { 281 | if (DefaultProjectParser.isExcluded(exclusions, cu.getSourcePath()) || cu.getSourcePath() 282 | .startsWith(buildDir)) { 283 | return null; 284 | } 285 | return cu; 286 | }).filter(Objects::nonNull).map(it -> it.withMarkers(it.getMarkers().add(javaVersion))); 287 | } 288 | 289 | private Stream parseKotlinFiles(List kotlinPaths, 290 | ExecutionContext ctx, 291 | Path buildDir, 292 | Collection exclusions, 293 | Charset javaSourceCharset, 294 | JavaVersion javaVersion, 295 | Set dependencyPaths, 296 | JavaTypeCache javaTypeCache) { 297 | ParsingExecutionContextView.view(ctx).setCharset(javaSourceCharset); 298 | 299 | return Stream.of((Supplier) () -> KotlinParser.builder() 300 | .classpath(dependencyPaths) 301 | .styles(styles) 302 | .typeCache(javaTypeCache) 303 | .logCompilationWarningsAndErrors(rewriteExtension.getLogCompilationWarningsAndErrors()) 304 | .build()).map(Supplier::get).flatMap(kp -> kp.parse(kotlinPaths, baseDir, ctx)).map(cu -> { 305 | if (DefaultProjectParser.isExcluded(exclusions, cu.getSourcePath()) || cu.getSourcePath() 306 | .startsWith(buildDir)) { 307 | return null; 308 | } 309 | return cu; 310 | }).filter(Objects::nonNull).map(it -> it.withMarkers(it.getMarkers().add(javaVersion))); 311 | } 312 | } 313 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/openrewrite/gradle/isolated/AndroidProjectVariant.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.gradle.isolated; 17 | 18 | import com.android.build.gradle.api.BaseVariant; 19 | import com.android.builder.model.SourceProvider; 20 | import org.gradle.api.logging.Logger; 21 | import org.gradle.api.logging.Logging; 22 | 23 | import java.io.File; 24 | import java.nio.file.Path; 25 | import java.util.*; 26 | import java.util.stream.Collectors; 27 | 28 | class AndroidProjectVariant { 29 | private static final Logger logger = Logging.getLogger(AndroidProjectVariant.class); 30 | private final String name; 31 | private final Map> javaSourceSets; 32 | private final Map> kotlinSourceSets; 33 | private final Map> resourcesSourceSets; 34 | private final Set sourceSetNames = new HashSet<>(); 35 | private final Set compileClasspath; 36 | private final Set runtimeClasspath; 37 | 38 | AndroidProjectVariant(String name, 39 | Map> javaSourceSets, 40 | Map> kotlinSourceSets, 41 | Map> resourcesSourceSets, 42 | Set compileClasspath, 43 | Set runtimeClasspath) { 44 | this.name = name; 45 | this.javaSourceSets = javaSourceSets; 46 | this.kotlinSourceSets = kotlinSourceSets; 47 | this.resourcesSourceSets = resourcesSourceSets; 48 | this.compileClasspath = compileClasspath; 49 | this.runtimeClasspath = runtimeClasspath; 50 | 51 | sourceSetNames.addAll(javaSourceSets.keySet()); 52 | sourceSetNames.addAll(kotlinSourceSets.keySet()); 53 | sourceSetNames.addAll(this.resourcesSourceSets.keySet()); 54 | } 55 | 56 | String getName() { 57 | return name; 58 | } 59 | 60 | Set getSourceSetNames() { 61 | return sourceSetNames; 62 | } 63 | 64 | Set getJavaDirectories(String sourceSetName) { 65 | return javaSourceSets.computeIfAbsent(sourceSetName, key -> Collections.emptySet()); 66 | } 67 | 68 | Set getKotlinDirectories(String sourceSetName) { 69 | return kotlinSourceSets.computeIfAbsent(sourceSetName, key -> Collections.emptySet()); 70 | } 71 | 72 | Set getResourcesDirectories(String sourceSetName) { 73 | return resourcesSourceSets.computeIfAbsent(sourceSetName, key -> Collections.emptySet()); 74 | } 75 | 76 | Set getCompileClasspath() { 77 | return compileClasspath; 78 | } 79 | 80 | Set getRuntimeClasspath() { 81 | return runtimeClasspath; 82 | } 83 | 84 | static AndroidProjectVariant fromBaseVariant(BaseVariant baseVariant) { 85 | Map> javaSourceSets = new HashMap<>(); 86 | Map> kotlinSourceSets = new HashMap<>(); 87 | Map> resourceSourceSets = new HashMap<>(); 88 | 89 | for (SourceProvider sourceProvider : baseVariant.getSourceSets()) { 90 | addSourceSets(javaSourceSets, sourceProvider.getName(), sourceProvider.getJavaDirectories()); 91 | if (hasMethod(baseVariant, "getKotlinDirectories")) { 92 | // Android gradle plugin versions prior to 7 do not have BaseVariant#getKotlinDirectories 93 | addSourceSets(kotlinSourceSets, sourceProvider.getName(), sourceProvider.getKotlinDirectories()); 94 | } 95 | addSourceSets(resourceSourceSets, sourceProvider.getName(), sourceProvider.getResDirectories()); 96 | addSourceSets(resourceSourceSets, sourceProvider.getName(), sourceProvider.getResourcesDirectories()); 97 | } 98 | 99 | Set compileClasspath = new LinkedHashSet<>(); 100 | try { 101 | baseVariant.getCompileClasspath(null) 102 | .getFiles() 103 | .stream() 104 | .map(File::toPath) 105 | .forEach(compileClasspath::add); 106 | } catch (RuntimeException e) { 107 | // Calling BaseVariant#getCompileClasspath will throw an exception when run with 108 | // an AGP version less than 8.0 and a gradle version less than 8, when trying to 109 | // create a task using org.gradle.api.tasks.incremental.IncrementalTaskInputs which 110 | // was removed in gradle 8. 111 | logger.warn("Unable to determine compile class path", e); 112 | } 113 | 114 | Set runtimeClasspath = new LinkedHashSet<>(); 115 | 116 | try { 117 | baseVariant.getRuntimeConfiguration().getFiles() 118 | .stream() 119 | .map(File::toPath) 120 | .forEach(runtimeClasspath::add); 121 | } catch (Exception e) { 122 | logger.warn("Unable to determine runtime class path", e); 123 | } 124 | 125 | return new AndroidProjectVariant( 126 | baseVariant.getName(), 127 | javaSourceSets, 128 | kotlinSourceSets, 129 | resourceSourceSets, 130 | compileClasspath, 131 | runtimeClasspath); 132 | } 133 | 134 | private static void addSourceSets(Map> sourceSets, String name, Collection directories) { 135 | sourceSets.put(name, directories.stream().map(File::toPath).collect(Collectors.toSet())); 136 | } 137 | 138 | private static boolean hasMethod(BaseVariant baseVariant, String methodName, Class... paramTypes) { 139 | try { 140 | baseVariant.getClass().getMethod(methodName, paramTypes); 141 | return true; 142 | } catch (NoSuchMethodException e) { 143 | return false; 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/openrewrite/gradle/isolated/ResultsContainer.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.gradle.isolated; 17 | 18 | import org.jspecify.annotations.Nullable; 19 | import org.openrewrite.*; 20 | import org.openrewrite.marker.Marker; 21 | import org.openrewrite.marker.Markers; 22 | import org.openrewrite.marker.Markup; 23 | import org.openrewrite.marker.SearchResult; 24 | 25 | import java.io.IOException; 26 | import java.io.UncheckedIOException; 27 | import java.nio.file.Files; 28 | import java.nio.file.Path; 29 | import java.nio.file.Paths; 30 | import java.util.*; 31 | import java.util.function.UnaryOperator; 32 | import java.util.stream.Stream; 33 | 34 | public class ResultsContainer { 35 | final Path projectRoot; 36 | final RecipeRun recipeRun; 37 | final List generated = new ArrayList<>(); 38 | final List deleted = new ArrayList<>(); 39 | final List moved = new ArrayList<>(); 40 | final List refactoredInPlace = new ArrayList<>(); 41 | 42 | public ResultsContainer(Path projectRoot, @Nullable RecipeRun recipeRun) { 43 | this.projectRoot = projectRoot; 44 | this.recipeRun = recipeRun; 45 | if (recipeRun != null) { 46 | for (Result result : recipeRun.getChangeset().getAllResults()) { 47 | if (result.getBefore() == null && result.getAfter() == null) { 48 | // This situation shouldn't happen / makes no sense 49 | continue; 50 | } 51 | if (result.getBefore() == null && result.getAfter() != null) { 52 | generated.add(result); 53 | } else if (result.getBefore() != null && result.getAfter() == null) { 54 | deleted.add(result); 55 | } else if (result.getBefore() != null && !result.getBefore().getSourcePath().equals(result.getAfter().getSourcePath())) { 56 | moved.add(result); 57 | } else { 58 | if (!result.diff(Paths.get(""), new FencedMarkerPrinter(), true).isEmpty()) { 59 | refactoredInPlace.add(result); 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | /** 67 | * Only retains output for markers of type {@code SearchResult} and {@code Markup}. 68 | */ 69 | private static class FencedMarkerPrinter implements PrintOutputCapture.MarkerPrinter { 70 | @Override 71 | public String beforeSyntax(Marker marker, Cursor cursor, UnaryOperator commentWrapper) { 72 | return marker instanceof SearchResult || marker instanceof Markup ? "{{" + marker.getId() + "}}" : ""; 73 | } 74 | 75 | @Override 76 | public String afterSyntax(Marker marker, Cursor cursor, UnaryOperator commentWrapper) { 77 | return marker instanceof SearchResult || marker instanceof Markup ? "{{" + marker.getId() + "}}" : ""; 78 | } 79 | } 80 | 81 | public @Nullable RuntimeException getFirstException() { 82 | for (Result result : generated) { 83 | for (RuntimeException error : getRecipeErrors(result)) { 84 | return error; 85 | } 86 | } 87 | for (Result result : deleted) { 88 | for (RuntimeException error : getRecipeErrors(result)) { 89 | return error; 90 | } 91 | } 92 | for (Result result : moved) { 93 | for (RuntimeException error : getRecipeErrors(result)) { 94 | return error; 95 | } 96 | } 97 | for (Result result : refactoredInPlace) { 98 | for (RuntimeException error : getRecipeErrors(result)) { 99 | return error; 100 | } 101 | } 102 | return null; 103 | } 104 | 105 | private List getRecipeErrors(Result result) { 106 | List exceptions = new ArrayList<>(); 107 | new TreeVisitor() { 108 | @Override 109 | public Tree preVisit(Tree tree, Integer integer) { 110 | Markers markers = tree.getMarkers(); 111 | markers.findFirst(Markup.Error.class).ifPresent(e -> { 112 | Optional sourceFile = Optional.ofNullable(getCursor().firstEnclosing(SourceFile.class)); 113 | String sourcePath = sourceFile.map(SourceFile::getSourcePath).map(Path::toString).orElse(""); 114 | exceptions.add(new RuntimeException("Error while visiting " + sourcePath + ": " + e.getDetail())); 115 | }); 116 | return tree; 117 | } 118 | }.visit(result.getAfter(), 0); 119 | return exceptions; 120 | } 121 | 122 | public Path getProjectRoot() { 123 | return projectRoot; 124 | } 125 | 126 | public boolean isNotEmpty() { 127 | return !generated.isEmpty() || !deleted.isEmpty() || !moved.isEmpty() || !refactoredInPlace.isEmpty(); 128 | } 129 | 130 | /** 131 | * List directories that are empty as a result of applying recipe changes 132 | */ 133 | public List newlyEmptyDirectories() { 134 | Set maybeEmptyDirectories = new LinkedHashSet<>(); 135 | for (Result result : moved) { 136 | assert result.getBefore() != null; 137 | maybeEmptyDirectories.add(projectRoot.resolve(result.getBefore().getSourcePath()).getParent()); 138 | } 139 | for (Result result : deleted) { 140 | assert result.getBefore() != null; 141 | maybeEmptyDirectories.add(projectRoot.resolve(result.getBefore().getSourcePath()).getParent()); 142 | } 143 | if (maybeEmptyDirectories.isEmpty()) { 144 | return Collections.emptyList(); 145 | } 146 | List emptyDirectories = new ArrayList<>(maybeEmptyDirectories.size()); 147 | for (Path maybeEmptyDirectory : maybeEmptyDirectories) { 148 | try (Stream contents = Files.list(maybeEmptyDirectory)) { 149 | if (contents.findAny().isPresent()) { 150 | continue; 151 | } 152 | Files.delete(maybeEmptyDirectory); 153 | } catch (IOException e) { 154 | throw new UncheckedIOException(e); 155 | } 156 | } 157 | return emptyDirectories; 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/openrewrite/gradle/isolated/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 | package org.openrewrite.gradle.isolated; 18 | 19 | import org.jspecify.annotations.NullMarked; 20 | -------------------------------------------------------------------------------- /plugin/src/main/java/org/openrewrite/gradle/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 | @NonNullApi 17 | package org.openrewrite.gradle; 18 | 19 | import org.gradle.api.NonNullApi; 20 | -------------------------------------------------------------------------------- /plugin/src/main/kotlin/org/openrewrite/gradle/GradleProjectSpec.kt: -------------------------------------------------------------------------------- 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.gradle 17 | 18 | import org.gradle.util.GradleVersion 19 | import org.intellij.lang.annotations.Language 20 | import java.io.File 21 | import java.nio.charset.Charset 22 | import java.nio.charset.StandardCharsets 23 | import java.nio.file.Files 24 | 25 | /** 26 | * Utility to help with writing gradle projects to disk to assist with plugin testing 27 | */ 28 | class GradleProjectSpec( 29 | private val dir: File 30 | ) { 31 | private val subprojects: MutableList = mutableListOf() 32 | private val sourceSets: MutableList = mutableListOf() 33 | 34 | @Language("groovy") 35 | var groovyBuildScript: String? = null 36 | 37 | fun buildGradle(@Language("groovy") text: String) { 38 | groovyBuildScript = text.trimIndent() 39 | } 40 | 41 | var otherGradleScripts: MutableMap = mutableMapOf() 42 | fun otherGradleScript(name: String, @Language("groovy") text: String) { 43 | otherGradleScripts[name] = text.trimIndent() 44 | } 45 | 46 | @Language("groovy") 47 | var settingsGradle: String? = null 48 | fun settingsGradle(@Language("groovy") text: String) { 49 | settingsGradle = text.trimIndent() 50 | } 51 | 52 | @Language("yaml") 53 | var rewriteYaml: String? = null 54 | fun rewriteYaml(@Language("yaml") text: String) { 55 | rewriteYaml = text.trimIndent() 56 | } 57 | 58 | @Language("xml") 59 | var checkstyleXml: String? = null 60 | fun checkstyleXml(@Language("xml") text: String) { 61 | checkstyleXml = text.trimIndent() 62 | } 63 | 64 | private val propertiesFiles: MutableMap = mutableMapOf() 65 | fun propertiesFile(name: String, @Language("properties") text: String) { 66 | propertiesFiles[name] = text 67 | } 68 | 69 | private val textFiles: MutableMap = mutableMapOf() 70 | fun textFile(name: String, text: String) { 71 | textFiles[name] = text 72 | } 73 | 74 | fun subproject(name: String, init: GradleProjectSpec.()->Unit): GradleProjectSpec { 75 | val subproject = GradleProjectSpec(File(dir, name)).apply(init) 76 | subprojects.add(subproject) 77 | return subproject 78 | } 79 | 80 | fun sourceSet(name: String, 81 | sourceCharset: Charset = StandardCharsets.UTF_8, 82 | resourceCharset: Charset = StandardCharsets.UTF_8, 83 | init: GradleSourceSetSpec.()->Unit): GradleSourceSetSpec { 84 | val sourceSet = GradleSourceSetSpec(name, sourceCharset, resourceCharset).apply(init) 85 | sourceSets.add(sourceSet) 86 | return sourceSet 87 | } 88 | 89 | fun build(): GradleProjectSpec { 90 | Files.createDirectories(dir.toPath()) 91 | val settings = dir.toPath().resolve("settings.gradle") 92 | val lines = ArrayList() 93 | if (settingsGradle == null) { 94 | val gradleVersionString = System.getProperty("org.openrewrite.test.gradleVersion", "8.0") 95 | val gradleVersion = GradleVersion.version(gradleVersionString) 96 | if (gradleVersion > GradleVersion.version("5.0")) { 97 | lines.add( 98 | """ 99 | pluginManagement { 100 | repositories { 101 | gradlePluginPortal() 102 | mavenLocal() 103 | mavenCentral() 104 | google() 105 | // jcenter is currently only required for AGP 3.* 106 | jcenter() 107 | } 108 | } 109 | 110 | dependencyResolutionManagement { 111 | repositories { 112 | gradlePluginPortal() 113 | google() 114 | mavenLocal() 115 | mavenCentral() 116 | maven { 117 | url = uri("https://oss.sonatype.org/content/repositories/snapshots") 118 | } 119 | // jcenter is currently only required for AGP 3.* 120 | jcenter() 121 | } 122 | } 123 | """.trimIndent() 124 | ) 125 | } 126 | lines.add("rootProject.name = \"${dir.name}\"") 127 | if (!subprojects.isEmpty()) { 128 | subprojects.forEach {subproject -> lines.add("include('${subproject.dir.name}')")} 129 | } 130 | } else { 131 | lines.add(settingsGradle!!) 132 | } 133 | Files.write(settings, lines) 134 | 135 | if (groovyBuildScript != null) { 136 | File(dir, "build.gradle").writeText(groovyBuildScript!!) 137 | } 138 | 139 | for (otherGradleScript in otherGradleScripts) { 140 | File(dir, otherGradleScript.key).writeText(otherGradleScript.value) 141 | } 142 | 143 | if (rewriteYaml != null) { 144 | File(dir, "rewrite.yml").writeText(rewriteYaml!!) 145 | } 146 | 147 | if (checkstyleXml != null) { 148 | File(dir, "config/checkstyle/checkstyle.xml").apply{ 149 | parentFile.mkdirs() 150 | writeText(checkstyleXml!!) 151 | } 152 | } 153 | 154 | for (props in propertiesFiles.entries) { 155 | File(dir, props.key).apply{ 156 | parentFile.mkdirs() 157 | writeText(props.value) 158 | } 159 | } 160 | 161 | for (text in textFiles.entries) { 162 | File(dir, text.key).apply{ 163 | parentFile.mkdirs() 164 | writeText(text.value) 165 | } 166 | } 167 | 168 | for (sourceSet in sourceSets) { 169 | sourceSet.build(File(dir, "src")) 170 | } 171 | for (subproject in subprojects) { 172 | subproject.build() 173 | } 174 | return this 175 | } 176 | } 177 | 178 | class GradleSourceSetSpec( 179 | private val name: String, 180 | private val sourceCharset: Charset = StandardCharsets.UTF_8, 181 | private val resourceCharset: Charset = StandardCharsets.UTF_8 182 | ) { 183 | private val javaSources: MutableList = mutableListOf() 184 | fun java(@Language("java") source: String) { 185 | javaSources.add(source.trimIndent()) 186 | } 187 | 188 | private val kotlinSources: MutableList = mutableListOf() 189 | fun kotlin(@Language("kotlin") source: String) { 190 | kotlinSources.add(source.trimIndent()) 191 | } 192 | 193 | private val propertiesFiles: MutableMap = mutableMapOf() 194 | fun propertiesFile(name: String, @Language("properties") text: String) { 195 | propertiesFiles[name] = text 196 | } 197 | 198 | private val yamlFiles: MutableMap = mutableMapOf() 199 | fun yamlFile(name: String, @Language("yaml") text: String) { 200 | yamlFiles[name] = text 201 | } 202 | 203 | private val groovyClasses: MutableList = mutableListOf() 204 | fun groovyClass(@Language("groovy") source: String) { 205 | groovyClasses.add(source.trimIndent()) 206 | } 207 | 208 | 209 | @Suppress("RegExpSimplifiable") 210 | fun build(dir: File): GradleSourceSetSpec { 211 | dir.mkdirs() 212 | for (javaSource in javaSources) { 213 | val packageDecl = if (javaSource.startsWith("package")) { 214 | "package\\s+([a-zA-Z0-9.]+);".toRegex(RegexOption.MULTILINE) 215 | .find(javaSource)!! 216 | .groupValues[1] 217 | } else { 218 | "" 219 | }.replace(".", "/") 220 | val clazz = ".*(class|interface|enum)\\s+([a-zA-Z0-9-_]+)".toRegex(RegexOption.MULTILINE).find(javaSource)!!.groupValues[2] 221 | val path = if (packageDecl.isEmpty()) { 222 | "$name/java/$clazz.java" 223 | } else { 224 | "$name/java/$packageDecl/$clazz.java" 225 | } 226 | File(dir, path).apply{ 227 | parentFile.mkdirs() 228 | writeText(javaSource, sourceCharset) 229 | } 230 | } 231 | for (kotlinSource in kotlinSources) { 232 | val packageDecl = if (kotlinSource.startsWith("package")) { 233 | "package\\s+([a-zA-Z0-9.]+)".toRegex(RegexOption.MULTILINE) 234 | .find(kotlinSource)!! 235 | .groupValues[1] 236 | } else { 237 | "" 238 | }.replace(".", "/") 239 | val clazz = ".*(class|interface|enum)\\s+([a-zA-Z0-9-_]+)".toRegex(RegexOption.MULTILINE).find(kotlinSource)!!.groupValues[2] 240 | val path = if (packageDecl.isEmpty()) { 241 | "$name/kotlin/$clazz.kt" 242 | } else { 243 | "$name/kotlin/$packageDecl/$clazz.kt" 244 | } 245 | File(dir, path).apply{ 246 | parentFile.mkdirs() 247 | writeText(kotlinSource, sourceCharset) 248 | } 249 | } 250 | for (groovySource in groovyClasses) { 251 | val packageDecl = if (groovySource.startsWith("package")) { 252 | "package\\s+([a-zA-Z0-9.]+);?".toRegex(RegexOption.MULTILINE) 253 | .find(groovySource)!! 254 | .groupValues[1] 255 | } else { 256 | "" 257 | }.replace(".", "/") 258 | val clazz = ".*(class|interface|enum)\\s+([a-zA-Z0-9-_]+)".toRegex(RegexOption.MULTILINE).find(groovySource)!!.groupValues[2] 259 | val path = if (packageDecl.isEmpty()) { 260 | "$name/groovy/$clazz.groovy" 261 | } else { 262 | "$name/groovy/$packageDecl/$clazz.groovy" 263 | } 264 | File(dir, path).apply{ 265 | parentFile.mkdirs() 266 | writeText(groovySource, sourceCharset) 267 | } 268 | } 269 | if (propertiesFiles.isNotEmpty()) { 270 | for (props in propertiesFiles.entries) { 271 | File(dir, "$name/resources/${props.key}").apply{ 272 | parentFile.mkdirs() 273 | writeText(props.value, resourceCharset) 274 | } 275 | } 276 | } 277 | if (yamlFiles.isNotEmpty()) { 278 | for (yaml in yamlFiles.entries) { 279 | File(dir, "$name/resources/${yaml.key}").apply{ 280 | parentFile.mkdirs() 281 | writeText(yaml.value, resourceCharset) 282 | } 283 | } 284 | } 285 | return this 286 | } 287 | } 288 | 289 | fun gradleProject(dir: File, init: GradleProjectSpec.()->Unit): GradleProjectSpec { 290 | return GradleProjectSpec(dir).apply(init).build() 291 | } 292 | 293 | fun commitFilesToGitRepo(dir: File) { 294 | exec("git init", dir) 295 | exec("git config user.email user@test.com", dir) 296 | exec("git config user.name TestUser", dir) 297 | exec("git add .", dir) 298 | exec("git commit -m \"Initial commit\"", dir) 299 | } 300 | 301 | private fun exec(command: String, workingDirectory: File) { 302 | Runtime.getRuntime().exec(command, null, workingDirectory) 303 | } 304 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/org/openrewrite/gradle/GradleRunnerTest.kt: -------------------------------------------------------------------------------- 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.gradle 17 | 18 | import org.gradle.testkit.runner.BuildResult 19 | import org.gradle.testkit.runner.GradleRunner 20 | import org.gradle.util.GradleVersion 21 | import java.io.File 22 | import java.lang.management.ManagementFactory 23 | 24 | val gradleVersion: String? = System.getProperty("org.openrewrite.test.gradleVersion") 25 | 26 | interface GradleRunnerTest { 27 | 28 | fun runGradle(testDir: File, vararg args: String): BuildResult { 29 | return GradleRunner.create() 30 | .withDebug(ManagementFactory.getRuntimeMXBean().inputArguments.toString().indexOf("-agentlib:jdwp") > 0) 31 | .withProjectDir(testDir) 32 | .apply { 33 | if (gradleVersion != null) { 34 | withGradleVersion(gradleVersion) 35 | } 36 | } 37 | .withArguments(*args, "--info", "--stacktrace") 38 | .withPluginClasspath() 39 | .forwardOutput() 40 | .build() 41 | } 42 | 43 | fun lessThanGradle6_1(): Boolean { 44 | val currentVersion = if (gradleVersion == null) GradleVersion.current() else GradleVersion.version(gradleVersion) 45 | return currentVersion < GradleVersion.version("6.1") 46 | } 47 | 48 | fun lessThanGradle6_8(): Boolean { 49 | val currentVersion = if (gradleVersion == null) GradleVersion.current() else GradleVersion.version(gradleVersion) 50 | return currentVersion < GradleVersion.version("6.8") 51 | } 52 | 53 | fun lessThanGradle7_4(): Boolean { 54 | val currentVersion = if (gradleVersion == null) GradleVersion.current() else GradleVersion.version(gradleVersion) 55 | return currentVersion < GradleVersion.version("7.4") 56 | } 57 | 58 | fun isAgp3CompatibleGradleVersion(): Boolean { 59 | val currentVersion = if (gradleVersion == null) GradleVersion.current() else GradleVersion.version(gradleVersion) 60 | return System.getenv("ANDROID_HOME") != null && 61 | currentVersion >= GradleVersion.version("5.0") && 62 | currentVersion < GradleVersion.version("8.0") 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/org/openrewrite/gradle/RewriteDiscoverTest.kt: -------------------------------------------------------------------------------- 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.gradle 17 | 18 | import org.assertj.core.api.Assertions.assertThat 19 | import org.gradle.testkit.runner.TaskOutcome 20 | import org.junit.jupiter.api.Test 21 | import org.junit.jupiter.api.condition.DisabledIf 22 | import org.junit.jupiter.api.io.TempDir 23 | import org.openrewrite.Issue 24 | import java.io.File 25 | 26 | class RewriteDiscoverTest : RewritePluginTest { 27 | 28 | override fun taskName(): String = "rewriteDiscover" 29 | 30 | @Issue("https://github.com/openrewrite/rewrite-gradle-plugin/issues/33") 31 | @Test 32 | fun `rewriteDiscover prints recipes from external dependencies`( 33 | @TempDir projectDir: File 34 | ) { 35 | gradleProject(projectDir) { 36 | buildGradle(""" 37 | plugins { 38 | id("java") 39 | id("org.openrewrite.rewrite") 40 | } 41 | 42 | repositories { 43 | mavenLocal() 44 | mavenCentral() 45 | maven { 46 | url = uri("https://oss.sonatype.org/content/repositories/snapshots") 47 | } 48 | } 49 | 50 | dependencies { 51 | rewrite("org.openrewrite.recipe:rewrite-testing-frameworks:latest.release") 52 | } 53 | 54 | rewrite { 55 | activeRecipe("org.openrewrite.java.testing.junit5.JUnit5BestPractices") 56 | activeRecipe("org.openrewrite.java.format.AutoFormat") 57 | activeStyle("org.openrewrite.java.SpringFormat") 58 | } 59 | """) 60 | 61 | } 62 | val result = runGradle(projectDir, taskName()) 63 | val rewriteDiscoverResult = result.task(":${taskName()}")!! 64 | assertThat(rewriteDiscoverResult.outcome).isEqualTo(TaskOutcome.SUCCESS) 65 | 66 | assertThat(result.output).contains("Configured with 2 active recipes and 1 active styles.") 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/org/openrewrite/gradle/RewriteDryRunSourceSetTest.kt: -------------------------------------------------------------------------------- 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.gradle 17 | 18 | import org.assertj.core.api.Assertions.assertThat 19 | import org.gradle.testkit.runner.BuildResult 20 | import org.gradle.testkit.runner.TaskOutcome 21 | import org.junit.jupiter.api.Test 22 | import org.junit.jupiter.api.io.TempDir 23 | import org.openrewrite.gradle.fixtures.GradleFixtures.Companion.REWRITE_BUILD_GRADLE 24 | import org.openrewrite.gradle.fixtures.JavaFixtures.Companion.GOODBYE_WORLD_JAVA_CLASS 25 | import org.openrewrite.gradle.fixtures.JavaFixtures.Companion.GOODBYE_WORLD_JAVA_INTERFACE 26 | import org.openrewrite.gradle.fixtures.JavaFixtures.Companion.HELLO_WORLD_JAVA_CLASS 27 | import org.openrewrite.gradle.fixtures.JavaFixtures.Companion.HELLO_WORLD_JAVA_INTERFACE 28 | import java.io.File 29 | 30 | class RewriteDryRunSourceSetTest : GradleRunnerTest { 31 | @TempDir 32 | lateinit var projectDir: File 33 | 34 | companion object { 35 | const val TASK_NAME = "rewriteDryRun" 36 | const val DEFAULT_SRC_DIR = "src/main/java" 37 | const val DEFAULT_RESOURCES_DIR = "src/main/resources" 38 | const val CUSTOM_SRC_DIR = "src" 39 | const val CUSTOM_RESOURCES_DIR = "resources" 40 | 41 | //language=yaml 42 | const val REWRITE_YAML = 43 | """ 44 | type: specs.openrewrite.org/v1beta/recipe 45 | name: org.openrewrite.gradle.ChangePackage 46 | description: Test. 47 | recipeList: 48 | - org.openrewrite.java.ChangePackage: 49 | oldPackageName: org.openrewrite.before 50 | newPackageName: org.openrewrite.after 51 | """ 52 | 53 | //language=groovy 54 | const val REWRITE_PLUGIN_ACTIVE_RECIPE = """ 55 | rewrite { 56 | activeRecipe("org.openrewrite.gradle.ChangePackage", "org.openrewrite.java.format.AutoFormat") 57 | } 58 | """ 59 | } 60 | 61 | @Test 62 | fun `can run a recipe for default source set directories`() { 63 | gradleProject(projectDir) { 64 | rewriteYaml(REWRITE_YAML) 65 | buildGradle( 66 | REWRITE_BUILD_GRADLE + 67 | REWRITE_PLUGIN_ACTIVE_RECIPE 68 | ) 69 | 70 | sourceSet("main") { 71 | java(HELLO_WORLD_JAVA_INTERFACE) 72 | java(HELLO_WORLD_JAVA_CLASS) 73 | yamlFile( 74 | "foo.yml", "foo: bar" 75 | ) 76 | } 77 | } 78 | 79 | val result = runGradle(projectDir, TASK_NAME) 80 | 81 | assertDryRunTaskOutcome(result) 82 | val patchFile = assertPatchFile() 83 | assertHelloWorldInPatchFileContents(patchFile, DEFAULT_SRC_DIR) 84 | } 85 | 86 | @Test 87 | fun `can run a recipe for custom Java source set directory with default directories`() { 88 | gradleProject(projectDir) { 89 | rewriteYaml(REWRITE_YAML) 90 | buildGradle( 91 | REWRITE_BUILD_GRADLE + 92 | REWRITE_PLUGIN_ACTIVE_RECIPE + 93 | //language=groovy 94 | """ 95 | sourceSets { 96 | main { 97 | java { 98 | srcDirs = ["src"] 99 | } 100 | } 101 | } 102 | """ 103 | ) 104 | 105 | createHelloWorldJavaInterfaceFile(CUSTOM_SRC_DIR) 106 | createHelloWorldJavaClassFile(CUSTOM_SRC_DIR) 107 | createYamlFile(DEFAULT_RESOURCES_DIR) 108 | } 109 | 110 | val result = runGradle(projectDir, TASK_NAME) 111 | 112 | assertDryRunTaskOutcome(result) 113 | val patchFile = assertPatchFile() 114 | assertHelloWorldInPatchFileContents(patchFile, CUSTOM_SRC_DIR) 115 | } 116 | 117 | @Test 118 | fun `can run a recipe for non-overlapping custom source set directories`() { 119 | gradleProject(projectDir) { 120 | rewriteYaml(REWRITE_YAML) 121 | buildGradle( 122 | REWRITE_BUILD_GRADLE + 123 | REWRITE_PLUGIN_ACTIVE_RECIPE + 124 | //language=groovy 125 | """ 126 | sourceSets { 127 | main { 128 | java { 129 | srcDirs = ["src"] 130 | } 131 | resources { 132 | srcDirs = ["resources"] 133 | } 134 | } 135 | } 136 | """ 137 | ) 138 | 139 | createHelloWorldJavaInterfaceFile(CUSTOM_SRC_DIR) 140 | createHelloWorldJavaClassFile(CUSTOM_SRC_DIR) 141 | createYamlFile(CUSTOM_RESOURCES_DIR) 142 | } 143 | 144 | val result = runGradle(projectDir, TASK_NAME) 145 | 146 | assertDryRunTaskOutcome(result) 147 | val patchFile = assertPatchFile() 148 | assertHelloWorldInPatchFileContents(patchFile, CUSTOM_SRC_DIR) 149 | } 150 | 151 | @Test 152 | fun `can run a recipe for overlapping custom source set directories`() { 153 | gradleProject(projectDir) { 154 | rewriteYaml(REWRITE_YAML) 155 | buildGradle( 156 | REWRITE_BUILD_GRADLE + 157 | REWRITE_PLUGIN_ACTIVE_RECIPE + 158 | //language=groovy 159 | """ 160 | sourceSets { 161 | main { 162 | java { 163 | srcDirs = ["src"] 164 | } 165 | resources { 166 | srcDirs = ["src"] 167 | excludes = ["**/*.java"] 168 | } 169 | } 170 | } 171 | """ 172 | ) 173 | 174 | createHelloWorldJavaInterfaceFile(CUSTOM_SRC_DIR) 175 | createHelloWorldJavaClassFile(CUSTOM_SRC_DIR) 176 | createYamlFile(CUSTOM_SRC_DIR) 177 | } 178 | 179 | val result = runGradle(projectDir, TASK_NAME) 180 | 181 | assertDryRunTaskOutcome(result) 182 | val patchFile = assertPatchFile() 183 | assertHelloWorldInPatchFileContents(patchFile, CUSTOM_SRC_DIR) 184 | } 185 | 186 | @Test 187 | fun `can run a recipe for custom source set directories added to default directories`() { 188 | gradleProject(projectDir) { 189 | rewriteYaml(REWRITE_YAML) 190 | buildGradle( 191 | REWRITE_BUILD_GRADLE + 192 | REWRITE_PLUGIN_ACTIVE_RECIPE + 193 | //language=groovy 194 | """ 195 | sourceSets { 196 | main { 197 | java { 198 | srcDir "src" 199 | } 200 | resources { 201 | srcDir "src" 202 | excludes = ["**/*.java"] 203 | } 204 | } 205 | } 206 | """ 207 | ) 208 | 209 | createHelloWorldJavaInterfaceFile(CUSTOM_SRC_DIR) 210 | createHelloWorldJavaClassFile(CUSTOM_SRC_DIR) 211 | createYamlFile(CUSTOM_SRC_DIR) 212 | } 213 | 214 | val result = runGradle(projectDir, TASK_NAME) 215 | 216 | assertDryRunTaskOutcome(result) 217 | val patchFile = assertPatchFile() 218 | assertHelloWorldInPatchFileContents(patchFile, CUSTOM_SRC_DIR) 219 | } 220 | 221 | @Test 222 | fun `can run a recipe for overlapping source sets`() { 223 | gradleProject(projectDir) { 224 | rewriteYaml(REWRITE_YAML) 225 | buildGradle( 226 | REWRITE_BUILD_GRADLE + 227 | REWRITE_PLUGIN_ACTIVE_RECIPE + 228 | //language=groovy 229 | """ 230 | sourceSets { 231 | main { 232 | java { 233 | srcDirs = ["src"] 234 | } 235 | } 236 | other { 237 | java { 238 | srcDirs = ["src"] 239 | } 240 | } 241 | } 242 | """ 243 | ) 244 | 245 | createHelloWorldJavaInterfaceFile(CUSTOM_SRC_DIR) 246 | createHelloWorldJavaClassFile(CUSTOM_SRC_DIR) 247 | createGoodbyeWorldJavaInterfaceFile(CUSTOM_SRC_DIR) 248 | createGoodbyeWorldJavaClassFile(CUSTOM_SRC_DIR) 249 | } 250 | 251 | val result = runGradle(projectDir, TASK_NAME) 252 | 253 | assertDryRunTaskOutcome(result) 254 | val compileOtherJavaResult = result.task(":compileOtherJava")!! 255 | assertThat(compileOtherJavaResult.outcome).isEqualTo(TaskOutcome.SKIPPED) 256 | val patchFile = assertPatchFile() 257 | assertHelloWorldInPatchFileContents(patchFile, CUSTOM_SRC_DIR) 258 | assertGoodbyeWorldInPatchFileContents(patchFile, CUSTOM_SRC_DIR) 259 | } 260 | 261 | private fun createHelloWorldJavaInterfaceFile(sourceDir: String) { 262 | createJavaFile(projectDir, sourceDir, "HelloWorld.java", HELLO_WORLD_JAVA_INTERFACE) 263 | } 264 | 265 | private fun createHelloWorldJavaClassFile(sourceDir: String) { 266 | createJavaFile(projectDir, sourceDir, "HelloWorldImpl.java", HELLO_WORLD_JAVA_CLASS) 267 | } 268 | 269 | private fun createGoodbyeWorldJavaInterfaceFile(sourceDir: String) { 270 | createJavaFile(projectDir, sourceDir, "GoodbyeWorld.java", GOODBYE_WORLD_JAVA_INTERFACE) 271 | } 272 | 273 | private fun createGoodbyeWorldJavaClassFile(sourceDir: String) { 274 | createJavaFile(projectDir, sourceDir, "GoodbyeWorldImpl.java", GOODBYE_WORLD_JAVA_CLASS) 275 | } 276 | 277 | private fun createJavaFile(projectDir: File, sourceDir: String, fileName: String, content: String) { 278 | val dir = File(projectDir, sourceDir) 279 | dir.mkdirs() 280 | val packageDir = File(dir, "org/openrewrite/before") 281 | packageDir.mkdirs() 282 | File(packageDir, fileName).writeText(content) 283 | } 284 | 285 | private fun createYamlFile(sourceDir: String) { 286 | val dir = File(projectDir, sourceDir) 287 | dir.mkdirs() 288 | File(dir, "foo.yml").writeText("foo: test") 289 | } 290 | 291 | private fun assertDryRunTaskOutcome(result: BuildResult) { 292 | val rewriteDryRunResult = result.task(":${TASK_NAME}")!! 293 | assertThat(rewriteDryRunResult.outcome).isEqualTo(TaskOutcome.SUCCESS) 294 | } 295 | 296 | private fun assertPatchFile(): File { 297 | val patchFile = File(projectDir, "build/reports/rewrite/rewrite.patch") 298 | assertThat(patchFile.exists()).isTrue 299 | return patchFile 300 | } 301 | 302 | private fun assertHelloWorldInPatchFileContents(patchFile: File, javaSourceDir: String) { 303 | assertThat(patchFile.readText()).contains( 304 | "rename from ${javaSourceDir}/org/openrewrite/before/HelloWorld.java", 305 | "rename to ${javaSourceDir}/org/openrewrite/after/HelloWorld.java", 306 | "rename from ${javaSourceDir}/org/openrewrite/before/HelloWorldImpl.java", 307 | "rename to ${javaSourceDir}/org/openrewrite/after/HelloWorldImpl.java" 308 | ) 309 | } 310 | 311 | private fun assertGoodbyeWorldInPatchFileContents(patchFile: File, javaSourceDir: String) { 312 | assertThat(patchFile.readText()).contains( 313 | "rename from ${javaSourceDir}/org/openrewrite/before/GoodbyeWorld.java", 314 | "rename to ${javaSourceDir}/org/openrewrite/after/GoodbyeWorld.java", 315 | "rename from ${javaSourceDir}/org/openrewrite/before/GoodbyeWorldImpl.java", 316 | "rename to ${javaSourceDir}/org/openrewrite/after/GoodbyeWorldImpl.java" 317 | ) 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/org/openrewrite/gradle/RewriteDryRunTest.kt: -------------------------------------------------------------------------------- 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.gradle 17 | 18 | import org.assertj.core.api.Assertions.assertThat 19 | import org.gradle.testkit.runner.TaskOutcome 20 | import org.junit.jupiter.api.Test 21 | import org.junit.jupiter.api.condition.DisabledIf 22 | import org.junit.jupiter.api.condition.EnabledIf 23 | import org.junit.jupiter.api.io.TempDir 24 | import org.junit.jupiter.params.ParameterizedTest 25 | import org.junit.jupiter.params.provider.ValueSource 26 | import org.openrewrite.Issue 27 | import java.io.File 28 | 29 | @Suppress("GroovyUnusedAssignment") 30 | class RewriteDryRunTest : RewritePluginTest { 31 | @TempDir 32 | lateinit var projectDir: File 33 | 34 | override fun taskName(): String = "rewriteDryRun" 35 | 36 | @Test 37 | fun `rewriteDryRun runs successfully without modifying source files`() { 38 | //language=java 39 | val helloWorld = """ 40 | package org.openrewrite.before; 41 | 42 | public class HelloWorld { public static void sayGoodbye() {System.out.println("Hello world"); 43 | }public static void main(String[] args) { sayGoodbye(); } 44 | } 45 | """.trimIndent() 46 | gradleProject(projectDir) { 47 | rewriteYaml( 48 | """ 49 | type: specs.openrewrite.org/v1beta/recipe 50 | name: org.openrewrite.gradle.SayHello 51 | description: Test. 52 | recipeList: 53 | - org.openrewrite.java.ChangeMethodName: 54 | methodPattern: org.openrewrite.before.HelloWorld sayGoodbye() 55 | newMethodName: sayHello 56 | - org.openrewrite.java.ChangePackage: 57 | oldPackageName: org.openrewrite.before 58 | newPackageName: org.openrewrite.after 59 | """ 60 | ) 61 | buildGradle( 62 | """ 63 | plugins { 64 | id("java") 65 | id("org.openrewrite.rewrite") 66 | } 67 | 68 | repositories { 69 | mavenLocal() 70 | mavenCentral() 71 | maven { 72 | url = uri("https://oss.sonatype.org/content/repositories/snapshots") 73 | } 74 | } 75 | 76 | rewrite { 77 | activeRecipe("org.openrewrite.gradle.SayHello", "org.openrewrite.java.format.AutoFormat") 78 | } 79 | """ 80 | ) 81 | sourceSet("main") { 82 | java(helloWorld) 83 | } 84 | } 85 | val result = runGradle(projectDir, taskName()) 86 | val rewriteDryRunResult = result.task(":${taskName()}")!! 87 | assertThat(rewriteDryRunResult.outcome).isEqualTo(TaskOutcome.SUCCESS) 88 | 89 | assertThat( 90 | File(projectDir, "src/main/java/org/openrewrite/before/HelloWorld.java").readText() 91 | ).isEqualTo(helloWorld) 92 | assertThat(File(projectDir, "build/reports/rewrite/rewrite.patch").exists()).isTrue 93 | } 94 | 95 | @Test 96 | fun `A recipe with optional configuration can be activated directly`() { 97 | gradleProject(projectDir) { 98 | buildGradle( 99 | """ 100 | plugins { 101 | id("java") 102 | id("org.openrewrite.rewrite") 103 | } 104 | 105 | repositories { 106 | mavenLocal() 107 | mavenCentral() 108 | maven { 109 | url = uri("https://oss.sonatype.org/content/repositories/snapshots") 110 | } 111 | } 112 | """ 113 | ) 114 | sourceSet("main") { 115 | java( 116 | """ 117 | package org.openrewrite.before; 118 | 119 | import java.util.List; 120 | import java.util.ArrayList; 121 | 122 | public class HelloWorld { 123 | 124 | public static void main(String[] args) { 125 | System.out.println("Hello world"); 126 | } 127 | } 128 | """ 129 | ) 130 | } 131 | } 132 | 133 | val result = runGradle(projectDir, taskName(), "-DactiveRecipe=org.openrewrite.java.OrderImports") 134 | val rewriteDryRunResult = result.task(":${taskName()}")!! 135 | assertThat(rewriteDryRunResult.outcome).isEqualTo(TaskOutcome.SUCCESS) 136 | assertThat(File(projectDir, "build/reports/rewrite/rewrite.patch").exists()).isTrue 137 | } 138 | 139 | @DisabledIf("lessThanGradle6_1") 140 | @Test 141 | fun multiplatform() { 142 | gradleProject(projectDir) { 143 | buildGradle( 144 | """ 145 | plugins { 146 | id("java") 147 | id("org.openrewrite.rewrite") 148 | id("org.jetbrains.kotlin.multiplatform") version "1.8.0" 149 | } 150 | group = "org.example" 151 | version = "1.0-SNAPSHOT" 152 | 153 | repositories { 154 | mavenLocal() 155 | mavenCentral() 156 | maven { 157 | url = uri("https://oss.sonatype.org/content/repositories/snapshots") 158 | } 159 | } 160 | 161 | kotlin { 162 | jvm { 163 | jvmToolchain(8) 164 | withJava() 165 | } 166 | 167 | sourceSets { 168 | commonMain { 169 | dependencies { 170 | } 171 | } 172 | commonTest { 173 | dependencies { 174 | } 175 | } 176 | jvmMain { 177 | } 178 | jvmTest { 179 | } 180 | } 181 | } 182 | """ 183 | ) 184 | settingsGradle( 185 | """ 186 | pluginManagement { 187 | repositories { 188 | mavenLocal() 189 | maven { url = uri("https://plugins.gradle.org/m2") } 190 | gradlePluginPortal() 191 | } 192 | } 193 | rootProject.name = "example" 194 | """ 195 | ) 196 | sourceSet("commonMain") { 197 | kotlin( 198 | """ 199 | class HelloWorld { 200 | fun sayHello() { 201 | println("Hello world") 202 | } 203 | } 204 | """ 205 | ) 206 | } 207 | } 208 | val result = runGradle(projectDir, taskName(), "-DactiveRecipe=org.openrewrite.kotlin.FindKotlinSources") 209 | val rewriteDryRunResult = result.task(":${taskName()}")!! 210 | 211 | assertThat(rewriteDryRunResult.outcome).isEqualTo(TaskOutcome.SUCCESS) 212 | assertThat(File(projectDir, "build/reports/rewrite/rewrite.patch").exists()).isTrue 213 | } 214 | 215 | // The configuration cache works on Gradle 6.6+, but rewrite-gradle-plugin uses notCompatibleWithConfigurationCache, 216 | // which is only available on Gradle 7.4+. 217 | @DisabledIf("lessThanGradle7_4") 218 | @Issue("https://github.com/openrewrite/rewrite-gradle-plugin/issues/227") 219 | @Test 220 | fun `rewriteDryRun is compatible with the configuration cache`() { 221 | gradleProject(projectDir) { 222 | buildGradle( 223 | """ 224 | plugins { 225 | id("org.openrewrite.rewrite") 226 | } 227 | 228 | repositories { 229 | mavenLocal() 230 | mavenCentral() 231 | maven { 232 | url = uri("https://oss.sonatype.org/content/repositories/snapshots") 233 | } 234 | } 235 | """ 236 | ) 237 | } 238 | val result = runGradle(projectDir, taskName(), "--configuration-cache") 239 | val rewriteDryRunResult = result.task(":${taskName()}")!! 240 | assertThat(rewriteDryRunResult.outcome).isEqualTo(TaskOutcome.SUCCESS) 241 | } 242 | 243 | @ParameterizedTest 244 | @ValueSource(strings = ["8.6.0", "7.0.4", "4.2.2"]) 245 | fun `rewriteDryRun is compatible with AGP version 4 and over`(pluginVersion: String) { 246 | if (lessThanGradle6_1()) { 247 | // @DisabledIf doesn't seem to work with @ParameterizedTest 248 | return 249 | } 250 | gradleProject(projectDir) { 251 | buildGradle( 252 | """ 253 | plugins { 254 | id("com.android.application") version "$pluginVersion" 255 | id("org.openrewrite.rewrite") 256 | } 257 | 258 | group = "org.example" 259 | version = "1.0-SNAPSHOT" 260 | 261 | android { 262 | namespace = "example" 263 | compileSdkVersion 30 264 | } 265 | 266 | dependencies { 267 | implementation("com.google.guava:guava:33.3.0-android") 268 | } 269 | """ 270 | ) 271 | sourceSet("main") { 272 | java( 273 | """ 274 | import java.util.List; 275 | import java.util.Collections; 276 | 277 | class HelloWorld { 278 | HelloWorld() { 279 | super(); 280 | } 281 | } 282 | """ 283 | ) 284 | } 285 | } 286 | val result = runGradle(projectDir, taskName(), "-DactiveRecipe=org.openrewrite.java.OrderImports") 287 | val rewriteDryRunResult = result.task(":${taskName()}")!! 288 | 289 | assertThat(rewriteDryRunResult.outcome).isEqualTo(TaskOutcome.SUCCESS) 290 | val patchFile = File(projectDir, "build/reports/rewrite/rewrite.patch") 291 | assertThat(patchFile).exists() 292 | assertThat(patchFile.readText().trim()) 293 | .containsOnlyOnce( 294 | """ 295 | -import java.util.List; 296 | import java.util.Collections; 297 | +import java.util.List; 298 | """.trimIndent() 299 | ) 300 | 301 | } 302 | 303 | @EnabledIf("isAgp3CompatibleGradleVersion") 304 | @Test 305 | fun `rewriteDryRun is compatible with AGP version 3`() { 306 | gradleProject(projectDir) { 307 | buildGradle( 308 | """ 309 | buildscript { 310 | dependencies { 311 | classpath 'com.android.tools.build:gradle:3.4.0' 312 | } 313 | } 314 | 315 | plugins { 316 | id("org.openrewrite.rewrite") 317 | } 318 | 319 | apply plugin: 'com.android.application' 320 | 321 | group = "org.example" 322 | version = "1.0-SNAPSHOT" 323 | 324 | android { 325 | compileSdkVersion 28 326 | } 327 | """ 328 | ) 329 | sourceSet("main") { 330 | java( 331 | """ 332 | import java.util.List; 333 | import java.util.Collections; 334 | 335 | class HelloWorld { 336 | HelloWorld() { 337 | super(); 338 | } 339 | } 340 | """ 341 | ) 342 | } 343 | } 344 | val result = runGradle(projectDir, taskName(), "-DactiveRecipe=org.openrewrite.java.OrderImports") 345 | val rewriteDryRunResult = result.task(":${taskName()}")!! 346 | 347 | assertThat(rewriteDryRunResult.outcome).isEqualTo(TaskOutcome.SUCCESS) 348 | assertThat(File(projectDir, "build/reports/rewrite/rewrite.patch")).exists() 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/org/openrewrite/gradle/RewritePluginTest.kt: -------------------------------------------------------------------------------- 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.gradle 17 | 18 | import org.assertj.core.api.Assertions.assertThat 19 | import org.gradle.testkit.runner.TaskOutcome 20 | import org.junit.jupiter.api.Test 21 | import org.junit.jupiter.api.condition.DisabledIf 22 | import org.junit.jupiter.api.io.TempDir 23 | import org.openrewrite.Issue 24 | import java.io.File 25 | 26 | interface RewritePluginTest: GradleRunnerTest { 27 | 28 | fun taskName(): String 29 | 30 | // The configuration cache works on Gradle 6.6+, but rewrite-gradle-plugin uses notCompatibleWithConfigurationCache, 31 | // which is only available on Gradle 7.4+. 32 | @DisabledIf("lessThanGradle7_4") 33 | @Issue("https://github.com/openrewrite/rewrite-gradle-plugin/issues/227") 34 | @Test 35 | fun `task is compatible with the configuration cache`( 36 | @TempDir projectDir: File 37 | ) { 38 | gradleProject(projectDir) { 39 | buildGradle( 40 | """ 41 | plugins { 42 | id("org.openrewrite.rewrite") 43 | } 44 | 45 | repositories { 46 | mavenLocal() 47 | mavenCentral() 48 | maven { 49 | url = uri("https://oss.sonatype.org/content/repositories/snapshots") 50 | } 51 | } 52 | """ 53 | ) 54 | } 55 | val result = runGradle(projectDir, taskName(), "--configuration-cache") 56 | val taskResult = result.task(":${taskName()}")!! 57 | assertThat(taskResult.outcome).isEqualTo(TaskOutcome.SUCCESS) 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/org/openrewrite/gradle/fixtures/GradleFixtures.kt: -------------------------------------------------------------------------------- 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.gradle.fixtures 17 | 18 | class GradleFixtures { 19 | companion object { 20 | //language=groovy 21 | const val REPOSITORIES = """ 22 | repositories { 23 | mavenLocal() 24 | mavenCentral() 25 | maven { 26 | url = uri("https://oss.sonatype.org/content/repositories/snapshots") 27 | } 28 | } 29 | """ 30 | 31 | //language=groovy 32 | const val REWRITE_BUILD_GRADLE = """ 33 | plugins { 34 | id("java") 35 | id("org.openrewrite.rewrite") 36 | } 37 | """ + REPOSITORIES 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /plugin/src/test/kotlin/org/openrewrite/gradle/fixtures/JavaFixtures.kt: -------------------------------------------------------------------------------- 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.gradle.fixtures 17 | 18 | class JavaFixtures { 19 | companion object { 20 | //language=java 21 | val HELLO_WORLD_JAVA_INTERFACE = """ 22 | package org.openrewrite.before; 23 | 24 | public interface HelloWorld { 25 | void sayHello(); 26 | } 27 | """.trimIndent() 28 | 29 | //language=java 30 | val HELLO_WORLD_JAVA_CLASS = """ 31 | package org.openrewrite.before; 32 | 33 | public class HelloWorldImpl implements HelloWorld { 34 | @Override 35 | public void sayHello() { 36 | System.out.println("Hello world"); 37 | } 38 | } 39 | """.trimIndent() 40 | 41 | //language=java 42 | val GOODBYE_WORLD_JAVA_INTERFACE = """ 43 | package org.openrewrite.before; 44 | 45 | public interface GoodbyeWorld { 46 | void sayGoodbye(); 47 | } 48 | """.trimIndent() 49 | 50 | //language=java 51 | val GOODBYE_WORLD_JAVA_CLASS = """ 52 | package org.openrewrite.before; 53 | 54 | public class GoodbyeWorldImpl implements GoodbyeWorld { 55 | @Override 56 | public void sayGoodbye() { 57 | System.out.println("Goodbye world"); 58 | } 59 | } 60 | """.trimIndent() 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /plugin/src/test/samples/resourceParserTest/root/project/file2.txt: -------------------------------------------------------------------------------- 1 | hello -------------------------------------------------------------------------------- /plugin/src/test/samples/resourceParserTest/root/project/subproject/file.txt: -------------------------------------------------------------------------------- 1 | bye -------------------------------------------------------------------------------- /scripts/spring-petclinic-test.init.gradle: -------------------------------------------------------------------------------- 1 | initscript{ 2 | repositories{ 3 | repositories{ 4 | mavenLocal() 5 | maven{ 6 | url = uri("https://oss.sonatype.org/content/repositories/snapshots") 7 | } 8 | mavenCentral() 9 | } 10 | } 11 | dependencies{ 12 | classpath("org.openrewrite:plugin:latest.integration") 13 | } 14 | } 15 | 16 | projectsEvaluated{ 17 | rootProject{ 18 | applyplugin: org.openrewrite.gradle.RewritePlugin 19 | 20 | repositories{ 21 | mavenLocal() 22 | maven{ 23 | url = uri("https://oss.sonatype.org/content/repositories/snapshots") 24 | } 25 | mavenCentral() 26 | } 27 | 28 | rewrite{ 29 | failOnInvalidActiveRecipes = true 30 | activeRecipe("org.openrewrite.java.spring.boot2.SpringBoot1To2Migration") 31 | } 32 | 33 | dependencies{ 34 | rewrite("org.openrewrite.recipe:rewrite-spring:latest.integration") 35 | } 36 | 37 | tasks.create("validateRewrite") { 38 | dependsOn(tasks.getByName("rewriteDryRun")) 39 | doLast{ 40 | File patchFile = file("build/reports/rewrite/rewrite.patch") 41 | if (!patchFile.exists()) { 42 | throw new RuntimeException("Running rewrite dryRun failed to produce a patch report at " + patchFile.absolutePath); 43 | } 44 | 45 | // Spot-check for some expected changes 46 | String patch = patchFile.text 47 | if (!patch.contains('+++ b/src/main/java/org/springframework/samples/petclinic/owner/OwnerController.java')) { 48 | throw new RuntimeException("Expected the recipe to make changes to OwnerController.java"); 49 | } 50 | if (!patch.contains('- @RequestMapping(value = "/owners/what")\n+ @GetMapping("/owners/what")')) { 51 | throw new RuntimeException("Expected the recipe to change @RequestMapping into @GetMapping") 52 | } 53 | if (!patch.contains('- public JCacheManagerCustomizer cacheManagerCustomizer() {\n+ JCacheManagerCustomizer cacheManagerCustomizer() {')) { 54 | throw new RuntimeException("Expected the recipe to reduce visibility of bean methods that don't need to be public") 55 | } 56 | if (!patch.contains('-@RunWith(MockitoJUnitRunner.class)\n+@ExtendWith(MockitoExtension.class)\n public class PetTypeFormatterTests {')) { 57 | throw new RuntimeException("Expected the recipe to swap MockitoJUnitRunner for MockitoExtension") 58 | } 59 | if (!patch.contains('-spring.datasource.schema=classpath*:db/${database}/schema.sql\n' + 60 | '-spring.datasource.data=classpath*:db/${database}/data.sql\n' + 61 | '+spring.sql.init.schema-locations=classpath*:db/${database}/schema.sql\n' + 62 | '+spring.sql.init.data-locations=classpath*:db/${database}/data.sql')) { 63 | throw new RuntimeException("Expected the recipe to update spring properties") 64 | } 65 | 66 | } 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "rewrite-gradle-plugin" 2 | 3 | include("plugin") 4 | 5 | plugins { 6 | id("com.gradle.develocity") version "latest.release" 7 | id("com.gradle.common-custom-user-data-gradle-plugin") version "latest.release" 8 | } 9 | 10 | develocity { 11 | val isCiServer = System.getenv("CI")?.equals("true") ?: false 12 | server = "https://ge.openrewrite.org/" 13 | val accessKey = System.getenv("GRADLE_ENTERPRISE_ACCESS_KEY") 14 | val authenticated = !accessKey.isNullOrBlank() 15 | buildCache { 16 | remote(develocity.buildCache) { 17 | isEnabled = true 18 | isPush = isCiServer && authenticated 19 | } 20 | } 21 | 22 | buildScan { 23 | capture { 24 | fileFingerprints = true 25 | } 26 | publishing { 27 | onlyIf { 28 | authenticated 29 | } 30 | } 31 | 32 | uploadInBackground = !isCiServer 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /suppressions.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | CVE-2020-11979 8 | CVE-2021-29427 9 | CVE-2021-29428 10 | CVE-2021-29429 11 | CVE-2021-32751 12 | CVE-2023-35946 13 | CVE-2023-35947 14 | CVE-2023-42445 15 | CVE-2023-44387 16 | 17 | 18 | 23 | CVE-2019-11065 24 | CVE-2019-11402 25 | CVE-2019-11403 26 | CVE-2019-15052 27 | CVE-2019-16370 28 | CVE-2020-11979 29 | CVE-2020-15767 30 | CVE-2020-15773 31 | CVE-2021-29428 32 | CVE-2021-29429 33 | CVE-2021-32751 34 | CVE-2021-41589 35 | CVE-2022-25364 36 | CVE-2022-30587 37 | CVE-2023-35946 38 | CVE-2023-35947 39 | CVE-2023-42445 40 | CVE-2023-44387 41 | CVE-2023-49238 42 | 43 | 44 | 48 | ^pkg:maven/com\.google\.guava/guava@.*$ 49 | CVE-2023-2976 50 | CVE-2020-8908 51 | 52 | 53 | 58 | CVE-2023-45161 59 | CVE-2023-45163 60 | CVE-2023-5964 61 | 62 | 63 | --------------------------------------------------------------------------------