├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ ├── gradle.yml │ ├── publish.yml │ └── update-gradle-wrapper.yml ├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ └── codeStyleConfig.xml └── vcs.xml ├── LICENSE.txt ├── PUBLISHING.md ├── README.md ├── SECURITY.md ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── project-accessors ├── build.gradle.kts └── src │ ├── functionalTest │ └── kotlin │ │ └── co │ │ └── hinge │ │ └── gradle │ │ └── projectaccessors │ │ └── ProjectAccessorsGeneratorFunctionalTest.kt │ ├── main │ └── kotlin │ │ └── co │ │ └── hinge │ │ └── gradle │ │ └── projectaccessors │ │ ├── GenerateProjectAccessorsTask.kt │ │ ├── ProjectAccessorsExtension.kt │ │ ├── ProjectAccessorsGenerator.kt │ │ ├── ProjectAccessorsPlugin.kt │ │ ├── ProjectAccessorsProjectExtension.kt │ │ └── ProjectDependencies.kt │ └── test │ ├── kotlin │ └── co │ │ └── hinge │ │ └── gradle │ │ └── projectaccessors │ │ └── ProjectAccessorsGeneratorTest.kt │ └── resources │ └── junit-platform.properties └── settings.gradle.kts /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @ansman 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Report a bug in project-accessors 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | 12 | 13 | **To Reproduce** 14 | 15 | 16 | **Expected behavior** 17 | 18 | 19 | **Actual behavior** 20 | 21 | 22 | **Environment** 23 | - Gradle Version: 24 | - Java Version: 25 | 26 | **Additional context** 27 | 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for project-accessors 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | 12 | 13 | **Describe the solution you'd like** 14 | 15 | 16 | **Describe alternatives you've considered** 17 | 18 | 19 | **Additional context** 20 | 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | registries: 3 | gradle-plugin-portal: 4 | type: maven-repository 5 | url: "https://plugins.gradle.org/m2" 6 | maven-central: 7 | type: maven-repository 8 | url: "https://repo.maven.apache.org/maven2/" 9 | updates: 10 | - package-ecosystem: "gradle" 11 | directory: "/" 12 | open-pull-requests-limit: 25 13 | registries: 14 | - gradle-plugin-portal 15 | - maven-central 16 | schedule: 17 | interval: "daily" 18 | time: "08:00" 19 | timezone: America/New_York 20 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **Describe the reason for the change** 2 | 3 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: Run Checks 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'main' 7 | pull_request: 8 | merge_group: 9 | workflow_dispatch: 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: ${{ github.ref != 'refs/heads/main' }} 14 | 15 | jobs: 16 | checks: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - uses: actions/setup-java@v3 22 | with: 23 | distribution: 'zulu' 24 | java-version: '21' 25 | 26 | - name: Run Checks 27 | uses: gradle/gradle-build-action@v2 28 | with: 29 | arguments: check --continue --stacktrace -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '**' 7 | 8 | jobs: 9 | checks: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v3 13 | 14 | - uses: actions/setup-java@v3 15 | with: 16 | distribution: 'zulu' 17 | java-version: '21' 18 | 19 | - name: Run Checks 20 | uses: gradle/gradle-build-action@v2 21 | with: 22 | arguments: check --continue --stacktrace 23 | publish: 24 | runs-on: ubuntu-latest 25 | needs: checks 26 | steps: 27 | - uses: actions/checkout@v3 28 | 29 | - uses: actions/setup-java@v3 30 | with: 31 | distribution: 'zulu' 32 | java-version: '21' 33 | 34 | - name: Publish 35 | uses: gradle/gradle-build-action@v2 36 | with: 37 | arguments: publishPlugins --stacktrace 38 | env: 39 | ORG_GRADLE_PROJECT_signingKey: ${{ secrets.GRADLE_SIGNING_KEY }} 40 | ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.GRADLE_SIGNING_PASSWORD }} 41 | ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.GRADLE_SIGNING_KEY_ID }} 42 | GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }} 43 | GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }} -------------------------------------------------------------------------------- /.github/workflows/update-gradle-wrapper.yml: -------------------------------------------------------------------------------- 1 | name: Update Gradle Wrapper 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | update-gradle-wrapper: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v3 14 | 15 | - uses: actions/setup-java@v3 16 | with: 17 | distribution: 'zulu' 18 | java-version: '21' 19 | 20 | - name: Update Gradle Wrapper 21 | uses: gradle-update/update-gradle-wrapper-action@v1 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Gradle Cache for the project 2 | .gradle 3 | 4 | # Local Android SDK directory 5 | local.properties 6 | 7 | # Project build folder with Kotlin session data 8 | build 9 | buildSrc/ 10 | **/bin 11 | 12 | ############### 13 | # Mac OS 14 | ############### 15 | .DS_Store 16 | Icon\r 17 | 18 | ############### 19 | # Idea (IntelliJ/Android Studio) 20 | ############### 21 | # Based on https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 22 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider 23 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 24 | 25 | # User-specific stuff 26 | .idea/**/workspace.xml 27 | .idea/**/tasks.xml 28 | .idea/**/usage.statistics.xml 29 | .idea/**/dictionaries 30 | .idea/**/shelfg 31 | .idea/**/deploymentTargetDropDown.xml 32 | .idea/**/shelf 33 | .idea/**/androidTestResultsUserPreferences.xml 34 | .idea/**/assertWizardSettings.xml 35 | .idea/**/kotlinScripting.xml 36 | .idea/migrations.xml 37 | .idea/**/deploymentTargetSelector.xml 38 | 39 | # AWS User-specific 40 | .idea/**/aws.xml 41 | 42 | # Generated files 43 | .idea/**/contentModel.xml 44 | 45 | # Sensitive or high-churn files 46 | .idea/**/dataSources/ 47 | .idea/**/dataSources.ids 48 | .idea/**/dataSources.local.xml 49 | .idea/**/sqlDataSources.xml 50 | .idea/**/dynamic.xml 51 | .idea/**/uiDesigner.xml 52 | .idea/**/dbnavigator.xml 53 | 54 | # Gradle 55 | .idea/**/gradle.xml 56 | .idea/**/libraries 57 | 58 | # Gradle and Maven with auto-import 59 | # When using Gradle or Maven with auto-import, you should exclude module files, 60 | # since they will be recreated, and may cause churn. Uncomment if using 61 | # auto-import. 62 | .idea/artifacts 63 | .idea/compiler.xml 64 | .idea/jarRepositories.xml 65 | .idea/modules.xml 66 | .idea/misc.xml 67 | .idea/kotlinc.xml 68 | .idea/*.iml 69 | .idea/modules 70 | *.iml 71 | *.ipr 72 | .idea/.name 73 | 74 | # Memory dumps 75 | *.hprof 76 | 77 | # Crash dumps 78 | **/hs_err_pid*.log 79 | 80 | # Kotlin compiler 81 | .kotlin -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # Default ignored files 2 | /shelf/ 3 | /workspace.xml 4 | # GitHub Copilot persisted chat sessions 5 | /copilot/chatSessions 6 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /LICENSE.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 2024 Match Group, LLC 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. -------------------------------------------------------------------------------- /PUBLISHING.md: -------------------------------------------------------------------------------- 1 | # Publishing 2 | To publish you need to do the following: 3 | 1. Check out `main` and update it: 4 | - `git checkout main && git pull origin main` 5 | 2. Update the version in [`gradle.properties`](gradle.properties) to a non `-SNAPSHOT` version. 6 | 3. Update the version in [the readme](README.md) setup docs. 7 | 4. Run checks: 8 | - `./gradlew check publishPlugins --validate-only` 9 | 5. Commit with the message "Prepare for release X.Y.Z.": 10 | - `git commit -am "Prepare for release X.Y.Z."` 11 | 6. Push: 12 | - `git push origin main` 13 | 7. Create a new release: https://github.com/Hinge/project-accessors/releases/new 14 | - Create new tag version: `X.Y.Z` 15 | - Release title: `X.Y.Z` 16 | - Description: Automatically generate the notes and describe the highlights above. 17 | 8. Check the [publish workflow](https://github.com/Hinge/project-accessors/actions/workflows/publish.yml). 18 | 9. Update the version in `gradle.properties` to the next `-SNAPSHOT` version 19 | - You get the next version by incrementing the minor version and adding `-SNAPSHOT` to the end. 20 | - For example, `1.2.3` would become `1.3.0-SNAPSHOT`. 21 | 10. Commit with the message "Prepare for next development iteration": 22 | - `git commit -am "Prepare for next development iteration"` 23 | 11. Push: 24 | - `git push origin main` 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Project Accessors 2 | A Gradle plugin that generates type safe project accessors for included builds. 3 | 4 | Gradle already has [native support](https://docs.gradle.org/current/userguide/declaring_dependencies.html#sec:type-safe-project-accessors) 5 | for type safe project accessors, but it doesn't support accessing parent projects in an included build since included 6 | builds can be used for multiple parent projects. 7 | 8 | But, most of the time included builds are only used with a single parent project, and thus it's very useful to be able 9 | to access the projects in a type safe way. 10 | 11 | ## Setup 12 | Firstly, you need to add the plugin to your included build's build script (for example `build-logic/build.gradle.kts`): 13 | ```kotlin 14 | plugin { 15 | id("co.hinge.gradle.project-accessors") version "1.2.0" 16 | } 17 | 18 | // If you also want to use the utilities you can add this: 19 | dependencies { 20 | id("co.hinge.gradle.project-accessors:project-accessors:1.2.0") 21 | } 22 | ``` 23 | 24 | For most projects, that's all that's needed. You can then access the projects using the generated accessors: 25 | ```kotlin 26 | dependencies { 27 | implementation(projects.someModule) 28 | } 29 | ``` 30 | or if not using precompiled scripts: 31 | ```kotlin 32 | class SomeConventionPlugin : Plugin { 33 | override fun apply(project: Project) { 34 | project.dependencies { 35 | implementation(project.projects.someModule) 36 | } 37 | } 38 | } 39 | ``` 40 | 41 | ## Configuration 42 | By default, the plugin detects if it's added to an included build. When this happens an accessor called `projects` is 43 | generated as an extension on `Project`. 44 | 45 | If you need to support multiple parent projects or for any other reason need to customize things, you use the 46 | `projectAccessors` DSL: 47 | ```kotlin 48 | projectAccessors { 49 | // Parent is the name of the default parent project 50 | project("parent") { 51 | // If you need to change the location of the settings file 52 | fromProject(gradle.parent!!.rootProject) 53 | // If you want to change the accessor name 54 | accessorName.set("myProjects") 55 | // If you want to change the accessor package name (defaults to the root package) 56 | packageName.set("com.example.accessors") 57 | } 58 | 59 | // If you need to support multiple parent projects 60 | project("other") { 61 | projectPaths.addAll(":path:to:module") 62 | accessorName.set("otherProjects") 63 | } 64 | } 65 | ``` 66 | 67 | ## Utilities 68 | ### Copying Project Dependencies 69 | Sometimes you need to depend on a specific configuration of a project. This is 70 | annoying to do with Kotlin build scripts as you have to copy the dependency then 71 | change the configuration. 72 | 73 | To make this easier, the plugin provides a utility function that allows you to quickly 74 | copy a dependency and change the configuration: 75 | ```kotlin 76 | import co.hinge.gradle.projectaccessors.copy 77 | 78 | dependencies { 79 | implementation(projects.someModule.copy(targetConfiguration = "someConfiguration")) 80 | } 81 | ``` 82 | 83 | ## Supported Gradle Versions 84 | The plugin is tested with all major Gradle versions from 8.2, but will likely work with other versions too. 85 | 86 | ## License 87 | ```plain 88 | Copyright 2024 Match Group, LLC 89 | 90 | Licensed under the Apache License, Version 2.0 (the "License"); 91 | you may not use this file except in compliance with the License. 92 | You may obtain a copy of the License at 93 | 94 | http://www.apache.org/licenses/LICENSE-2.0 95 | 96 | Unless required by applicable law or agreed to in writing, software 97 | distributed under the License is distributed on an "AS IS" BASIS, 98 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 99 | See the License for the specific language governing permissions and 100 | limitations under the License. 101 | ``` 102 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | Currently all 1.x versions are supported with security updates. 6 | 7 | | Version | Supported | 8 | | ------- | ------------------ | 9 | | 1.x.x | :white_check_mark: | 10 | 11 | ## Reporting a Vulnerability 12 | 13 | To report a security vulnerability open an [issue](https://github.com/Hinge/project-accessors/issues/new?assignees=&labels=bug&projects=&template=bug_report.md&title=) 14 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.parallel=true 2 | org.gradle.jvmargs=-XX:+UseG1GC -Dfile.encoding=UTF-8 -Xmx2g -Xms1g -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m 3 | org.gradle.vfs.watch=true 4 | org.gradle.caching=true 5 | org.gradle.configuration.cache=true 6 | systemProp.org.gradle.kotlin.dsl.precompiled.accessors.strict=true 7 | 8 | kotlin.code.style=official 9 | 10 | version=1.3.0-SNAPSHOT 11 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | assertk = "0.28.1" 3 | kotlin = "2.1.21" 4 | kotlinpoet = "2.2.0" 5 | kotlinCompileTesting = "0.7.1" 6 | gradlePublish = "1.3.1" 7 | junit5 = "5.13.0" 8 | 9 | [plugins] 10 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 11 | gradle-publish = { id = "com.gradle.plugin-publish", version.ref = "gradlePublish" } 12 | 13 | [libraries] 14 | kotlin-stdlib = { module = "org.jetbrains.kotlin:kotlin-stdlib", version.ref = "kotlin" } 15 | kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } 16 | 17 | kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet" } 18 | 19 | kotlinCompileTesting = { module = "dev.zacsweers.kctfork:core", version.ref = "kotlinCompileTesting" } 20 | 21 | assertk = { module = "com.willowtreeapps.assertk:assertk", version.ref = "assertk" } 22 | 23 | junit-bom = { module = "org.junit:junit-bom", version.ref = "junit5" } 24 | junit-jupiter = { module = "org.junit.jupiter:junit-jupiter" } 25 | junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Hinge/project-accessors/0e0f035c5cc5c4b8618da696d1090f554843e425/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=efe9a3d147d948d7528a9887fa35abcf24ca1a43ad06439996490f77569b02d1 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-all.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /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="\\\"\\\"" 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, 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 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 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= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 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 | -------------------------------------------------------------------------------- /project-accessors/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | import org.jetbrains.kotlin.gradle.dsl.KotlinVersion 3 | 4 | plugins { 5 | alias(libs.plugins.kotlin.jvm) 6 | `java-gradle-plugin` 7 | `maven-publish` 8 | alias(libs.plugins.gradle.publish) 9 | } 10 | 11 | if (providers.gradleProperty("signArtifacts").orNull?.toBooleanStrict() == true) { 12 | pluginManager.apply("signing") 13 | } 14 | 15 | group = "co.hinge.gradle.project-accessors" 16 | version = providers.gradleProperty("version").get() 17 | 18 | tasks.withType { 19 | useJUnitPlatform() 20 | maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1) 21 | maxHeapSize = "1g" 22 | } 23 | 24 | kotlin { 25 | jvmToolchain(21) 26 | compilerOptions { 27 | jvmTarget.set(JvmTarget.JVM_11) 28 | languageVersion.set(KotlinVersion.KOTLIN_1_9) 29 | apiVersion.set(languageVersion) 30 | } 31 | } 32 | 33 | java { 34 | toolchain { 35 | languageVersion.set(JavaLanguageVersion.of(21)) 36 | vendor.set(JvmVendorSpec.AZUL) 37 | implementation.set(JvmImplementation.VENDOR_SPECIFIC) 38 | } 39 | } 40 | 41 | tasks.withType().configureEach { 42 | sourceCompatibility = "11" 43 | targetCompatibility = "11" 44 | } 45 | 46 | val buildDirMavenRepo = layout.buildDirectory.dir("repo") 47 | publishing { 48 | repositories { 49 | val buildDirRepo = maven { 50 | name = "buildDir" 51 | url = uri(buildDirMavenRepo) 52 | } 53 | tasks.withType().configureEach { 54 | doLast { 55 | if (this is PublishToMavenRepository && repository == buildDirRepo) { 56 | return@doLast 57 | } 58 | println("Published ${publication.groupId}:${publication.artifactId}:${publication.version}") 59 | } 60 | } 61 | } 62 | } 63 | 64 | 65 | val functionalTest: SourceSet by sourceSets.creating 66 | 67 | @Suppress("UnstableApiUsage") 68 | gradlePlugin { 69 | isAutomatedPublishing = true 70 | testSourceSets(sourceSets.test.get(), functionalTest) 71 | plugins { 72 | register("projectAccessors") { 73 | id = "co.hinge.gradle.project-accessors" 74 | implementationClass = "co.hinge.gradle.projectaccessors.ProjectAccessorsPlugin" 75 | displayName = "Project Accessors Plugin" 76 | description = "A Gradle plugin that can generate type safe project accessors for included builds." 77 | tags = setOf("accessors", "projects") 78 | } 79 | } 80 | vcsUrl.set("https://github.com/Hinge/project-accessors") 81 | website.set("https://github.com/Hinge/project-accessors") 82 | } 83 | 84 | tasks.validatePlugins { 85 | enableStricterValidation = true 86 | } 87 | 88 | tasks.publishPlugins { 89 | doFirst { 90 | check(!version.toString().endsWith("-SNAPSHOT")) { 91 | "You cannot publish snapshot versions" 92 | } 93 | } 94 | } 95 | 96 | pluginManager.withPlugin("org.gradle.signing") { 97 | configure { 98 | val signingKeyId: String? by project 99 | val signingKey: String? by project 100 | val signingPassword: String? by project 101 | useInMemoryPgpKeys(signingKeyId, signingKey, signingPassword) 102 | } 103 | } 104 | 105 | val functionalTestTask = tasks.register("functionalTest") { 106 | description = "Runs the integration tests." 107 | group = "verification" 108 | testClassesDirs = functionalTest.output.classesDirs 109 | classpath = functionalTest.runtimeClasspath 110 | dependsOn("publishAllPublicationsToBuildDirRepository") 111 | mustRunAfter(tasks.test) 112 | systemProperty("mavenRepo", buildDirMavenRepo.get().asFile.absolutePath) 113 | systemProperty("pluginVersion", version.toString()) 114 | systemProperty("currentGradleVersion", gradle.gradleVersion) 115 | systemProperty("kotlinVersion", libs.versions.kotlin.get()) 116 | } 117 | 118 | tasks.check { 119 | dependsOn(functionalTestTask) 120 | } 121 | 122 | dependencies { 123 | implementation(libs.kotlinpoet) 124 | implementation(libs.kotlin.stdlib) 125 | testImplementation(libs.kotlin.test) 126 | testImplementation(libs.assertk) 127 | testImplementation(libs.kotlinCompileTesting) 128 | testImplementation(platform(libs.junit.bom)) 129 | testImplementation(libs.junit.jupiter) 130 | testRuntimeOnly(libs.junit.platform.launcher) 131 | 132 | "functionalTestImplementation"(project) 133 | "functionalTestImplementation"(libs.kotlin.test) 134 | "functionalTestImplementation"(platform(libs.junit.bom)) 135 | "functionalTestImplementation"(libs.junit.jupiter) 136 | "functionalTestImplementation"(libs.assertk) 137 | "functionalTestRuntimeOnly"(libs.junit.platform.launcher) 138 | } -------------------------------------------------------------------------------- /project-accessors/src/functionalTest/kotlin/co/hinge/gradle/projectaccessors/ProjectAccessorsGeneratorFunctionalTest.kt: -------------------------------------------------------------------------------- 1 | package co.hinge.gradle.projectaccessors 2 | 3 | import assertk.assertFailure 4 | import assertk.assertThat 5 | import assertk.assertions.isEqualTo 6 | import assertk.assertions.isInstanceOf 7 | import assertk.assertions.isNotNull 8 | import assertk.assertions.prop 9 | import org.gradle.testkit.runner.BuildTask 10 | import org.gradle.testkit.runner.GradleRunner 11 | import org.gradle.testkit.runner.TaskOutcome 12 | import org.gradle.testkit.runner.UnexpectedBuildFailure 13 | import org.intellij.lang.annotations.Language 14 | import org.junit.jupiter.api.BeforeEach 15 | import org.junit.jupiter.api.io.TempDir 16 | import org.junit.jupiter.api.parallel.Execution 17 | import org.junit.jupiter.api.parallel.ExecutionMode 18 | import org.junit.jupiter.params.ParameterizedTest 19 | import org.junit.jupiter.params.provider.Arguments 20 | import org.junit.jupiter.params.provider.MethodSource 21 | import java.io.File 22 | import java.util.stream.Stream 23 | 24 | class ProjectAccessorsGeneratorFunctionalTest { 25 | @TempDir 26 | lateinit var projectDir: File 27 | 28 | private val includedBuildPlugin: File get() = projectDir.resolve("gradle-plugin/src/main/kotlin/com/example/ExamplePlugin.kt") 29 | 30 | private fun gradleRunner(version: String) = GradleRunner.create() 31 | .withGradleVersion(version) 32 | .withProjectDir(projectDir) 33 | 34 | @BeforeEach 35 | fun setup() { 36 | projectDir.resolve("settings.gradle.kts").writeKotlin( 37 | """ 38 | pluginManagement { 39 | includeBuild("gradle-plugin") 40 | } 41 | 42 | dependencyResolutionManagement { 43 | repositories { 44 | mavenCentral() 45 | } 46 | } 47 | 48 | include(":dependency") 49 | include(":dependant") 50 | """ 51 | ) 52 | 53 | projectDir.resolve("build.gradle.kts").writeKotlin( 54 | """ 55 | plugins { 56 | kotlin("jvm") version "${System.getProperty("kotlinVersion")}" apply false 57 | } 58 | """.trimIndent() 59 | ) 60 | 61 | projectDir.resolve("gradle-plugin/settings.gradle.kts").writeKotlin( 62 | """ 63 | pluginManagement { 64 | repositories { 65 | maven("${System.getProperty("mavenRepo")}") 66 | gradlePluginPortal() 67 | mavenCentral() 68 | } 69 | } 70 | 71 | dependencyResolutionManagement { 72 | repositories { 73 | mavenCentral() 74 | } 75 | } 76 | """.trimIndent() 77 | ) 78 | 79 | projectDir.resolve("gradle-plugin/build.gradle.kts").writeKotlin( 80 | """ 81 | plugins { 82 | id("co.hinge.gradle.project-accessors") version "${System.getProperty("pluginVersion")}" 83 | kotlin("jvm") version "${System.getProperty("kotlinVersion")}" 84 | `java-gradle-plugin` 85 | } 86 | 87 | gradlePlugin { 88 | plugins { 89 | register("com.example") { 90 | id = name 91 | implementationClass = "com.example.ExamplePlugin" 92 | } 93 | } 94 | } 95 | 96 | kotlin { 97 | compilerOptions { 98 | jvmTarget.set(org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_1_8) 99 | } 100 | } 101 | 102 | tasks.withType().configureEach { 103 | sourceCompatibility = "1.8" 104 | targetCompatibility = "1.8" 105 | } 106 | 107 | projectAccessors { 108 | // Should just compile 109 | projects.configureEach {} 110 | } 111 | """ 112 | ) 113 | 114 | includedBuildPlugin.writeKotlin( 115 | """ 116 | package com.example 117 | 118 | import org.gradle.api.Plugin 119 | import org.gradle.api.Project 120 | import projects 121 | 122 | class ExamplePlugin : Plugin { 123 | override fun apply(target: Project) { 124 | target.pluginManager.apply("org.jetbrains.kotlin.jvm") 125 | target.dependencies.add("implementation", target.projects.dependency) 126 | } 127 | } 128 | """ 129 | ) 130 | 131 | projectDir.resolve("dependency/build.gradle.kts").writeKotlin( 132 | """ 133 | plugins { 134 | kotlin("jvm") 135 | } 136 | """ 137 | ) 138 | projectDir.resolve("dependency/src/main/kotlin/Dependency.kt").writeKotlin( 139 | """ 140 | package com.example.dependency 141 | 142 | abstract class Dependency 143 | """.trimIndent() 144 | ) 145 | 146 | projectDir.resolve("dependant/build.gradle.kts").writeKotlin( 147 | """ 148 | plugins { 149 | id("com.example") 150 | kotlin("jvm") 151 | } 152 | """ 153 | ) 154 | projectDir.resolve("dependant/src/main/kotlin/Dependant.kt").writeKotlin( 155 | """ 156 | package com.example.dependant 157 | 158 | import com.example.dependency.Dependency 159 | 160 | object Dependant : Dependency() 161 | """.trimIndent() 162 | ) 163 | } 164 | 165 | @Execution(ExecutionMode.CONCURRENT) 166 | @ParameterizedTest 167 | @MethodSource("provideGradleVersions") 168 | fun setsUpDependencyCorrectly(gradleVersion: String) { 169 | val result = gradleRunner(gradleVersion) 170 | .withArguments(":dependant:assemble", "--stacktrace") 171 | .build() 172 | 173 | assertThat(result.task(":dependency:compileKotlin")) 174 | .isNotNull() 175 | .prop(BuildTask::getOutcome) 176 | .isEqualTo(TaskOutcome.SUCCESS) 177 | 178 | assertThat(result.task(":dependency:compileKotlin")) 179 | .isNotNull() 180 | .prop(BuildTask::getOutcome) 181 | .isEqualTo(TaskOutcome.SUCCESS) 182 | } 183 | 184 | @Execution(ExecutionMode.CONCURRENT) 185 | @ParameterizedTest 186 | @MethodSource("provideGradleVersions") 187 | fun worksWithProjectIsolation(gradleVersion: String) { 188 | gradleRunner(gradleVersion) 189 | .withArguments( 190 | "-Dorg.gradle.internal.invalidate-coupled-projects=false", 191 | "-Dorg.gradle.unsafe.isolated-projects=true", 192 | "--stacktrace" 193 | ) 194 | .build() 195 | } 196 | 197 | @Execution(ExecutionMode.CONCURRENT) 198 | @ParameterizedTest 199 | @MethodSource("provideGradleVersions") 200 | fun doesNotAllowDuplicateAccessorNames(gradleVersion: String) { 201 | projectDir.resolve("gradle-plugin/build.gradle.kts").appendText( 202 | """ 203 | projectAccessors { 204 | project("projects1") { 205 | settingsFile.fileValue(file("../settings.gradle.kts")) 206 | accessorName.set("projects") 207 | } 208 | project("projects2") { 209 | settingsFile.fileValue(file("../settings.gradle.kts")) 210 | accessorName.set("projects") 211 | } 212 | } 213 | """.trimIndent() 214 | ) 215 | 216 | assertFailure { 217 | gradleRunner(gradleVersion) 218 | .withArguments("--stacktrace") 219 | .build() 220 | }.isInstanceOf() 221 | } 222 | 223 | private fun File.writeKotlin(@Language("kts") content: String) { 224 | parentFile.mkdirs() 225 | writeText(content.trimIndent()) 226 | } 227 | 228 | companion object { 229 | @JvmStatic 230 | private fun provideGradleVersions(): Stream { 231 | return setOf( 232 | System.getProperty("currentGradleVersion"), 233 | "8.14", 234 | "8.13", 235 | "8.12.1", 236 | "8.11.1", 237 | "8.10.2", 238 | "8.9", 239 | "8.8", 240 | "8.7", 241 | "8.6", 242 | "8.5", 243 | "8.4", 244 | "8.3", 245 | "8.2.1", 246 | ).map { Arguments.of(it) }.stream() 247 | } 248 | } 249 | } 250 | -------------------------------------------------------------------------------- /project-accessors/src/main/kotlin/co/hinge/gradle/projectaccessors/GenerateProjectAccessorsTask.kt: -------------------------------------------------------------------------------- 1 | package co.hinge.gradle.projectaccessors 2 | 3 | import org.gradle.api.DefaultTask 4 | import org.gradle.api.file.DirectoryProperty 5 | import org.gradle.api.provider.Property 6 | import org.gradle.api.provider.SetProperty 7 | import org.gradle.api.tasks.CacheableTask 8 | import org.gradle.api.tasks.Input 9 | import org.gradle.api.tasks.OutputDirectory 10 | import org.gradle.api.tasks.TaskAction 11 | 12 | @Suppress("LeakingThis") 13 | @CacheableTask 14 | abstract class GenerateProjectAccessorsTask : DefaultTask() { 15 | @get:Input 16 | abstract val projectPaths: SetProperty 17 | 18 | @get:Input 19 | abstract val projectName: Property 20 | 21 | @get:Input 22 | abstract val packageName: Property 23 | 24 | @get:Input 25 | abstract val accessorName: Property 26 | 27 | @get:Input 28 | abstract val className: Property 29 | 30 | @get:OutputDirectory 31 | abstract val outputDirectory: DirectoryProperty 32 | 33 | init { 34 | group = "build" 35 | projectPaths.finalizeValueOnRead() 36 | packageName.finalizeValueOnRead() 37 | accessorName.finalizeValueOnRead() 38 | className.finalizeValueOnRead() 39 | outputDirectory.finalizeValueOnRead() 40 | } 41 | 42 | @TaskAction 43 | fun generateAccessors() { 44 | with(outputDirectory.get().asFile) { 45 | deleteRecursively() 46 | mkdirs() 47 | } 48 | val generator = ProjectAccessorsGenerator( 49 | projectName = projectName.get(), 50 | packageName = packageName.get(), 51 | accessorName = accessorName.get(), 52 | className = className.get(), 53 | ) 54 | 55 | generator.generate(projectPaths.get()) 56 | .writeTo(outputDirectory.get().asFile) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /project-accessors/src/main/kotlin/co/hinge/gradle/projectaccessors/ProjectAccessorsExtension.kt: -------------------------------------------------------------------------------- 1 | package co.hinge.gradle.projectaccessors 2 | 3 | import org.gradle.api.NamedDomainObjectContainer 4 | 5 | abstract class ProjectAccessorsExtension { 6 | abstract val projects: NamedDomainObjectContainer 7 | 8 | /** 9 | * Adds a new project to generate accessors for. 10 | * 11 | * By default, if the project is a part of an included build and no projects are registered, a project named 12 | * `parent` will be registered which will point to the parent `settings.gradle.kts` and generate accessors available 13 | * using `projects`. 14 | */ 15 | fun project(name: String, configure: ProjectAccessorsProjectExtension.() -> Unit) { 16 | projects.maybeCreate(name).apply(configure) 17 | } 18 | } -------------------------------------------------------------------------------- /project-accessors/src/main/kotlin/co/hinge/gradle/projectaccessors/ProjectAccessorsGenerator.kt: -------------------------------------------------------------------------------- 1 | package co.hinge.gradle.projectaccessors 2 | 3 | import com.squareup.kotlinpoet.ClassName 4 | import com.squareup.kotlinpoet.CodeBlock 5 | import com.squareup.kotlinpoet.FileSpec 6 | import com.squareup.kotlinpoet.FunSpec 7 | import com.squareup.kotlinpoet.KModifier 8 | import com.squareup.kotlinpoet.PropertySpec 9 | import com.squareup.kotlinpoet.STRING 10 | import com.squareup.kotlinpoet.TypeSpec 11 | import org.gradle.util.GradleVersion 12 | import java.util.Locale 13 | 14 | internal class ProjectAccessorsGenerator( 15 | private val projectName: String, 16 | private val packageName: String, 17 | private val accessorName: String, 18 | private val className: String, 19 | private val gradleVersion: GradleVersion = GradleVersion.current(), 20 | ) { 21 | 22 | fun generate(projectPaths: Set): FileSpec { 23 | val projectDependenciesType = ClassName(packageName, className) 24 | val projectDependencies = TypeSpec.classBuilder(projectDependenciesType) 25 | .addModifiers(KModifier.INTERNAL) 26 | .primaryConstructor(projectDependencyConstructor) 27 | .addProperty( 28 | PropertySpec.builder("project", projectType, KModifier.PRIVATE) 29 | .initializer("project") 30 | .build() 31 | ) 32 | 33 | val projects = mutableMapOf( 34 | ":" to Module( 35 | listOf(""), 36 | projectDependenciesType, 37 | name = "root", 38 | isProject = true 39 | ) 40 | ) 41 | projectPaths 42 | .asSequence() 43 | .map { it.removePrefix(":") } 44 | .filter { it.isNotEmpty() } 45 | .map { it.split(":") } 46 | .forEach { path -> 47 | projects 48 | .getOrPut(path.first()) { 49 | Module( 50 | path.first(), 51 | projectDependenciesType 52 | ) 53 | } 54 | .add(path) 55 | } 56 | for (project in projects.values) { 57 | project.renderTo(projectDependencies) 58 | } 59 | 60 | return FileSpec.builder(projectDependenciesType) 61 | .addProperty( 62 | PropertySpec.builder(accessorName, projectDependenciesType, KModifier.INTERNAL) 63 | .addKdoc("Returns the project dependencies for the %L project.", projectName) 64 | .receiver(projectType) 65 | .getter( 66 | FunSpec.getterBuilder() 67 | .addCode("return %T(this)", projectDependenciesType) 68 | .build() 69 | ) 70 | .build() 71 | ) 72 | .addType(projectDependencies.build()) 73 | .build() 74 | } 75 | 76 | /** 77 | */ 78 | private fun Module.renderTo(parent: TypeSpec.Builder) { 79 | parent.addProperty( 80 | PropertySpec.builder(accessorName, typeName) 81 | .addKdoc("Creates a project dependency on the project at path \"%L\"", path.joinToString(":", prefix = ":")) 82 | .getter( 83 | FunSpec.getterBuilder() 84 | .addStatement("return %T()", typeName) 85 | .build() 86 | ) 87 | .build() 88 | ) 89 | 90 | parent.addType( 91 | TypeSpec.classBuilder(typeName) 92 | .addModifiers(KModifier.INNER) 93 | .apply { 94 | if (isProject) { 95 | addSuperinterface(projectDependencyType, projectDependency()) 96 | } 97 | val path = path.joinToString(":", prefix = ":") 98 | if (!isProject || gradleVersion < maxGradleVersionForProjectPath) { 99 | addProperty( 100 | PropertySpec.builder("path", STRING) 101 | .addKdoc("Returns the path to the project as a string.") 102 | .apply { 103 | if (!isProject) { 104 | addKdoc("\n\nPlease note that %S is not declared project so this path cannot be used\nas a dependency, this accessor is here for convenience.", path) 105 | } 106 | } 107 | .initializer("%S", path) 108 | .build() 109 | ) 110 | } 111 | for (child in children.values) { 112 | child.renderTo(this) 113 | } 114 | } 115 | .build() 116 | ) 117 | } 118 | 119 | private fun Module.projectDependency(): CodeBlock = 120 | CodeBlock.of( 121 | "project.dependencies.project(mapOf(%S to %S)) as %T", 122 | "path", 123 | path.joinToString(":", prefix = ":"), 124 | projectDependencyType 125 | ) 126 | 127 | companion object { 128 | private val TO_CAMEL_CASE = Regex("[-_]([a-z])") 129 | private val projectType = ClassName("org.gradle.api", "Project") 130 | private val projectDependencyType = 131 | ClassName("org.gradle.api.internal.artifacts.dependencies", "ProjectDependencyInternal") 132 | private val projectDependencyConstructor = FunSpec.constructorBuilder() 133 | .addParameter("project", projectType) 134 | .build() 135 | 136 | private val maxGradleVersionForProjectPath = GradleVersion.version("8.11") 137 | } 138 | 139 | data class Module( 140 | val path: List, 141 | val parent: ClassName, 142 | val name: String = path.last(), 143 | var isProject: Boolean = false, 144 | ) { 145 | val children = mutableMapOf() 146 | val accessorName: String = name.replace(TO_CAMEL_CASE) { it.groupValues[1].uppercase(Locale.ROOT) } 147 | val typeName: ClassName = parent.nestedClass(accessorName.replaceFirstChar(Char::uppercaseChar) + "Project") 148 | 149 | constructor(name: String, parent: ClassName) : this(listOf(name), parent, name) 150 | 151 | fun add(modulePath: List) { 152 | if (modulePath == path) { 153 | isProject = true 154 | return 155 | } 156 | 157 | require(modulePath.subList(0, path.size) == path) { 158 | "Module $modulePath does not start with $path" 159 | } 160 | val next = modulePath[path.size] 161 | children.getOrPut(next) { Module(path + next, typeName) } 162 | .add(modulePath) 163 | } 164 | } 165 | } -------------------------------------------------------------------------------- /project-accessors/src/main/kotlin/co/hinge/gradle/projectaccessors/ProjectAccessorsPlugin.kt: -------------------------------------------------------------------------------- 1 | package co.hinge.gradle.projectaccessors 2 | 3 | import org.gradle.api.Plugin 4 | import org.gradle.api.Project 5 | import org.gradle.api.tasks.SourceSetContainer 6 | import org.gradle.api.tasks.TaskProvider 7 | 8 | class ProjectAccessorsPlugin : Plugin { 9 | override fun apply(target: Project) { 10 | val extension = target.extensions.create("projectAccessors", ProjectAccessorsExtension::class.java) 11 | extension.projects.all { ext -> 12 | target.setUp(ext) 13 | } 14 | target.afterEvaluate { 15 | val parent = target.gradle.parent 16 | if (extension.projects.isNotEmpty()) { 17 | validateNames(extension.projects) 18 | } else if (parent != null) { 19 | extension.projects.register("parent") 20 | } 21 | } 22 | } 23 | 24 | private fun Project.setUp(extension: ProjectAccessorsProjectExtension): TaskProvider { 25 | val name = extension.name 26 | val task = tasks.register( 27 | "generate${name.replaceFirstChar(Char::uppercaseChar)}ProjectAccessors", 28 | GenerateProjectAccessorsTask::class.java 29 | ) { 30 | it.description = "Generates the $name project accessors" 31 | 32 | it.projectName.set(extension.name) 33 | it.projectPaths.set(extension.projectPaths) 34 | it.packageName.set(extension.packageName) 35 | it.accessorName.set(extension.accessorName) 36 | it.className.set("${extension.name.replaceFirstChar(kotlin.Char::uppercaseChar)}ProjectAccessors") 37 | it.outputDirectory.set(layout.buildDirectory.dir("generated/project-accessors/${name}")) 38 | } 39 | 40 | plugins.withId("org.jetbrains.kotlin.jvm") { 41 | extensions.configure("sourceSets") { sourceSets -> 42 | sourceSets.named("main") { sourceSet -> 43 | sourceSet.java.srcDir(task.flatMap { t -> t.outputDirectory }) 44 | } 45 | } 46 | } 47 | 48 | // The parent name is special as it refers to the project that includes this build 49 | val parent = gradle.parent 50 | if (name == "parent" && parent != null) { 51 | parent.rootProject { 52 | extension.fromProject(it) 53 | extension.accessorName.convention("projects") 54 | } 55 | } 56 | 57 | return task 58 | } 59 | 60 | private fun validateNames(projects: Iterable) { 61 | val accessors = mutableSetOf() 62 | projects.forEach { 63 | val accessorName = it.packageName.get() + "." + it.accessorName.get() 64 | if (!accessors.add(accessorName)) { 65 | throw IllegalArgumentException("There are multiple projects accessors with the accessor name $accessorName. When adding multiple accessors, make sure to set explicit and unique accessor names.") 66 | } 67 | } 68 | } 69 | } -------------------------------------------------------------------------------- /project-accessors/src/main/kotlin/co/hinge/gradle/projectaccessors/ProjectAccessorsProjectExtension.kt: -------------------------------------------------------------------------------- 1 | package co.hinge.gradle.projectaccessors 2 | 3 | import org.gradle.api.Named 4 | import org.gradle.api.Project 5 | import org.gradle.api.provider.Property 6 | import org.gradle.api.provider.SetProperty 7 | 8 | @Suppress("LeakingThis") 9 | abstract class ProjectAccessorsProjectExtension(private val name: String) : Named { 10 | 11 | /** 12 | * The project paths to generate the path for 13 | */ 14 | abstract val projectPaths: SetProperty 15 | 16 | /** 17 | * The package name to generate the accessors in. 18 | * 19 | * Defaults an empty string. 20 | */ 21 | abstract val packageName: Property 22 | 23 | /** 24 | * The name of the accessor property to generate. 25 | * 26 | * This can be changed if you need multiple accessors. 27 | * 28 | * Defaults to `projects` when using an included build. 29 | */ 30 | abstract val accessorName: Property 31 | 32 | init { 33 | with(projectPaths) { 34 | finalizeValueOnRead() 35 | } 36 | with(packageName) { 37 | finalizeValueOnRead() 38 | convention("") 39 | } 40 | with(accessorName) { 41 | finalizeValueOnRead() 42 | } 43 | } 44 | 45 | override fun getName(): String = name 46 | 47 | fun fromProject(project: Project) { 48 | projectPaths.convention(project.rootProject.allprojects.map { it.path }) 49 | } 50 | } -------------------------------------------------------------------------------- /project-accessors/src/main/kotlin/co/hinge/gradle/projectaccessors/ProjectDependencies.kt: -------------------------------------------------------------------------------- 1 | package co.hinge.gradle.projectaccessors 2 | 3 | import org.gradle.api.artifacts.ProjectDependency 4 | import org.gradle.api.artifacts.VersionConstraint 5 | 6 | /** 7 | * Returns a copy of this [ProjectDependency] with specified attributes. 8 | * 9 | * @receiver The [ProjectDependency] to copy. 10 | * @param targetConfiguration The requested target configuration of this dependency. This is the name of the configuration in the target module that should be used when selecting the matching configuration. If null, a default configuration will be used. 11 | * @param isTransitive Whether this dependency should be resolved including or excluding its transitive dependencies. The artifacts belonging to this dependency might themselves have dependencies on other artifacts. The latter are called transitive dependencies. 12 | * @param endorseStrictVersions Will, if true, endorse version constraints with [VersionConstraint.getStrictVersion] from the target module. Endorsing strict versions of another module/platform means that all strict versions will be interpreted during dependency resolution as if they were defined by the endorsing module. 13 | */ 14 | fun ProjectDependency.copy( 15 | targetConfiguration: String? = getTargetConfiguration(), 16 | isTransitive: Boolean = isTransitive(), 17 | endorseStrictVersions: Boolean = isEndorsingStrictVersions, 18 | ) : ProjectDependency = copy().apply { 19 | setTargetConfiguration(targetConfiguration) 20 | setTransitive(isTransitive) 21 | if (endorseStrictVersions) { 22 | endorseStrictVersions() 23 | } else { 24 | doNotEndorseStrictVersions() 25 | } 26 | } -------------------------------------------------------------------------------- /project-accessors/src/test/kotlin/co/hinge/gradle/projectaccessors/ProjectAccessorsGeneratorTest.kt: -------------------------------------------------------------------------------- 1 | package co.hinge.gradle.projectaccessors 2 | 3 | import assertk.assertThat 4 | import assertk.assertions.isEqualTo 5 | import com.tschuchort.compiletesting.KotlinCompilation 6 | import com.tschuchort.compiletesting.SourceFile 7 | import org.gradle.util.GradleVersion 8 | import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi 9 | import org.junit.jupiter.api.Test 10 | 11 | class ProjectAccessorsGeneratorTest { 12 | private var generator = ProjectAccessorsGenerator( 13 | projectName = "parent", 14 | packageName = "com.example", 15 | accessorName = "projects", 16 | className = "ProjectsAccessors" 17 | ) 18 | 19 | @Test 20 | fun `generate empty accessor`() { 21 | test( 22 | setOf(":"), 23 | """ 24 | package com.example 25 | 26 | import org.gradle.api.Project 27 | import org.gradle.api.`internal`.artifacts.dependencies.ProjectDependencyInternal 28 | 29 | /** 30 | * Returns the project dependencies for the parent project. 31 | */ 32 | internal val Project.projects: ProjectsAccessors 33 | get() = ProjectsAccessors(this) 34 | 35 | internal class ProjectsAccessors( 36 | private val project: Project, 37 | ) { 38 | /** 39 | * Creates a project dependency on the project at path ":" 40 | */ 41 | public val root: RootProject 42 | get() = RootProject() 43 | 44 | public inner class RootProject : ProjectDependencyInternal by project.dependencies.project(mapOf("path" to ":")) as ProjectDependencyInternal 45 | } 46 | """ 47 | ) 48 | } 49 | 50 | @Test 51 | fun `with project path`() { 52 | generator = ProjectAccessorsGenerator( 53 | projectName = "parent", 54 | packageName = "com.example", 55 | accessorName = "projects", 56 | className = "ProjectsAccessors", 57 | gradleVersion = GradleVersion.version("8.10.2") 58 | ) 59 | test( 60 | setOf( 61 | ":", 62 | ":module1", 63 | ), 64 | """ 65 | package com.example 66 | 67 | import kotlin.String 68 | import org.gradle.api.Project 69 | import org.gradle.api.`internal`.artifacts.dependencies.ProjectDependencyInternal 70 | 71 | /** 72 | * Returns the project dependencies for the parent project. 73 | */ 74 | internal val Project.projects: ProjectsAccessors 75 | get() = ProjectsAccessors(this) 76 | 77 | internal class ProjectsAccessors( 78 | private val project: Project, 79 | ) { 80 | /** 81 | * Creates a project dependency on the project at path ":" 82 | */ 83 | public val root: RootProject 84 | get() = RootProject() 85 | 86 | /** 87 | * Creates a project dependency on the project at path ":module1" 88 | */ 89 | public val module1: Module1Project 90 | get() = Module1Project() 91 | 92 | public inner class RootProject : ProjectDependencyInternal by project.dependencies.project(mapOf("path" to ":")) as ProjectDependencyInternal { 93 | /** 94 | * Returns the path to the project as a string. 95 | */ 96 | public val path: String = ":" 97 | } 98 | 99 | public inner class Module1Project : ProjectDependencyInternal by project.dependencies.project(mapOf("path" to ":module1")) as ProjectDependencyInternal { 100 | /** 101 | * Returns the path to the project as a string. 102 | */ 103 | public val path: String = ":module1" 104 | } 105 | } 106 | """, 107 | compile = false 108 | ) 109 | } 110 | 111 | @Test 112 | fun `with projects`() { 113 | test( 114 | setOf( 115 | ":", 116 | ":module1", 117 | ":module2", 118 | ":module2:submodule1", 119 | ":module2:submodule2", 120 | ":module2:submodule2:subsubmodule1", 121 | ":module2:submodule3:subsubmodule1", 122 | ), 123 | """ 124 | package com.example 125 | 126 | import kotlin.String 127 | import org.gradle.api.Project 128 | import org.gradle.api.`internal`.artifacts.dependencies.ProjectDependencyInternal 129 | 130 | /** 131 | * Returns the project dependencies for the parent project. 132 | */ 133 | internal val Project.projects: ProjectsAccessors 134 | get() = ProjectsAccessors(this) 135 | 136 | internal class ProjectsAccessors( 137 | private val project: Project, 138 | ) { 139 | /** 140 | * Creates a project dependency on the project at path ":" 141 | */ 142 | public val root: RootProject 143 | get() = RootProject() 144 | 145 | /** 146 | * Creates a project dependency on the project at path ":module1" 147 | */ 148 | public val module1: Module1Project 149 | get() = Module1Project() 150 | 151 | /** 152 | * Creates a project dependency on the project at path ":module2" 153 | */ 154 | public val module2: Module2Project 155 | get() = Module2Project() 156 | 157 | public inner class RootProject : ProjectDependencyInternal by project.dependencies.project(mapOf("path" to ":")) as ProjectDependencyInternal 158 | 159 | public inner class Module1Project : ProjectDependencyInternal by project.dependencies.project(mapOf("path" to ":module1")) as ProjectDependencyInternal 160 | 161 | public inner class Module2Project : ProjectDependencyInternal by project.dependencies.project(mapOf("path" to ":module2")) as ProjectDependencyInternal { 162 | /** 163 | * Creates a project dependency on the project at path ":module2:submodule1" 164 | */ 165 | public val submodule1: Submodule1Project 166 | get() = Submodule1Project() 167 | 168 | /** 169 | * Creates a project dependency on the project at path ":module2:submodule2" 170 | */ 171 | public val submodule2: Submodule2Project 172 | get() = Submodule2Project() 173 | 174 | /** 175 | * Creates a project dependency on the project at path ":module2:submodule3" 176 | */ 177 | public val submodule3: Submodule3Project 178 | get() = Submodule3Project() 179 | 180 | public inner class Submodule1Project : ProjectDependencyInternal by project.dependencies.project(mapOf("path" to ":module2:submodule1")) as ProjectDependencyInternal 181 | 182 | public inner class Submodule2Project : ProjectDependencyInternal by project.dependencies.project(mapOf("path" to ":module2:submodule2")) as ProjectDependencyInternal { 183 | /** 184 | * Creates a project dependency on the project at path ":module2:submodule2:subsubmodule1" 185 | */ 186 | public val subsubmodule1: Subsubmodule1Project 187 | get() = Subsubmodule1Project() 188 | 189 | public inner class Subsubmodule1Project : ProjectDependencyInternal by project.dependencies.project(mapOf("path" to ":module2:submodule2:subsubmodule1")) as ProjectDependencyInternal 190 | } 191 | 192 | public inner class Submodule3Project { 193 | /** 194 | * Returns the path to the project as a string. 195 | * 196 | * Please note that ":module2:submodule3" is not declared project so this path cannot be used 197 | * as a dependency, this accessor is here for convenience. 198 | */ 199 | public val path: String = ":module2:submodule3" 200 | 201 | /** 202 | * Creates a project dependency on the project at path ":module2:submodule3:subsubmodule1" 203 | */ 204 | public val subsubmodule1: Subsubmodule1Project 205 | get() = Subsubmodule1Project() 206 | 207 | public inner class Subsubmodule1Project : ProjectDependencyInternal by project.dependencies.project(mapOf("path" to ":module2:submodule3:subsubmodule1")) as ProjectDependencyInternal 208 | } 209 | } 210 | } 211 | """ 212 | ) 213 | } 214 | 215 | @Test 216 | fun `with dashes`() { 217 | test( 218 | setOf(":", ":some-module"), 219 | """ 220 | package com.example 221 | 222 | import org.gradle.api.Project 223 | import org.gradle.api.`internal`.artifacts.dependencies.ProjectDependencyInternal 224 | 225 | /** 226 | * Returns the project dependencies for the parent project. 227 | */ 228 | internal val Project.projects: ProjectsAccessors 229 | get() = ProjectsAccessors(this) 230 | 231 | internal class ProjectsAccessors( 232 | private val project: Project, 233 | ) { 234 | /** 235 | * Creates a project dependency on the project at path ":" 236 | */ 237 | public val root: RootProject 238 | get() = RootProject() 239 | 240 | /** 241 | * Creates a project dependency on the project at path ":some-module" 242 | */ 243 | public val someModule: SomeModuleProject 244 | get() = SomeModuleProject() 245 | 246 | public inner class RootProject : ProjectDependencyInternal by project.dependencies.project(mapOf("path" to ":")) as ProjectDependencyInternal 247 | 248 | public inner class SomeModuleProject : ProjectDependencyInternal by project.dependencies.project(mapOf("path" to ":some-module")) as ProjectDependencyInternal 249 | } 250 | """ 251 | ) 252 | } 253 | 254 | @Test 255 | fun `with underscore`() { 256 | test( 257 | setOf(":some_module"), 258 | """ 259 | package com.example 260 | 261 | import org.gradle.api.Project 262 | import org.gradle.api.`internal`.artifacts.dependencies.ProjectDependencyInternal 263 | 264 | /** 265 | * Returns the project dependencies for the parent project. 266 | */ 267 | internal val Project.projects: ProjectsAccessors 268 | get() = ProjectsAccessors(this) 269 | 270 | internal class ProjectsAccessors( 271 | private val project: Project, 272 | ) { 273 | /** 274 | * Creates a project dependency on the project at path ":" 275 | */ 276 | public val root: RootProject 277 | get() = RootProject() 278 | 279 | /** 280 | * Creates a project dependency on the project at path ":some_module" 281 | */ 282 | public val someModule: SomeModuleProject 283 | get() = SomeModuleProject() 284 | 285 | public inner class RootProject : ProjectDependencyInternal by project.dependencies.project(mapOf("path" to ":")) as ProjectDependencyInternal 286 | 287 | public inner class SomeModuleProject : ProjectDependencyInternal by project.dependencies.project(mapOf("path" to ":some_module")) as ProjectDependencyInternal 288 | } 289 | """ 290 | ) 291 | } 292 | 293 | @Test 294 | fun `with more things in settings`() { 295 | test( 296 | setOf(":project-accessors"), 297 | """ 298 | package com.example 299 | 300 | import org.gradle.api.Project 301 | import org.gradle.api.`internal`.artifacts.dependencies.ProjectDependencyInternal 302 | 303 | /** 304 | * Returns the project dependencies for the parent project. 305 | */ 306 | internal val Project.projects: ProjectsAccessors 307 | get() = ProjectsAccessors(this) 308 | 309 | internal class ProjectsAccessors( 310 | private val project: Project, 311 | ) { 312 | /** 313 | * Creates a project dependency on the project at path ":" 314 | */ 315 | public val root: RootProject 316 | get() = RootProject() 317 | 318 | /** 319 | * Creates a project dependency on the project at path ":project-accessors" 320 | */ 321 | public val projectAccessors: ProjectAccessorsProject 322 | get() = ProjectAccessorsProject() 323 | 324 | public inner class RootProject : ProjectDependencyInternal by project.dependencies.project(mapOf("path" to ":")) as ProjectDependencyInternal 325 | 326 | public inner class ProjectAccessorsProject : ProjectDependencyInternal by project.dependencies.project(mapOf("path" to ":project-accessors")) as ProjectDependencyInternal 327 | } 328 | """ 329 | ) 330 | } 331 | 332 | @OptIn(ExperimentalCompilerApi::class) 333 | private fun test( 334 | projectPaths: Set, 335 | expected: String, 336 | compile: Boolean = true 337 | ) { 338 | val output = generator.generate(projectPaths).toString().trim() 339 | assertThat(output).isEqualTo(expected.trimIndent()) 340 | if (!compile) return 341 | val result = KotlinCompilation().run { 342 | sources = listOf(SourceFile.kotlin("ProjectsAccessors.kt", expected)) 343 | inheritClassPath = true 344 | compile() 345 | } 346 | assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) 347 | println(result.outputDirectory.resolve("com/example").list()?.asList()) 348 | result.classLoader.loadClass("com.example.ProjectsAccessors") 349 | result.classLoader.loadClass("com.example.ProjectsAccessorsKt") 350 | } 351 | } -------------------------------------------------------------------------------- /project-accessors/src/test/resources/junit-platform.properties: -------------------------------------------------------------------------------- 1 | junit.jupiter.execution.parallel.enabled=true -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | @file:Suppress("UnstableApiUsage") 2 | 3 | pluginManagement { 4 | repositories { 5 | mavenCentral() 6 | gradlePluginPortal() 7 | } 8 | } 9 | 10 | plugins { 11 | id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" 12 | } 13 | 14 | dependencyResolutionManagement { 15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 16 | 17 | repositories { 18 | mavenCentral() 19 | } 20 | } 21 | 22 | rootProject.name = "project-accessors" 23 | 24 | include(":project-accessors") --------------------------------------------------------------------------------