diff options
author | Daniel Santiago Rivera <danysantiago@google.com> | 2023-02-07 17:05:51 +0000 |
---|---|---|
committer | Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com> | 2023-02-07 17:05:51 +0000 |
commit | 3a0794f490651afc8490a95f1a66a78526bed5b1 (patch) | |
tree | 32591b66c39ac18e0ab8aa8387dcfc6e07812cb5 | |
parent | fe5ac83ed28793b359d3f844ab46015ca4f30fa1 (diff) | |
parent | 3870e30a312431f6651dbd26812563e4214cef00 (diff) | |
download | kotlinpoet-android14-d1-s7-release.tar.gz |
Initial import of kotlinpoet from upstream master am: 8b6cde9a0e am: 920796b32b am: 3870e30a31frc_340821000frc_340819280frc_340819220frc_340819190frc_340819030frc_340819020frc_340819010frc_340818170frc_340818110android-vts-14.0_r4android-vts-14.0_r3android-vts-14.0_r2android-vts-14.0_r1android-security-14.0.0_r9android-security-14.0.0_r8android-security-14.0.0_r7android-security-14.0.0_r6android-security-14.0.0_r5android-security-14.0.0_r4android-security-14.0.0_r3android-security-14.0.0_r2android-security-14.0.0_r1android-platform-14.0.0_r7android-platform-14.0.0_r6android-platform-14.0.0_r5android-platform-14.0.0_r4android-platform-14.0.0_r3android-platform-14.0.0_r2android-platform-14.0.0_r1android-cts-14.0_r4android-cts-14.0_r3android-cts-14.0_r2android-cts-14.0_r1android-14.0.0_r9android-14.0.0_r8android-14.0.0_r7android-14.0.0_r6android-14.0.0_r5android-14.0.0_r45android-14.0.0_r44android-14.0.0_r43android-14.0.0_r42android-14.0.0_r41android-14.0.0_r40android-14.0.0_r4android-14.0.0_r39android-14.0.0_r38android-14.0.0_r3android-14.0.0_r28android-14.0.0_r27android-14.0.0_r26android-14.0.0_r25android-14.0.0_r24android-14.0.0_r23android-14.0.0_r22android-14.0.0_r21android-14.0.0_r20android-14.0.0_r2android-14.0.0_r19android-14.0.0_r18android-14.0.0_r17android-14.0.0_r16android-14.0.0_r15android-14.0.0_r14android-14.0.0_r13android-14.0.0_r12android-14.0.0_r11android-14.0.0_r10android-14.0.0_r1aml_wif_341410080aml_wif_341310010aml_wif_341110010aml_wif_341011010aml_wif_340913010aml_uwb_341310300aml_uwb_341310030aml_uwb_341111010aml_uwb_341011000aml_tet_341411060aml_tet_341310230aml_tet_341112070aml_tet_341010040aml_tet_340913030aml_swc_341312300aml_swc_341312020aml_swc_341111000aml_swc_341011020aml_swc_340922010aml_sta_341410000aml_sta_341311010aml_sta_341114000aml_sta_341111000aml_sta_341010020aml_sta_340912000aml_sta_340911000aml_sdk_341410000aml_sdk_341110080aml_sdk_341110000aml_sdk_341010000aml_sdk_340912010aml_rkp_341311000aml_rkp_341114000aml_rkp_341015010aml_rkp_341012000aml_res_341410010aml_res_341311030aml_res_341110000aml_res_340912000aml_per_341410020aml_per_341311000aml_per_341110020aml_per_341110010aml_per_341011100aml_per_341011020aml_per_340916010aml_neu_341010080aml_neu_341010000aml_net_341411030aml_net_341311010aml_net_341310020aml_net_341111030aml_net_341014000aml_net_340913000aml_mpr_341411070aml_mpr_341313030aml_mpr_341111030aml_mpr_341111020aml_mpr_341015090aml_mpr_341015030aml_mpr_340919000aml_med_341312300aml_med_341312020aml_med_341111000aml_med_341011000aml_med_340922010aml_ips_340914280aml_ips_340914200aml_ips_340914000aml_hef_341415040aml_hef_341311010aml_hef_341114030aml_ext_341414010aml_ext_341317010aml_ext_341131030aml_ext_341027030aml_doc_341312010aml_doc_341112000aml_doc_341012000aml_doc_340916000aml_con_341310090aml_con_341110000aml_cbr_341410010aml_cbr_341311010aml_cbr_341110000aml_cbr_341011000aml_cbr_340914000aml_ase_341410000aml_ase_341310010aml_ase_341113000aml_ase_340913000aml_art_341411300aml_art_341311100aml_art_341110110aml_art_341110060aml_art_341010050aml_art_340915060aml_ads_341413000aml_ads_341316030aml_ads_341131050aml_ads_341027030aml_ads_340915050aml_adb_340912530aml_adb_340912350aml_adb_340912200aml_adb_340912000android14-tests-releaseandroid14-security-releaseandroid14-s2-releaseandroid14-s1-releaseandroid14-releaseandroid14-qpr1-s2-releaseandroid14-qpr1-releaseandroid14-platform-releaseandroid14-mainline-wifi-releaseandroid14-mainline-uwb-releaseandroid14-mainline-tethering-releaseandroid14-mainline-sdkext-releaseandroid14-mainline-resolv-releaseandroid14-mainline-permission-releaseandroid14-mainline-os-statsd-releaseandroid14-mainline-networking-releaseandroid14-mainline-mediaprovider-releaseandroid14-mainline-media-swcodec-releaseandroid14-mainline-media-releaseandroid14-mainline-healthfitness-releaseandroid14-mainline-extservices-releaseandroid14-mainline-cellbroadcast-releaseandroid14-mainline-art-releaseandroid14-mainline-appsearch-releaseandroid14-mainline-adservices-releaseandroid14-mainline-adbd-releaseandroid14-gsiandroid14-devandroid14-d2-s5-releaseandroid14-d2-s4-releaseandroid14-d2-s3-releaseandroid14-d2-s2-releaseandroid14-d2-s1-releaseandroid14-d2-releaseandroid14-d1-s7-releaseandroid14-d1-s6-releaseandroid14-d1-s5-releaseandroid14-d1-s4-releaseandroid14-d1-s3-releaseandroid14-d1-s2-releaseandroid14-d1-s1-releaseandroid14-d1-release
Original change: https://android-review.googlesource.com/c/platform/external/kotlinpoet/+/2413152
Change-Id: Ic663a7bfaf78e703948aa55c9b9555d87a035ebd
Signed-off-by: Automerger Merge Worker <android-build-automerger-merge-worker@system.gserviceaccount.com>
161 files changed, 39922 insertions, 0 deletions
diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..3ac56613 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_size = 2 +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.{kt,kts}] +ij_kotlin_imports_layout = * +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = true diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..27fc7c29 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,4 @@ +* text=auto eol=lf + +*.bat text eol=crlf +*.jar binary
\ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 00000000..cbe36f66 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,20 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: bug +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +A sample Github project that reproduces the problem is ideal. Alternatively, please provide a failing unit test that triggers the bug. + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 00000000..11fc491e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..46c31895 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,97 @@ +name: Build + +on: [push, pull_request] + +env: + GRADLE_OPTS: "-Dorg.gradle.jvmargs=-Xmx4g -Dorg.gradle.daemon=false -Dkotlin.incremental=false" + +jobs: + jvm: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Validate Gradle Wrapper + uses: gradle/wrapper-validation-action@v1 + + - name: Configure JDK + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: 18 + + - name: Test + run: ./gradlew build + + build-docs: + runs-on: ubuntu-latest + if: github.repository == 'square/kotlinpoet' && github.ref != 'refs/heads/master' + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Configure JDK + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: 18 + + - name: Prep docs + run: ./gradlew dokkaHtml + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.8 + + - name: Build mkdocs + run: | + pip3 install -r .github/workflows/mkdocs-requirements.txt + mkdocs build + + publish: + runs-on: ubuntu-latest + if: github.repository == 'square/kotlinpoet' && github.ref == 'refs/heads/master' + needs: + - jvm + + steps: + - name: Checkout + uses: actions/checkout@v3 + + - name: Configure JDK + uses: actions/setup-java@v3 + with: + distribution: 'zulu' + java-version: 18 + + - name: Upload Artifacts + run: ./gradlew publish + env: + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} + + - name: Prep docs + run: ./gradlew dokkaHtml + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: 3.8 + + - name: Build mkdocs + run: | + pip3 install -r .github/workflows/mkdocs-requirements.txt + mkdocs build + + - name: Deploy 🚀 + if: success() && github.ref == 'refs/heads/master' + uses: JamesIves/github-pages-deploy-action@releases/v3 + with: + GITHUB_TOKEN: ${{ secrets.PERSONAL_TOKEN }} + BRANCH: gh-pages # The branch the action should deploy to. + FOLDER: site # The folder the action should deploy. + SINGLE_COMMIT: true diff --git a/.github/workflows/mkdocs-requirements.txt b/.github/workflows/mkdocs-requirements.txt new file mode 100644 index 00000000..67c640f2 --- /dev/null +++ b/.github/workflows/mkdocs-requirements.txt @@ -0,0 +1,19 @@ +click==8.1.3 +future==0.18.3 +Jinja2==3.1.2 +livereload==2.6.3 +lunr==0.6.2 +Markdown==3.3.7 # See https://github.com/mkdocs/mkdocs/issues/2892. +MarkupSafe==2.1.2 +mkdocs==1.4.2 +mkdocs-macros-plugin==0.7.0 +mkdocs-material==9.0.9 +mkdocs-material-extensions==1.1.1 +Pygments==2.14.0 +pymdown-extensions==9.9.2 +python-dateutil==2.8.2 +PyYAML==6.0 +repackage==0.7.3 +six==1.16.0 +termcolor==2.2.0 +tornado==6.2 diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..75e7808e --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +.classpath +.gradle +.project +.settings +eclipsebin +local.properties + +bin +gen +build +out +lib +reports + +.idea +*.iml +classes + +# Mkdocs files +docs/1.x/* + +obj + +.DS_Store diff --git a/Android.bp b/Android.bp new file mode 100644 index 00000000..ef85457e --- /dev/null +++ b/Android.bp @@ -0,0 +1,14 @@ +package { + default_applicable_licenses: ["external_kotlinpoet_license"], +} + +license { + name: "external_kotlinpoet_license", + visibility: [":__subpackages__"], + license_kinds: [ + "SPDX-license-identifier-Apache-2.0", + ], + license_text: [ + "LICENSE.txt", + ], +} diff --git a/LICENSE b/LICENSE new file mode 120000 index 00000000..85de3d45 --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +LICENSE.txt
\ No newline at end of file diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 00000000..62589edd --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + https://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/METADATA b/METADATA new file mode 100644 index 00000000..95ed8d4d --- /dev/null +++ b/METADATA @@ -0,0 +1,17 @@ +name: "kotlinpoet" +description: + "A Kotlin API for generating .kt source files" + +third_party { + url { + type: HOMEPAGE + value: "https://square.github.io/kotlinpoet/" + } + url { + type: GIT + value: "https://github.com/square/kotlinpoet.git" + } + version: "c99ed244c6e4c7f8594f75566bb19ec62fa9c80d" + last_upgrade_date { year: 2023 month: 1 day: 31 } + license_type: NOTICE +} diff --git a/MODULE_LICENSE_APACHE2 b/MODULE_LICENSE_APACHE2 new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/MODULE_LICENSE_APACHE2 @@ -0,0 +1,3 @@ +krzysio@google.com +danysantiago@google.com +aurimas@google.com diff --git a/README.md b/README.md new file mode 100644 index 00000000..90ee21b7 --- /dev/null +++ b/README.md @@ -0,0 +1,23 @@ +KotlinPoet +========== + +`KotlinPoet` is a Kotlin and Java API for generating `.kt` source files. + +### [square.github.io/kotlinpoet](https://square.github.io/kotlinpoet) + +License +------- + + Copyright 2017 Square, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 00000000..f987b6eb --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,26 @@ +Releasing +========= + + 1. Change the version in `gradle.properties` to a non-SNAPSHOT version. + 2. Update `docs/changelog.md` for the impending release. + 3. `git commit -am "Prepare for release X.Y.Z."` (where X.Y.Z is the new version) + 4. `./gradlew clean publish --no-daemon --no-parallel`. + 5. Visit [Sonatype Nexus][sonatype] and ensure there's only one staging repository. + 6. `./gradlew closeAndReleaseRepository`. + 7. `git tag -a X.Y.Z -m "Version X.Y.Z"` (where X.Y.Z is the new version). + 8. Update `gradle.properties` to the next SNAPSHOT version. + 9. `git commit -am "Prepare next development version."`. + 10. `git push && git push --tags`. + +If steps 5-6 fail, drop the Sonatype repo, fix the problem, commit, and start again at step 4. + + +Prerequisites +------------- + +In `~/.gradle/gradle.properties`, set the following: + + * `ORG_GRADLE_PROJECT_mavenCentralUsername` - Sonatype username for releasing to `com.squareup`. + * `ORG_GRADLE_PROJECT_mavenCentralPassword` - Sonatype password for releasing to `com.squareup`. + + [sonatype]: https://s01.oss.sonatype.org/ diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 00000000..421de38b --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import com.diffplug.gradle.spotless.SpotlessExtension +import org.jetbrains.dokka.gradle.DokkaTask +import org.jetbrains.kotlin.gradle.dsl.KotlinProjectExtension +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + alias(libs.plugins.kotlin.jvm) apply false + alias(libs.plugins.ksp) apply false + alias(libs.plugins.dokka) apply false + alias(libs.plugins.spotless) apply false + alias(libs.plugins.mavenPublish) apply false + alias(libs.plugins.kotlinBinaryCompatibilityValidator) +} + +allprojects { + group = property("GROUP") as String + version = property("VERSION_NAME") as String + + repositories { + mavenCentral() + } +} + +subprojects { + tasks.withType<KotlinCompile> { + kotlinOptions { + freeCompilerArgs += listOf("-opt-in=kotlin.RequiresOptIn") + } + } + // Ensure "org.gradle.jvm.version" is set to "8" in Gradle metadata. + tasks.withType<JavaCompile> { + sourceCompatibility = JavaVersion.VERSION_1_8.toString() + targetCompatibility = JavaVersion.VERSION_1_8.toString() + } + + apply(plugin = "org.jetbrains.kotlin.jvm") + if ("test" !in name && buildFile.exists()) { + apply(plugin = "org.jetbrains.dokka") + apply(plugin = "com.vanniktech.maven.publish") + configure<KotlinProjectExtension> { + explicitApi() + } + afterEvaluate { + tasks.named<DokkaTask>("dokkaHtml") { + val projectFolder = project.path.trim(':').replace(':', '-') + outputDirectory.set(rootProject.rootDir.resolve("docs/1.x/$projectFolder")) + dokkaSourceSets.configureEach { + skipDeprecated.set(true) + } + } + } + } + + apply(plugin = "com.diffplug.spotless") + configure<SpotlessExtension> { + kotlin { + target("**/*.kt") + ktlint(libs.versions.ktlint.get()).editorConfigOverride( + mapOf("ktlint_standard_filename" to "disabled"), + ) + trimTrailingWhitespace() + endWithNewline() + + licenseHeader( + """ + |/* + | * Copyright (C) ${'$'}YEAR Square, Inc. + | * + | * Licensed under the Apache License, Version 2.0 (the "License"); + | * you may not use this file except in compliance with the License. + | * You may obtain a copy of the License at + | * + | * https://www.apache.org/licenses/LICENSE-2.0 + | * + | * Unless required by applicable law or agreed to in writing, software + | * distributed under the License is distributed on an "AS IS" BASIS, + | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + | * See the License for the specific language governing permissions and + | * limitations under the License. + | */ + """.trimMargin() + ) + } + } + + // Copied from https://github.com/square/retrofit/blob/master/retrofit/build.gradle#L28. + // Create a test task for each supported JDK. + for (majorVersion in 8..18) { + // Adoptium JDK 9 cannot extract on Linux or Mac OS. + if (majorVersion == 9) continue + // Started causing build failures in late 2022, e.g.: + // https://github.com/square/kotlinpoet/actions/runs/3816320722/jobs/6531532305. + if (majorVersion == 10) continue + + val jdkTest = tasks.register<Test>("testJdk$majorVersion") { + val javaToolchains = project.extensions.getByType(JavaToolchainService::class) + javaLauncher.set(javaToolchains.launcherFor { + languageVersion.set(JavaLanguageVersion.of(majorVersion)) + }) + + description = "Runs the test suite on JDK $majorVersion" + group = LifecycleBasePlugin.VERIFICATION_GROUP + + // Copy inputs from normal Test task. + val testTask = tasks.getByName<Test>("test") + classpath = testTask.classpath + testClassesDirs = testTask.testClassesDirs + } + tasks.named("check").configure { + dependsOn(jdkTest) + } + } +} + +apiValidation { + nonPublicMarkers += "com.squareup.kotlinpoet.ExperimentalKotlinPoetApi" + ignoredProjects += listOf( + "interop", // Empty middle package + "test-processor" // Test only + ) +} diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 00000000..05680e65 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,588 @@ +Change Log +========== + +## Version 1.12.0 + +_2022-06-13_ + +Thanks to [@WhosNickDoglio][WhosNickDoglio], [@sullis][sullis], [@DRSchlaubi][DRSchlaubi], +[@martinbonnin][martinbonnin], [@seriouslyhypersonic][seriouslyhypersonic], [@ephemient][ephemient], +[@dkilmer][dkilmer], [@aksh1618][aksh1618], [@zsqw123][zsqw123], [@roihershberg][roihershberg] for +contributing to this release. + + * New: Kotlin 1.7.0. + * New: Add support for context receivers. + * New: Add support for external property getter. + * New: `interop-ksp` API promoted to stable. + * Fix: Resolve enum constants when emitting types. + * Fix: Fix type argument mapping when processing typealiases with KSP. + * Fix: Properly unwrap `KSTypeAlias` with an unused type parameter. + * Fix: Unwrap nested `KSTypeAlias`-es recursively. + * Fix: Add support for context receivers `@PropertySpec` and fix issues with annotations. + * Fix: Treat `header` and `impl` as keywords (workaround for KT-52315). + * Fix: Use `%N` instead of `%L` for annotation arg names so keywords are handled. + * Fix: Improve handling of long `return` expressions. + +## Version 1.11.0 + +_2022-03-24_ + +Thanks to [@liujingxing][liujingxing] and [@BoD][BoD] for contributing to this release. + +* New: Kotlin scripting support in `FileSpec`. + +```kotlin +val spec = FileSpec.scriptBuilder("Taco") + .addStatement("println(%S)", "hello world!") + .addKotlinDefaultImports() + .build() +``` + +Generates a `Taco.kts` file with the following contents: + +```kotlin +println("hello world!") +``` + +* New: Emit trailing commas for multi-line parameters and annotations. +* New: Add `KSAnnotation.toAnnotationSpec()`. +* New: Add `Unit` and `CharSequence` conversions in `javapoet-interop`. +* New: Add support for default imports in `FileSpec`. + * This is particularly oriented at scripting support, but can also be used in non-script files. +* New: Update to Kotlin 1.6.10. +* Fix: Fail compilation if you only pass one string to `ClassName`. +* Fix: Inline `val` property if its getter is `inline`. +* Fix: Add `yield` to the list of reserved keywords. +* Fix: Enforce only allowed parameter modifiers in `ParameterSpec` (i.e. `crossinline`, `vararg`, and `noinline`). +* Fix: Fix `CodeBlock`s in class delegation getting `toString()`'d instead of participating in code writing. +* Fix: Error when attempting to convert KSP error types (i.e. if `KSType.isError` is true) to `TypeName`. + +## Version 1.10.2 + +_2021-10-22_ + +Thanks to [@glureau][glureau] and [@goooler][goooler] for contributing to this release. + +* New: Switch `AnnotationSpec.get()` to use the `arrayOf()` syntax instead of `[]`. +* Fix: Don't wrap aliasing imports with long package names. +* Fix: Don't wrap type names inside line comments. +* Fix: Ignore Java's `@Deprecated` annotations on synthetic methods for annotations. + +## Version 1.10.1 + +_2021-09-21_ + +Thanks to [@evant][evant] for contributing to this release. + + * Fix: Correct generation of typealiases with type args in KSP interop. + * Fix: Add missing default `TypeParameterResolver.EMPTY` argument to + `fun KSTypeArgument.toTypeName` in KSP interop. + +## Version 1.10.0 + +_2021-09-20_ + +Thanks to [@martinbonnin][martinbonnin], [@idanakav][idanakav], [@goooler][goooler], and +[@anandwana001][anandwana001] for contributing to this release. + + * New: Add a new [KSP][ksp] interop artifact. See [docs][ksp-interop-docs] for more details. + * New: Add a new [JavaPoet][javapoet] interop artifact. See [docs][javapoet-interop-docs] for more + details. + * New: Allow copying a `ParameterizedTypeName` with new type arguments via new `copy()` overload. + * kotlinx-metadata artifacts have been consolidated to a single `com.squareup:kotlinpoet-metadata` + maven artifact. The previous `kotlinpoet-metadata-*` subartifacts are no longer published. + * New: `TypeNameAliasTag` has been moved to KotlinPoet's main artifact under `TypeAliasTag`, for + reuse with KSP interop. + * `ImmutableKm*` classes have been removed. They were deemed to be a needless abstraction over the base `kotlinx-metadata` Km types. All usages of these should be substituted with their non-immutable base types. + * Fix: Fix self-referencing type variables in metadata parsing. + * Fix: Use delicate APIs rather than noisy logging ones when converting annotation mirrors in + `AnnotationSpec.get`. + * Fix: Update error message when metadata cannot be read to a more actionable one. + * Fix: Avoid escaping already escaped strings. + * Add docs about `kotlin-reflect` usage. + * Avoid using kotlin-reflect for looking up `Unit` types where possible. + * Test all the way up to JDK 17. + * Update Kotlin to 1.5.31. + +## Version 1.9.0 + +_2021-06-22_ + + * New: Kotlin 1.5.10. + * New: Previously deprecated API to interop with Java reflection and Mirror API have been + un-deprecated and marked with `@DelicateKotlinPoetApi` annotation. + * New: `CodeBlock.Builder.withIndent` helper function. + * New: Allow changing initializers and default values in `ParameterSpec.Builder` and + `PropertySpec.Builder` after they were set. + * New: `MemberName.isExtension` property that instructs KotlinPoet to always import the member, + even if conflicting declarations are present in the same scope. + * Fix: Escape member names that only contain underscores. + * Fix: Always emit an empty primary constructor if it was set via `TypeSpec.primaryConstructor`. + +## Version 1.8.0 + +_2021-03-29_ + + * New: Kotlin 1.4.31. + * New: Add `KModifier.VALUE` to support `value class` declarations. + * New: Allow using a custom `ClassLoader` with `ReflectiveClassInspector`. + * New: Update to kotlinx-metadata 0.2.0. + * Fix: Ensure `ImmutableKmProperty.toMutable()` copies `fieldSignature`. + * Fix: Prevent name clashes between an imported `MemberName` and a member in current scope. + * Fix: Prevent name clashes between a type and a supertype with the same name. + * Fix: Don't generate empty body for `expect` and `external` functions. + * Fix: Don't allow `expect` or `external` classes to initialize supertypes. + * Fix: Disallow delegate constructor calls in `external` classes. + * Fix: Allow non-public primary constructors inside inline/value classes. + * Fix: Allow init blocks inside inline/value classes. + * Fix: Omit redundant `abstract` modifiers on members inside interfaces + +## Version 1.7.2 + +_2020-10-20_ + + * New: Detect expression bodies with `return·` and `throw·` prefixes. + * Fix: Omit visibility modifiers on custom accessors. + +## Version 1.7.1 + +_2020-10-15_ + + * Fix: 1.7.0 was published using JDK 11 which set `"org.gradle.jvm.version"` to `"11"` in Gradle + metadata, making it impossible to use the library on earlier Java versions (see + [#999][issue-999]). 1.7.1 is published with JDK 8, which fixes the problem. + +## Version 1.7.0 + +_2020-10-14_ + + * New: Kotlin 1.4.10. + * New: Generated code is now compatible with the [explicit API mode][explicit-api-mode] by default. + * New: Escape soft and modifier keywords, in addition to hard keywords. + * New: Improve enum constants generation for cleaner diffs. + * New: Disallow setters on immutable properties. + * New: Ensure trailing new lines in expression bodies. + * New: Ensure trailing new lines after parameterless custom setters. + * Fix: Don't auto-convert properties with custom accessors to primary constructor properties. + * Fix: Don't allow parameterless setters with body. + * Fix: Prevent auto-wrapping spaces inside escaped keywords. + +## Version 1.6.0 + +_2020-05-28_ + + * New: Deprecate Mirror API integrations. + + Mirror API integrations, such as `TypeElement.asClassName()` and + `FunSpec.overriding(ExecutableElement)`, are being deprecated in this release. These KotlinPoet + APIs are most often used in annotation processors. Since kapt runs annotation processors over + stubs, which are Java files, a lot of the Kotlin-specific information gets lost in translation + and cannot be accessed by KotlinPoet through the Mirror API integrations. Examples include: + + - Alias types, such as `kotlin.String`, get converted to their JVM representations, such as + `java.lang.String`. + - Type nullability information is not accessible. + - `suspend` functions are seen as simple functions with an additional `Continuation` parameter. + + The correct solution is to switch to [KotlinPoet-metadata][kotlinpoet-metadata] or + [KotlinPoet-metadata-specs][kotlinpoet-metadata-specs] API, which fetches Kotlin-specific + information from the `@Metadata` annotation and produces correct KotlinPoet Specs. We may explore + adding new metadata-based alternatives to the deprecated APIs in the future. + + * New: Kotlin 1.3.72. + * New: Improve `MemberName` to support operator overloading. + * New: Support generics in `AnnotationSpec`. + * New: Add support for functional interfaces. + * New: Make more `FunSpec.Builder` members public for easier mutation. + * Fix: Properly propagate implicit type and function modifiers in nested declarations. + * Fix: Properly escape type names containing `$` character. + * Fix: Don't emit `LambdaTypeName` annotations twice. + * Fix: Preserve tags in `TypeName.copy()`. + +## Version 1.5.0 + +_2020-01-09_ + + KotlinPoet now targets JDK8, which means that executing a build that includes KotlinPoet as a + dependency on a machine with an older version of JDK installed won't work. **This has no effect on + the code that KotlinPoet produces**: the code can still be compiled against JDK6, as long as it + doesn't use any features that were introduced in newer releases. + + * New: Kotlin 1.3.61. + * New: Add support for processing FileFacades in KotlinPoet-metadata. + * New: Add support for inner nested and companion objects on annotation classes. + * New: Improve error messages for mismatched open/close statement characters. + * New: Tag `AnnotationSpec`s with the annotation mirror when available. + * New: Include annotations on enum entries when creating `TypeSpec`s from metadata. + * Fix: Fix metadata parsing for types. + * Fix: Allow file names that are Kotlin keywords. + * Fix: Properly escape type alias names with backticks. + * Fix: Allow creating `TypeSpec`s with names that can be escaped with backticks. + * Fix: Properly escape enum constant names with backticks. + * Fix: Maintain proper ordering of properties and initializers when emitting a `TypeSpec`. + **Note**: with this change, any properties declared after any initializer blocks will not be + added to the primary constructor and will instead be emitted inside the `TypeSpec` body. + * Fix: Don't emit a leading new line if type KDoc is empty but parameter KDocs are present. + * Fix: Ensure KotlinPoet-metadata resolves package names properly. + + ## Version 1.4.4 + +_2019-11-16_ + + * Fix: Support reified inline types in KotlinPoet-metadata. + +## Version 1.4.3 + +_2019-10-30_ + + * Fix: Don't emit stubs for abstract functions in KotlinPoet-metadata. + +## Version 1.4.2 + +_2019-10-28_ + + * Fix: Properly handle abstract elements in KotlinPoet-metadata. + * Fix: Properly handle typealiases in KotlinPoet-metadata. + * Fix: Properly render % symbols at the end of KDocs. + +## Version 1.4.1 + +_2019-10-18_ + + * New: Add annotations support to `TypeAliasSpec`. + * New: Read type annotations from Kotlin `Metadata`. + * New: Introduce `ImmutableKmDeclarationContainer`. + * Fix: Use full package name for shading `auto-common`. + * Fix: Support reading self-type variables (e.g. `Asset<A : Asset<A>>`) from Kotlin `Metadata`. + +## Version 1.4.0 + +_2019-09-24_ + + * New: This release introduces the new KotlinPoet-metadata API that makes it easy to introspect + Kotlin types and build KotlinPoet Specs based on that data. + + The strategy for type introspection is driven by `ClassInspector`, which is a basic interface for + looking up JVM information about a given Class. This optionally is used by the + `toTypeSpec()`/`toFileSpec()` APIs in `kotlinpoet-metadata-specs` artifact to inform about + Classes with information that isn’t present in metadata (overrides, JVM modifiers, etc). There + are two batteries-included implementations available in `ReflectiveClassInspector` + (for reflection) and `ElementsClassInspector` (for the javax Elements API in annotation + processing). These implementations are available through their respective + `kotlinpoet-classinspector-*` artifacts. For more information refer to the + [KotlinPoet-metadata-specs README][kotlinpoet-metadata-specs]. + + At the time of this release the API is in experimental mode and has to be opted into via the + `KotlinPoetMetadataPreview` annotation. + + * New: Kotlin 1.3.50. + * New: A new constructor to simplify creation of `ParameterSpec` instances. + * New: New `ClassName` constructors. + * New: `TypeName` and subclasses can now store tags. + * New: Optional parameters added to `toBuilder()` methods of most Specs. + * New: `List` overrides for Spec methods that accept `vararg`s. + * New: `CodeBlock.Builder.clear()` helper method. + * New: `FunSpec.Builder.clearBody()` helper method. + * Fix: Properly escape enum constant names. + * Fix: Ensure trailing newlines in KDoc and function bodies. + * Fix: `TypeVariableName`s with empty bounds will now default to `Any?`. + * Fix: Don't emit parens for primary constructors. + * Fix: `ClassName`s with empty simple names are not allowed anymore. + * Fix: Throw if names contain illegal characters that can't be escaped with backticks. + +## Version 1.3.0 + +_2019-05-30_ + + * New: Don't inline annotations in the primary constructor. + * New: Force new lines when emitting primary constructors. + * New: Support using MemberNames as arguments to %N. + * New: Add more ClassName constants: ClassName.STRING, ClassName.LIST, etc. + * New: Add ClassName.constructorReference() and MemberName.reference(). + * New: Make %N accept MemberNames. + * New: Escape spaces in import aliases. + * New: Escape spaces in ClassNames. + * New: Escape spaces in MemberNames. + * New: Escape imports containing spaces. + * New: Escape package name containing spaces. + * New: Use 2-space indents. + * New: Only indent one level on annotation values. + * Fix: Pass only unique originating elements to Filer. + * Fix: Fix bug with MemberNames in same package nested inside a class. + +## Version 1.2.0 + +_2019-03-28_ + + * New: Add writeTo(Filer) and originating element API. + * New: Make *Spec types taggable. + * New: Make FunSpec.Builder#addCode take vararg Any?. + * Fix: Import members from default package. + * Fix: Add non-wrapping spaces in control flow creation methods. + * Fix: Named "value" argument being omitted in annotation array types. + +## Version 1.1.0 + +_2019-02-28_ + + * New: Kotlin 1.3.21. + * New: Support referencing members using `%M` and `MemberName` type. + * New: Add extensions for getting a `MemberName` from a `ClassName`, `KClass` and `Class`. + * New: Allow passing `CodeBlock`s as arguments to `%P`. + * New: Allow interface delegation for objects. + * Fix: Don't emit visible whitespace in `toString()`. + * Fix: Prevent line wrapping in weird places inside function signature. + * Fix: No line wrapping between val and property name. + * Fix: Allow passing line prefix into `LineWrapper` to enable proper line wrapping in KDoc. + * Fix: Add newline for `TypeSpec` Kdoc with no tags. + * Fix: Add newline for remaining Specs. + * Fix: Fix kdoc formatting for property getter/setters. + * Fix: Don't wrap single line comments inside `FunSpec`. + * Fix: Add non-wrapping package name. + * Fix: Remove n^2 algorithm in `CodeWriter.resolve()` by precomputing all of the nested simple names of a `TypeSpec`. + * Fix: Fix edge case with empty enum classes. + * Fix: Fix Nullable Type Parameter handling in `KType.asTypeName()`. + * Fix: Fix incorrect long comment wrapping in `FileSpec`. + * Fix: Attach primary constructor param/property KDoc to the element vs emitting it inside the type header. + +## Version 1.0.1 + +_2019-01-02_ + + * New: Allow enums without constants. + * New: Improved formatting of TypeSpec KDoc. + * New: Support @property and @param KDoc tags in TypeSpec. + * Fix: Use pre-formatted strings for arguments to %P. + +## Version 1.0.0 + +_2018-12-10_ + + * New: Kotlin 1.3.11. + * Fix: Prevent wrapping in import statements. + +## Version 1.0.0-RC3 + +_2018-11-28_ + + * New: Kotlin 1.3.10. + * New: Add `%P` placeholder for string templates. + * New: Add support for receiver kdoc. + * New: Avoid emitting `Unit` as return type. + * New: Add support for empty setters. + * New: Add checks for inline classes. + * New: Escape property and variable names if keywords. + * New: Replace `%>`, `%<`, `%[`, `%]` placeholders with `⇥`, `⇤`, `«`, `»`. + * New: Replace `%W` with space, and add `·` as a non-breaking space. + * New: Change `TypeName` to sealed class. + * New: Documentation improvements. + * New: Replace `TypeName` modifier methods with `copy()`. + * New: Rename members of `WildcardTypeName` to match with the producer/consumer generics model. + * New: Rename `TypeName.nullable` into `TypeName.isNullable`. + * New: Rename `LambdaTypeName.suspending` into `LambdaTypeName.isSuspending`. + * New: Rename `TypeVariableName.reified` into `TypeVariableName.isReified`. + * Fix: Emit star-projection only for types with `Any?` upper bound. + * Fix: Fold property with escaped name. + +## Version 1.0.0-RC2 + +_2018-10-22_ + + * New: Kotlin 1.2.71. + * New: README improvements. + * New: Allow opening braces and params in `beginControlFlow()`. + * New: Add KDoc to `ParameterSpec`, collapse into parent KDoc. + * New: Support `TypeVariable`s in `PropertySpec`. + * New: Add parens for annotated types in `LambdaTypeName`. + * New: Improve error messaging and documentation for inline properties. + * New: Allow sealed classes to declare abstract properties. + * New: Added `buildCodeBlock()` helper function. + * New: Allow using `CodeBlock`s with statements as property initializers and default parameter values. + * New: Rename `NameAllocator.clone()` into `NameAllocator.copy(). + * New: Rename `TypeName.asNonNullable()` to `TypeName.asNonNull()`. + * New: Remove `PropertySpec.varBuilder()` (use `mutable()` instead). + * New: Allow importing top-level members in default package. + * New: Add overloads to add KDoc to return type. + * Fix: Distinguishing `IntArray` and `Array<Int>` when creating `TypeName`. + * Fix: Use `TypeName` instead of `ClassName` as parameter type of `plusParameter()`. + * Fix: Keep type-parameter variance when constructing `TypeName` from `KType`. + * Fix: Don't validate modifiers when merging properties with primary constructor parameters. + * Fix: Escape $ characters in formatted strings. + * Fix: `FileSpec.Builder` blank package and subfolder fix. + * Fix: Append new line at end of parameter KDoc. + * Fix: Add parameter KDoc in `toBuilder()`. + +## Version 1.0.0-RC1 + +_2018-07-16_ + + * New: Escape keywords in imports and canonical class names. + * New: Improve `external` support. + * New: Extensions for `KType` and `KTypeParameter`. + * New: Add builder methods to simplify adding common kotlin.jvm annotations. + * New: Enums are able to have companion objects. + * New: Add missing primaryConstructor & companionObject to `TypeSpec#toBuilder()`. + * New: Make subtype checking vals inside Kind public. + * New: Escape (class/property/function/variable) names automatically if they contain space, hyphen, or other symbols. + * New: Improve `ParameterizedTypeName` API. + * New: Add `WildcardTypeName.STAR` constant. + * New: Expose mutable builder properties and move their validations to build-time. + * Fix: Use regular indents for parameter lists. + * Fix: Inline annotations on properties defined in primary constructor. + * Fix: Use `Any?` as the default type variable bounds. + * Fix: Fix importing annotated `TypeName`. + * Fix: If any primary constructor property has KDoc, put properties on new lines. + * Fix: Properly emit where block in type signature. + * Fix: Avoid type name collisions in primary constructor. + * Fix: Remove implicit `TypeVariable` bound when more bounds are added. + * Fix: Combine annotations and modifiers from constructor params and properties. + * Fix: Replace delegate constructor args along with the constructor. + +## Version 0.7.0 + +_2018-02-16_ + + * New: Increase indent to 4 spaces. + * New: Delegate super interfaces as constructor parameters. + * New: Support `PropertySpec`s as `CodeBlock` literals. + * New: Support KDoc for `TypeAliasSpec`. + * New: Allow for adding an initializer block inside a companion object. + * New: Escape name in `ParameterSpec` which is also a keyword. + * New: Escape names in statements. + * New: Set com.squareup.kotlinpoet as automatic module name. + * New: Support suspending lambda types. + * New: Support named `LambdaTypeName` parameters. + * New: Support dynamic type. + * New: Disallow wildcard imports. + * New: Depend on Kotlin 1.2.21. + * Fix: Correct handling of super-classes/interfaces on anonymous classes. + * Fix: Fix boundary filtering to `Any?`. + * Fix: Wrap long property initializers. + * Fix: Fix formatting and indentation of parameter lists. + +## Version 0.6.0 + +_2017-11-03_ + + * New: Support lambda extensions. + * New: Support renames in imports like `import bar.Bar as bBar`. + * New: Support extension and inline properties. + * New: Support reified types. + * New: Expose enclosed types inside `LambdaTypeName`. + * New: Depend on Kotlin Kotlin 1.1.51. + * New: Improved API and formatting of annotations. + * New: Improved multiplatform support. + * Fix: Escape function and package names if they are a Kotlin keyword. + * Fix: Properly format WildcardTypeName's class declaration. + + +## Version 0.5.0 + +_2017-09-13_ + + * New: Rename `addFun()` to `addFunction()`. + * New: Rename `KotlinFile` to `FileSpec`. + * New: Rename `KotlinFile.addFileAnnotation()` to `addAnnotation()`. + * New: Rename `KotlinFile.addFileComment()` to `addComment()`. + * New: Support cross-platform code, including `HEADER` and `IMPL` modifiers. + * New: Support type variables for type aliases. + * New: Support constructor delegation. + * New: Support named companion objects. + * New: Depend on Kotlin 1.1.4-3. + * Fix: Format one parameter per line when there are more than two parameters. + * Fix: Don't emit braces when the constructor body is empty. + * Fix: Do not invoke superclass constructor when no primary constructor. + * Fix: Enforce the right modifiers on functions. + + +## Version 0.4.0 + +_2017-08-08_ + + * New: Change KotlinPoet's extensions like `asClassName()` to be top-level functions. + * New: Add declaration-site variance support. + * New: Improve handling of single expression bodies. + * New: Support file annotations. + * New: Support imports from the top-level file. + * New: Accept superclass constructor parameters. + * New: Support primary constructors using the `constructor` keyword. + * Fix: Don't emit setter parameter types. + * Fix: Support Kotlin keywords in `NameAllocator`. + * Fix: Emit the right default parameters for primary constructors. + * Fix: Format annotations properly when used as parameters. + * Fix: Recognize imports when emitting nullable types. + * Fix: Call through to the superclass constructor when superclass has a no-args constructor. + * Fix: Omit class braces if all properties are declared in primary constructor. + * Fix: Don't emit empty class bodies. + * Fix: Emit the right syntax for declaring multiple generic type constraints. + * Fix: Support properties on objects, companions and interfaces. + * Fix: Use `AnnotationSpec` for throws. + + +## Version 0.3.0 + +_2017-06-11_ + + * New: Objects and companion objects. + * New: `TypeAliasSpec` to create type aliases. + * New: `LambdaTypeName` to create lambda types. + * New: Collapse property declarations into constructor params. + * New: Extension and invoke functions for creating type names: `Runnable::class.asClassName()`. + * New: Basic support for expression bodies. + * New: Basic support for custom accessors. + * New: Remove `Filer` writing and originating elements concept. These stem from `javac` annotation + processors. + * Fix: Generate valid annotation classes. + * Fix: Use `KModifier` for varargs. + * Fix: Use `ParameterizedTypeName` for array types. + * Fix: Extract Kotlin name from `KClass` instead of Java name. + * Fix: Emit valid class literals: `Double::class` instead of `Double.class`. + * Fix: Emit modifiers in the expected order. + * Fix: Emit the correct syntax for enum classes and overridden members. + + +## Version 0.2.0 + +_2017-05-21_ + + * New: Flip API signatures to be (name, type) instead of (type, name). + * New: Support for nullable types. + * New: Support delegated properties. + * New: Extension functions. + * New: Support top-level properties. + * Fix: Inheritance should use `:` instead of `extends` and `implements`. + * Fix: Make initializerBlock emit `init {}`. + + +## Version 0.1.0 + +_2017-05-16_ + + * Initial public release. + + [kotlinpoet-metadata]: ../kotlinpoet_metadata + [kotlinpoet-metadata-specs]: ../kotlinpoet_metadata_specs + [explicit-api-mode]: https://kotlinlang.org/docs/reference/whatsnew14.html#explicit-api-mode-for-library-authors + [issue-999]: https://github.com/square/kotlinpoet/issues/999 + [ksp]: https://github.com/google/ksp + [ksp-interop-docs]: https://square.github.io/kotlinpoet/interop-ksp/ + [javapoet]: https://github.com/square/javapoet + [javapoet-interop-docs]: https://square.github.io/kotlinpoet/interop-javapoet/ + + [martinbonnin]: https://github.com/martinbonnin + [idanakav]: https://github.com/idanakav + [goooler]: https://github.com/goooler + [anandwana001]: https://github.com/anandwana001 + [evant]: https://github.com/evant + [glureau]: https://github.com/glureau + [liujingxing]: https://github.com/liujingxing + [BoD]: https://github.com/BoD + [WhosNickDoglio]: https://github.com/WhosNickDoglio + [sullis]: https://github.com/sullis + [DRSchlaubi]: https://github.com/DRSchlaubi + [seriouslyhypersonic]: https://github.com/seriouslyhypersonic + [ephemient]: https://github.com/ephemient + [dkilmer]: https://github.com/dkilmer + [aksh1618]: https://github.com/aksh1618 + [zsqw123]: https://github.com/zsqw123 + [roihershberg]: https://github.com/roihershberg diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 00000000..74108a8c --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,15 @@ +Contributing +============ + +If you would like to contribute code you can do so through GitHub by forking +the repository and sending a pull request. + +When submitting code, please make every effort to follow existing conventions +and style in order to keep the code as readable as possible. Please also make +sure your code compiles by running `./gradlew clean build`. + +Before your code can be accepted into the project you must also sign the +[Individual Contributor License Agreement (CLA)][1]. + + + [1]: https://spreadsheets.google.com/spreadsheet/viewform?formkey=dDViT2xzUHAwRkI3X3k5Z0lQM091OGc6MQ&ndplr=1 diff --git a/docs/css/app.css b/docs/css/app.css new file mode 100644 index 00000000..a982ae6f --- /dev/null +++ b/docs/css/app.css @@ -0,0 +1,30 @@ +@font-face { + font-family: cash-market; + src: url("https://cash-f.squarecdn.com/static/fonts/cash-market/v2/CashMarket-Regular.woff2") format("woff2"); + font-weight: 400; + font-style: normal +} + +@font-face { + font-family: cash-market; + src: url("https://cash-f.squarecdn.com/static/fonts/cash-market/v2/CashMarket-Medium.woff2") format("woff2"); + font-weight: 500; + font-style: normal +} + +@font-face { + font-family: cash-market; + src: url("https://cash-f.squarecdn.com/static/fonts/cash-market/v2/CashMarket-Bold.woff2") format("woff2"); + font-weight: 700; + font-style: normal +} + +body, input { + font-family: cash-market,"Helvetica Neue",helvetica,sans-serif; +} + +.md-typeset h1, .md-typeset h2, .md-typeset h3, .md-typeset h4 { + font-family: cash-market,"Helvetica Neue",helvetica,sans-serif; + line-height: normal; + font-weight: bold; +} diff --git a/docs/images/icon-square.png b/docs/images/icon-square.png Binary files differnew file mode 100644 index 00000000..bdc98d1c --- /dev/null +++ b/docs/images/icon-square.png diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 00000000..4b8ec1a1 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,1541 @@ +KotlinPoet +========== + +`KotlinPoet` is a Kotlin and Java API for generating `.kt` source files. + +Source file generation can be useful when doing things such as annotation processing or interacting +with metadata files (e.g., database schemas, protocol formats). By generating code, you eliminate +the need to write boilerplate while also keeping a single source of truth for the metadata. + +### Example + +Here's a `HelloWorld` file: + +```kotlin +class Greeter(val name: String) { + fun greet() { + println("""Hello, $name""") + } +} + +fun main(vararg args: String) { + Greeter(args[0]).greet() +} +``` + +And this is the code to generate it with KotlinPoet: + +```kotlin +val greeterClass = ClassName("", "Greeter") +val file = FileSpec.builder("", "HelloWorld") + .addType( + TypeSpec.classBuilder("Greeter") + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("name", String::class) + .build() + ) + .addProperty( + PropertySpec.builder("name", String::class) + .initializer("name") + .build() + ) + .addFunction( + FunSpec.builder("greet") + .addStatement("println(%P)", "Hello, \$name") + .build() + ) + .build() + ) + .addFunction( + FunSpec.builder("main") + .addParameter("args", String::class, VARARG) + .addStatement("%T(args[0]).greet()", greeterClass) + .build() + ) + .build() + +file.writeTo(System.out) +``` + +The [KDoc][kdoc] catalogs the complete KotlinPoet API, which is inspired by [JavaPoet][javapoet]. + +**Note:** In order to maximize portability, KotlinPoet generates code with explicit visibility +modifiers. This ensures compatibility with both standard Kotlin projects as well as projects +using [explicit API mode](https://kotlinlang.org/docs/whatsnew14.html#explicit-api-mode-for-library-authors). +Examples in this file omit those modifiers for brevity. + +### Code & Control Flow + +Most of KotlinPoet's API uses immutable Kotlin objects. There's also builders, method chaining +and varargs to make the API friendly. KotlinPoet offers models for Kotlin files (`FileSpec`), +classes, interfaces & objects (`TypeSpec`), type aliases (`TypeAliasSpec`), +properties (`PropertySpec`), functions & constructors (`FunSpec`), parameters (`ParameterSpec`) and +annotations (`AnnotationSpec`). + +But the _body_ of methods and constructors is not modeled. There's no expression class, no +statement class or syntax tree nodes. Instead, KotlinPoet uses strings for code blocks, and you can +take advantage of Kotlin's multiline strings to make this look nice: + +```kotlin +val main = FunSpec.builder("main") + .addCode(""" + |var total = 0 + |for (i in 0 until 10) { + | total += i + |} + |""".trimMargin()) + .build() +``` + +Which generates this: + +```kotlin +fun main() { + var total = 0 + for (i in 0 until 10) { + total += i + } +} +``` + +There are additional APIs to assist with newlines, braces and indentation: + +```kotlin +val main = FunSpec.builder("main") + .addStatement("var total = 0") + .beginControlFlow("for (i in 0 until 10)") + .addStatement("total += i") + .endControlFlow() + .build() +``` + +This example is lame because the generated code is constant! Suppose instead of just adding 0 to 10, +we want to make the operation and range configurable. Here's a method that generates a method: + +```kotlin +private fun computeRange(name: String, from: Int, to: Int, op: String): FunSpec { + return FunSpec.builder(name) + .returns(Int::class) + .addStatement("var result = 1") + .beginControlFlow("for (i in $from until $to)") + .addStatement("result = result $op i") + .endControlFlow() + .addStatement("return result") + .build() +} +``` + +And here's what we get when we call `computeRange("multiply10to20", 10, 20, "*")`: + +```kotlin +fun multiply10to20(): kotlin.Int { + var result = 1 + for (i in 10 until 20) { + result = result * i + } + return result +} +``` + +Methods generating methods! And since KotlinPoet generates source instead of bytecode, you can +read through it to make sure it's right. + +### %S for Strings + +When emitting code that includes string literals, we can use **`%S`** to emit a **string**, complete +with wrapping quotation marks and escaping. Here's a program that emits 3 methods, each of which +returns its own name: + +```kotlin +fun main(args: Array<String>) { + val helloWorld = TypeSpec.classBuilder("HelloWorld") + .addFunction(whatsMyNameYo("slimShady")) + .addFunction(whatsMyNameYo("eminem")) + .addFunction(whatsMyNameYo("marshallMathers")) + .build() + + val kotlinFile = FileSpec.builder("com.example.helloworld", "HelloWorld") + .addType(helloWorld) + .build() + + kotlinFile.writeTo(System.out) +} + +private fun whatsMyNameYo(name: String): FunSpec { + return FunSpec.builder(name) + .returns(String::class) + .addStatement("return %S", name) + .build() +} +``` + +In this case, using `%S` gives us quotation marks: + +```kotlin +class HelloWorld { + fun slimShady(): String = "slimShady" + + fun eminem(): String = "eminem" + + fun marshallMathers(): String = "marshallMathers" +} +``` + +### %P for String Templates + +`%S` also handles the escaping of dollar signs (`$`), to avoid inadvertent creation of string +templates, which may fail to compile in generated code: + +```kotlin +val stringWithADollar = "Your total is " + "$" + "50" +val funSpec = FunSpec.builder("printTotal") + .returns(String::class) + .addStatement("return %S", stringWithADollar) + .build() +``` + +produces: + +```kotlin +fun printTotal(): String = "Your total is ${'$'}50" +``` + +If you need to generate string templates, use `%P`, which doesn't escape dollars: + +```kotlin +val amount = 50 +val stringWithADollar = "Your total is " + "$" + "amount" +val funSpec = FunSpec.builder("printTotal") + .returns(String::class) + .addStatement("return %P", stringWithADollar) + .build() +``` + +produces: + +```kotlin +fun printTotal(): String = "Your total is $amount" +``` + +You can also use `CodeBlock`s as arguments to `%P`, which is handy when you need to reference +importable types or members inside the string template: + +```kotlin +val file = FileSpec.builder("com.example", "Digits") + .addFunction( + FunSpec.builder("print") + .addParameter("digits", IntArray::class) + .addStatement("println(%P)", buildCodeBlock { + val contentToString = MemberName("kotlin.collections", "contentToString") + add("These are the digits: \${digits.%M()}", contentToString) + }) + .build() + ) + .build() +println(file) +``` + +The snippet above will produce the following output, handling the imports properly: + +```kotlin +package com.example + +import kotlin.IntArray +import kotlin.collections.contentToString + +fun print(digits: IntArray) { + println("""These are the digits: ${digits.contentToString()}""") +} +``` + +### %T for Types + +KotlinPoet has rich built-in support for types, including automatic generation of `import` +statements. Just use **`%T`** to reference **types**: + +```kotlin +val today = FunSpec.builder("today") + .returns(Date::class) + .addStatement("return %T()", Date::class) + .build() + +val helloWorld = TypeSpec.classBuilder("HelloWorld") + .addFunction(today) + .build() + +val kotlinFile = FileSpec.builder("com.example.helloworld", "HelloWorld") + .addType(helloWorld) + .build() + +kotlinFile.writeTo(System.out) +``` + +That generates the following `.kt` file, complete with the necessary `import`: + +```kotlin +package com.example.helloworld + +import java.util.Date + +class HelloWorld { + fun today(): Date = Date() +} +``` + +We passed `Date::class` to reference a class that just-so-happens to be available when we're +generating code. This doesn't need to be the case. Here's a similar example, but this one +references a class that doesn't exist (yet): + +```kotlin +val hoverboard = ClassName("com.mattel", "Hoverboard") + +val tomorrow = FunSpec.builder("tomorrow") + .returns(hoverboard) + .addStatement("return %T()", hoverboard) + .build() +``` + +And that not-yet-existent class is imported as well: + +```kotlin +package com.example.helloworld + +import com.mattel.Hoverboard + +class HelloWorld { + fun tomorrow(): Hoverboard = Hoverboard() +} +``` + +The `ClassName` type is very important, and you'll need it frequently when you're using KotlinPoet. +It can identify any _declared_ class. Declared types are just the beginning of Kotlin's rich type +system: we also have arrays, parameterized types, wildcard types, lambda types and type variables. +KotlinPoet has classes for building each of these: + +```kotlin +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy + +val hoverboard = ClassName("com.mattel", "Hoverboard") +val list = ClassName("kotlin.collections", "List") +val arrayList = ClassName("kotlin.collections", "ArrayList") +val listOfHoverboards = list.parameterizedBy(hoverboard) +val arrayListOfHoverboards = arrayList.parameterizedBy(hoverboard) + +val thing = ClassName("com.misc", "Thing") +val array = ClassName("kotlin", "Array") +val producerArrayOfThings = array.parameterizedBy(WildcardTypeName.producerOf(thing)) + +val beyond = FunSpec.builder("beyond") + .returns(listOfHoverboards) + .addStatement("val result = %T()", arrayListOfHoverboards) + .addStatement("result += %T()", hoverboard) + .addStatement("result += %T()", hoverboard) + .addStatement("result += %T()", hoverboard) + .addStatement("return result") + .build() + +val printThings = FunSpec.builder("printThings") + .addParameter("things", producerArrayOfThings) + .addStatement("println(things)") + .build() +``` + +KotlinPoet will decompose each type and import its components where possible. + +```kotlin +package com.example.helloworld + +import com.mattel.Hoverboard +import com.misc.Thing +import kotlin.Array +import kotlin.collections.ArrayList +import kotlin.collections.List + +class HelloWorld { + fun beyond(): List<Hoverboard> { + val result = ArrayList<Hoverboard>() + result += Hoverboard() + result += Hoverboard() + result += Hoverboard() + return result + } + + fun printThings(things: Array<out Thing>) { + println(things) + } +} +``` + +#### Nullable Types + +KotlinPoet supports nullable types. To convert a `TypeName` into its nullable counterpart, use the +`copy()` method with `nullable` parameter set to `true`: + +```kotlin +val java = PropertySpec.builder("java", String::class.asTypeName().copy(nullable = true)) + .mutable() + .addModifiers(KModifier.PRIVATE) + .initializer("null") + .build() + +val helloWorld = TypeSpec.classBuilder("HelloWorld") + .addProperty(java) + .addProperty("kotlin", String::class, KModifier.PRIVATE) + .build() +``` + +generates: + +```kotlin +class HelloWorld { + private var java: String? = null + + private val kotlin: String +} +``` + +### %M for Members + +Similar to types, KotlinPoet has a special placeholder for **members** (functions and properties), +which comes handy when your code needs to access top-level members and members declared inside +objects. Use **`%M`** to reference members, pass an instance of `MemberName` as the argument for the +placeholder, and KotlinPoet will handle imports automatically: + +```kotlin +val createTaco = MemberName("com.squareup.tacos", "createTaco") +val isVegan = MemberName("com.squareup.tacos", "isVegan") +val file = FileSpec.builder("com.squareup.example", "TacoTest") + .addFunction( + FunSpec.builder("main") + .addStatement("val taco = %M()", createTaco) + .addStatement("println(taco.%M)", isVegan) + .build() + ) + .build() +println(file) +``` + +The code above generates the following file: + +```kotlin +package com.squareup.example + +import com.squareup.tacos.createTaco +import com.squareup.tacos.isVegan + +fun main() { + val taco = createTaco() + println(taco.isVegan) +} +``` + +As you can see, it's also possible to use `%M` to reference extension functions and properties. You +just need to make sure the member can be imported without simple name collisions, otherwise +importing will fail and the code generator output will not pass compilation. There's a way to work +around such cases though - use `FileSpec.addAliasedImport()` to create an alias for a clashing +`MemberName`: + +```kotlin +val createTaco = MemberName("com.squareup.tacos", "createTaco") +val createCake = MemberName("com.squareup.cakes", "createCake") +val isTacoVegan = MemberName("com.squareup.tacos", "isVegan") +val isCakeVegan = MemberName("com.squareup.cakes", "isVegan") +val file = FileSpec.builder("com.squareup.example", "Test") + .addAliasedImport(isTacoVegan, "isTacoVegan") + .addAliasedImport(isCakeVegan, "isCakeVegan") + .addFunction( + FunSpec.builder("main") + .addStatement("val taco = %M()", createTaco) + .addStatement("val cake = %M()", createCake) + .addStatement("println(taco.%M)", isTacoVegan) + .addStatement("println(cake.%M)", isCakeVegan) + .build() + ) + .build() +println(file) +``` + +KotlinPoet will produce an aliased import for `com.squareup.tacos2.isVegan`: + +```kotlin +package com.squareup.example + +import com.squareup.cakes.createCake +import com.squareup.tacos.createTaco +import com.squareup.cakes.isVegan as isCakeVegan +import com.squareup.tacos.isVegan as isTacoVegan + +fun main() { + val taco = createTaco() + val cake = createCake() + println(taco.isTacoVegan) + println(cake.isCakeVegan) +} +``` + +#### MemberName and operators + +MemberName also supports operators, you can use `MemberName(String, KOperator)` +or `MemberName(ClassName, KOperator)` to import and reference operators. + +```kotlin +val taco = ClassName("com.squareup.tacos", "Taco") +val meat = ClassName("com.squareup.tacos.ingredient", "Meat") +val iterator = MemberName("com.squareup.tacos.internal", KOperator.ITERATOR) +val minusAssign = MemberName("com.squareup.tacos.internal", KOperator.MINUS_ASSIGN) +val file = FileSpec.builder("com.example", "Test") + .addFunction( + FunSpec.builder("makeTacoHealthy") + .addParameter("taco", taco) + .beginControlFlow("for (ingredient %M taco)", iterator) + .addStatement("if (ingredient is %T) taco %M ingredient", meat, minusAssign) + .endControlFlow() + .addStatement("return taco") + .build() + ) + .build() +println(file) +``` + +KotlinPoet will import the extension operator functions and emit the operator. + +```kotlin +package com.example + +import com.squareup.tacos.Taco +import com.squareup.tacos.ingredient.Meat +import com.squareup.tacos.internal.iterator +import com.squareup.tacos.internal.minusAssign + +fun makeTacoHealthy(taco: Taco) { + for (ingredient in taco) { + if (ingredient is Meat) taco -= ingredient + } + return taco +} + +``` + +### %N for Names + +Generated code is often self-referential. Use **`%N`** to refer to another generated declaration by +its name. Here's a method that calls another: + +```kotlin +fun byteToHex(b: Int): String { + val result = CharArray(2) + result[0] = hexDigit((b ushr 4) and 0xf) + result[1] = hexDigit(b and 0xf) + return String(result) +} + +fun hexDigit(i: Int): Char { + return (if (i < 10) i + '0'.toInt() else i - 10 + 'a'.toInt()).toChar() +} +``` + +When generating the code above, we pass the `hexDigit()` method as an argument to the `byteToHex()` +method using `%N`: + +```kotlin +val hexDigit = FunSpec.builder("hexDigit") + .addParameter("i", Int::class) + .returns(Char::class) + .addStatement("return (if (i < 10) i + '0'.toInt() else i - 10 + 'a'.toInt()).toChar()") + .build() + +val byteToHex = FunSpec.builder("byteToHex") + .addParameter("b", Int::class) + .returns(String::class) + .addStatement("val result = CharArray(2)") + .addStatement("result[0] = %N((b ushr 4) and 0xf)", hexDigit) + .addStatement("result[1] = %N(b and 0xf)", hexDigit) + .addStatement("return String(result)") + .build() +``` + +Another handy feature that `%N` provides is automatically escaping names that contain illegal +identifier characters with double ticks. Suppose your code creates a `MemberName` with a Kotlin +keyword as the simple name: + +```kotlin +val taco = ClassName("com.squareup.tacos", "Taco") +val packager = ClassName("com.squareup.tacos", "TacoPackager") +val file = FileSpec.builder("com.example", "Test") + .addFunction( + FunSpec.builder("packageTacos") + .addParameter("tacos", LIST.parameterizedBy(taco)) + .addParameter("packager", packager) + .addStatement("packager.%N(tacos)", packager.member("package")) + .build() + ) + .build() +``` + +`%N` will escape the name for you, ensuring that the output will pass compilation: + +```kotlin +package com.example + +import com.squareup.tacos.Taco +import com.squareup.tacos.TacoPackager +import kotlin.collections.List + +fun packageTacos(tacos: List<Taco>, packager: TacoPackager) { + packager.`package`(tacos) +} +``` + +### %L for Literals + +Although Kotlin's string templates usually work well in cases when you want to include literals into +generated code, KotlinPoet offers additional syntax inspired-by but incompatible-with +[`String.format()`][formatter]. It accepts **`%L`** to emit a **literal** value in the output. This +works just like `Formatter`'s `%s`: + +```kotlin +private fun computeRange(name: String, from: Int, to: Int, op: String): FunSpec { + return FunSpec.builder(name) + .returns(Int::class) + .addStatement("var result = 0") + .beginControlFlow("for (i in %L until %L)", from, to) + .addStatement("result = result %L i", op) + .endControlFlow() + .addStatement("return result") + .build() +} +``` + +Literals are emitted directly to the output code with no escaping. Arguments for literals may be +strings, primitives, and a few KotlinPoet types described below. + +### Code block format strings + +Code blocks may specify the values for their placeholders in a few ways. Only one style may be used +for each operation on a code block. + +#### Relative Arguments + +Pass an argument value for each placeholder in the format string to `CodeBlock.add()`. In each +example, we generate code to say "I ate 3 tacos" + +```kotlin +CodeBlock.builder().add("I ate %L %L", 3, "tacos") +``` + +#### Positional Arguments + +Place an integer index (1-based) before the placeholder in the format string to specify which +argument to use. + +```kotlin +CodeBlock.builder().add("I ate %2L %1L", "tacos", 3) +``` + +#### Named Arguments + +Use the syntax `%argumentName:X` where `X` is the format character and call `CodeBlock.addNamed()` +with a map containing all argument keys in the format string. Argument names use characters in +`a-z`, `A-Z`, `0-9`, and `_`, and must start with a lowercase character. + +```kotlin +val map = LinkedHashMap<String, Any>() +map += "food" to "tacos" +map += "count" to 3 +CodeBlock.builder().addNamed("I ate %count:L %food:L", map) +``` + +### Functions + +All of the above functions have a code body. Use `KModifier.ABSTRACT` to get a function without any +body. This is only legal if it is enclosed by an abstract class or an interface. + +```kotlin +val flux = FunSpec.builder("flux") + .addModifiers(KModifier.ABSTRACT, KModifier.PROTECTED) + .build() + +val helloWorld = TypeSpec.classBuilder("HelloWorld") + .addModifiers(KModifier.ABSTRACT) + .addFunction(flux) + .build() +``` + +Which generates this: + +```kotlin +abstract class HelloWorld { + protected abstract fun flux() +} +``` + +The other modifiers work where permitted. + +Methods also have parameters, varargs, KDoc, annotations, type variables, return type and receiver +type for extension functions. All of these are configured with `FunSpec.Builder`. + +#### Extension functions + +Extension functions can be generated by specifying a `receiver`. + +```kotlin +val square = FunSpec.builder("square") + .receiver(Int::class) + .returns(Int::class) + .addStatement("var s = this * this") + .addStatement("return s") + .build() +``` + +Which outputs: + +```kotlin +fun Int.square(): Int { + val s = this * this + return s +} +``` + +#### Single-expression functions + +KotlinPoet can recognize single-expression functions and print them out properly. It treats +each function with a body that starts with `return` as a single-expression function: + +```kotlin +val abs = FunSpec.builder("abs") + .addParameter("x", Int::class) + .returns(Int::class) + .addStatement("return if (x < 0) -x else x") + .build() +``` + +Which outputs: + +```kotlin +fun abs(x: Int): Int = if (x < 0) -x else x +``` + +#### Default function arguments + +Consider the example below. +Function argument `b` has a default value of 0 to avoid overloading this function. + +```kotlin +fun add(a: Int, b: Int = 0) { + print("a + b = ${a + b}") +} +``` + +Use the `defaultValue()` builder function to declare default value for a function argument. + +```kotlin +FunSpec.builder("add") + .addParameter("a", Int::class) + .addParameter( + ParameterSpec.builder("b", Int::class) + .defaultValue("%L", 0) + .build() + ) + .addStatement("print(\"a + b = ${a + b}\")") + .build() +``` + +#### Spaces wrap by default! + +In order to provide meaningful formatting, KotlinPoet would replace spaces, found in blocks of code, +with new line symbols, in cases when the line of code exceeds the length limit. Let's take this +function for example: + +```kotlin +val funSpec = FunSpec.builder("foo") + .addStatement("return (100..10000).map { number -> number * number }.map { number -> number.toString() }.also { string -> println(string) }") + .build() +``` + +Depending on where it's found in the file, it may end up being printed out like this: + +```kotlin +fun foo() = (100..10000).map { number -> number * number }.map { number -> number.toString() }.also +{ string -> println(string) } +``` + +Unfortunately this code is broken: the compiler expects `also` and `{` to be on the same line. +KotlinPoet is unable to understand the context of the expression and fix the formatting for you, but +there's a trick you can use to declare a non-breaking space - use the `·` symbol where you would +otherwise use a space. Let's apply this to our example: + +```kotlin +val funSpec = FunSpec.builder("foo") + .addStatement("return (100..10000).map·{ number -> number * number }.map·{ number -> number.toString() }.also·{ string -> println(string) }") + .build() +``` + +This will now produce the following result: + +```kotlin +fun foo() = (100..10000).map { number -> number * number }.map { number -> + number.toString() +}.also { string -> println(string) } +``` + +The code is now correct and will compile properly. It still doesn't look perfect - you can play with +replacing other spaces in the code block with `·` symbols to achieve better formatting. + +### Constructors + +`FunSpec` is a slight misnomer; it can also be used for constructors: + +```kotlin +val flux = FunSpec.constructorBuilder() + .addParameter("greeting", String::class) + .addStatement("this.%N = %N", "greeting", "greeting") + .build() + +val helloWorld = TypeSpec.classBuilder("HelloWorld") + .addProperty("greeting", String::class, KModifier.PRIVATE) + .addFunction(flux) + .build() +``` + +Which generates this: + +```kotlin +class HelloWorld { + private val greeting: String + + constructor(greeting: String) { + this.greeting = greeting + } +} +``` + +For the most part, constructors work just like methods. When emitting code, KotlinPoet will place +constructors before methods in the output file. + +Often times you'll need to generate the primary constructor for a class: + +```kotlin +val helloWorld = TypeSpec.classBuilder("HelloWorld") + .primaryConstructor(flux) + .addProperty("greeting", String::class, KModifier.PRIVATE) + .build() +``` + +This code, however, generates the following: + +```kotlin +class HelloWorld(greeting: String) { + private val greeting: String + + init { + this.greeting = greeting + } +} +``` + +By default, KotlinPoet won't merge primary constructor parameters and properties, even if they share +the same name. To achieve the effect, you have to tell KotlinPoet that the property is initialized +via the constructor parameter: + +```kotlin +val flux = FunSpec.constructorBuilder() + .addParameter("greeting", String::class) + .build() + +val helloWorld = TypeSpec.classBuilder("HelloWorld") + .primaryConstructor(flux) + .addProperty( + PropertySpec.builder("greeting", String::class) + .initializer("greeting") + .addModifiers(KModifier.PRIVATE) + .build() + ) + .build() +``` + +Now we're getting the following output: + +```kotlin +class HelloWorld(private val greeting: String) +``` + +Notice that KotlinPoet omits `{}` for classes with empty bodies. + +### Parameters + +Declare parameters on methods and constructors with either `ParameterSpec.builder()` or +`FunSpec`'s convenient `addParameter()` API: + +```kotlin +val android = ParameterSpec.builder("android", String::class) + .defaultValue("\"pie\"") + .build() + +val welcomeOverlords = FunSpec.builder("welcomeOverlords") + .addParameter(android) + .addParameter("robot", String::class) + .build() +``` + +The code above generates: + +```kotlin +fun welcomeOverlords(android: String = "pie", robot: String) { +} +``` + +The extended `Builder` form is necessary when the parameter has annotations (such as `@Inject`). + +### Properties + +Like parameters, properties can be created either with builders or by using convenient helper +methods: + +```kotlin +val android = PropertySpec.builder("android", String::class) + .addModifiers(KModifier.PRIVATE) + .build() + +val helloWorld = TypeSpec.classBuilder("HelloWorld") + .addProperty(android) + .addProperty("robot", String::class, KModifier.PRIVATE) + .build() +``` + +Which generates: + +```kotlin +class HelloWorld { + private val android: String + + private val robot: String +} +``` + +The extended `Builder` form is necessary when a field has KDoc, annotations, or a field +initializer. Field initializers use the same [`String.format()`][formatter]-like syntax as the code +blocks above: + +```kotlin +val android = PropertySpec.builder("android", String::class) + .addModifiers(KModifier.PRIVATE) + .initializer("%S + %L", "Oreo v.", 8.1) + .build() +``` + +Which generates: + +```kotlin +private val android: String = "Oreo v." + 8.1 +``` + +By default `PropertySpec.Builder` produces `val` properties. Use `mutable()` if you need a +`var`: + +```kotlin +val android = PropertySpec.builder("android", String::class) + .mutable() + .addModifiers(KModifier.PRIVATE) + .initializer("%S + %L", "Oreo v.", 8.1) + .build() +``` + +#### Inline properties + +The way KotlinPoet models inline properties deserves special mention. The following snippet of code: + +```kotlin +val android = PropertySpec.builder("android", String::class) + .mutable() + .addModifiers(KModifier.INLINE) + .build() +``` + +will produce an error: + +``` +java.lang.IllegalArgumentException: KotlinPoet doesn't allow setting the inline modifier on +properties. You should mark either the getter, the setter, or both inline. +``` + +Indeed, a property marked with `inline` should have at least one accessor which will be inlined by +the compiler. Let's add a getter to this property: + +```kotlin +val android = PropertySpec.builder("android", String::class) + .mutable() + .getter( + FunSpec.getterBuilder() + .addModifiers(KModifier.INLINE) + .addStatement("return %S", "foo") + .build() + ) + .build() +``` + +The result is the following: + +```kotlin +var android: kotlin.String + inline get() = "foo" +``` + +Now, what if we wanted to add a non-inline setter to the property above? We can do so without +modifying any of the code we wrote previously: + +```kotlin +val android = PropertySpec.builder("android", String::class) + .mutable() + .getter( + FunSpec.getterBuilder() + .addModifiers(KModifier.INLINE) + .addStatement("return %S", "foo") + .build() + ) + .setter( + FunSpec.setterBuilder() + .addParameter("value", String::class) + .build() + ) + .build() +``` + +We get the expected result: + +```kotlin +var android: kotlin.String + inline get() = "foo" + set(`value`) { + } +``` + +Finally, if we go back and add `KModifier.INLINE` to the setter, KotlinPoet can wrap it nicely and +produce the following result: + +```kotlin +inline var android: kotlin.String + get() = "foo" + set(`value`) { + } +``` + +Removing the modifier from either the getter or the setter will unwrap the expression back. + +If, on the other hand, KotlinPoet had allowed marking a property `inline` directly, the programmer +would have had to manually add/remove the modifier whenever the state of the accessors changes in +order to get correct and compilable output. We're solving this problem by making accessors the +source of truth for the `inline` modifier. + +### Interfaces + +KotlinPoet has no trouble with interfaces. Note that interface methods must always be `ABSTRACT`. +The modifier is necessary when defining the interface: + +```kotlin +val helloWorld = TypeSpec.interfaceBuilder("HelloWorld") + .addProperty("buzz", String::class) + .addFunction( + FunSpec.builder("beep") + .addModifiers(KModifier.ABSTRACT) + .build() + ) + .build() +``` + +But these modifiers are omitted when the code is generated. These are the default so we don't need +to include them for `kotlinc`'s benefit! + +```kotlin +interface HelloWorld { + val buzz: String + + fun beep() +} +``` + +Kotlin 1.4 adds support for functional interfaces via `fun interface` syntax. To create this in +KotlinPoet, use `TypeSpec.funInterfaceBuilder()`. + +```kotlin +val helloWorld = TypeSpec.funInterfaceBuilder("HelloWorld") + .addFunction( + FunSpec.builder("beep") + .addModifiers(KModifier.ABSTRACT) + .build() + ) + .build() + +// Generates... +fun interface HelloWorld { + fun beep() +} +``` + +### Objects + +KotlinPoet supports objects: + +```kotlin +val helloWorld = TypeSpec.objectBuilder("HelloWorld") + .addProperty( + PropertySpec.builder("buzz", String::class) + .initializer("%S", "buzz") + .build() + ) + .addFunction( + FunSpec.builder("beep") + .addStatement("println(%S)", "Beep!") + .build() + ) + .build() +``` + +Similarly, you can create companion objects and add them to classes using `addType()`: + +```kotlin +val companion = TypeSpec.companionObjectBuilder() + .addProperty( + PropertySpec.builder("buzz", String::class) + .initializer("%S", "buzz") + .build() + ) + .addFunction( + FunSpec.builder("beep") + .addStatement("println(%S)", "Beep!") + .build() + ) + .build() + +val helloWorld = TypeSpec.classBuilder("HelloWorld") + .addType(companion) + .build() +``` + +You can provide an optional name for a companion object. + +### Enums + +Use `enumBuilder` to create the enum type, and `addEnumConstant()` for each value: + +```kotlin +val helloWorld = TypeSpec.enumBuilder("Roshambo") + .addEnumConstant("ROCK") + .addEnumConstant("SCISSORS") + .addEnumConstant("PAPER") + .build() +``` + +To generate this: + +```kotlin +enum class Roshambo { + ROCK, + + SCISSORS, + + PAPER +} +``` + +Fancy enums are supported, where the enum values override methods or call a superclass constructor. +Here's a comprehensive example: + +```kotlin +val helloWorld = TypeSpec.enumBuilder("Roshambo") + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("handsign", String::class) + .build() + ) + .addEnumConstant( + "ROCK", TypeSpec.anonymousClassBuilder() + .addSuperclassConstructorParameter("%S", "fist") + .addFunction( + FunSpec.builder("toString") + .addModifiers(KModifier.OVERRIDE) + .addStatement("return %S", "avalanche!") + .returns(String::class) + .build() + ) + .build() + ) + .addEnumConstant( + "SCISSORS", TypeSpec.anonymousClassBuilder() + .addSuperclassConstructorParameter("%S", "peace") + .build() + ) + .addEnumConstant( + "PAPER", TypeSpec.anonymousClassBuilder() + .addSuperclassConstructorParameter("%S", "flat") + .build() + ) + .addProperty( + PropertySpec.builder("handsign", String::class, KModifier.PRIVATE) + .initializer("handsign") + .build() + ) + .build() +``` + +Which generates this: + +```kotlin +enum class Roshambo(private val handsign: String) { + ROCK("fist") { + override fun toString(): String = "avalanche!" + }, + + SCISSORS("peace"), + + PAPER("flat"); +} +``` + +### Anonymous Inner Classes + +In the enum code, we used `TypeSpec.anonymousClassBuilder()`. Anonymous inner classes can also be +used in code blocks. They are values that can be referenced with `%L`: + +```kotlin +val comparator = TypeSpec.anonymousClassBuilder() + .addSuperinterface(Comparator::class.parameterizedBy(String::class)) + .addFunction( + FunSpec.builder("compare") + .addModifiers(KModifier.OVERRIDE) + .addParameter("a", String::class) + .addParameter("b", String::class) + .returns(Int::class) + .addStatement("return %N.length - %N.length", "a", "b") + .build() + ) + .build() + +val helloWorld = TypeSpec.classBuilder("HelloWorld") + .addFunction( + FunSpec.builder("sortByLength") + .addParameter("strings", List::class.parameterizedBy(String::class)) + .addStatement("%N.sortedWith(%L)", "strings", comparator) + .build() + ) + .build() +``` + +This generates a method that contains a class that contains a method: + +```kotlin +class HelloWorld { + fun sortByLength(strings: List<String>) { + strings.sortedWith(object : Comparator<String> { + override fun compare(a: String, b: String): Int = a.length - b.length + }) + } +} +``` + +One particularly tricky part of defining anonymous inner classes is the arguments to the superclass +constructor. To pass them use `TypeSpec.Builder`'s `addSuperclassConstructorParameter()` method. + +### Annotations + +Simple annotations are easy: + +```kotlin +val test = FunSpec.builder("test string equality") + .addAnnotation(Test::class) + .addStatement("assertThat(%1S).isEqualTo(%1S)", "foo") + .build() +``` + +Which generates this function with an `@Test` annotation: + +```kotlin +@Test +fun `test string equality`() { + assertThat("foo").isEqualTo("foo") +} +``` + +Use `AnnotationSpec.builder()` to set properties on annotations: + +```kotlin +val logRecord = FunSpec.builder("recordEvent") + .addModifiers(KModifier.ABSTRACT) + .addAnnotation( + AnnotationSpec.builder(Headers::class) + .addMember("accept = %S", "application/json; charset=utf-8") + .addMember("userAgent = %S", "Square Cash") + .build() + ) + .addParameter("logRecord", LogRecord::class) + .returns(LogReceipt::class) + .build() +``` + +Which generates this annotation with `accept` and `userAgent` properties: + +```kotlin +@Headers( + accept = "application/json; charset=utf-8", + userAgent = "Square Cash" +) +abstract fun recordEvent(logRecord: LogRecord): LogReceipt +``` + +When you get fancy, annotation values can be annotations themselves. Use `%L` for embedded +annotations: + +```kotlin +val headerList = ClassName("", "HeaderList") +val header = ClassName("", "Header") +val logRecord = FunSpec.builder("recordEvent") + .addModifiers(KModifier.ABSTRACT) + .addAnnotation( + AnnotationSpec.builder(headerList) + .addMember( + "[\n⇥%L,\n%L⇤\n]", + AnnotationSpec.builder(header) + .addMember("name = %S", "Accept") + .addMember("value = %S", "application/json; charset=utf-8") + .build(), + AnnotationSpec.builder(header) + .addMember("name = %S", "User-Agent") + .addMember("value = %S", "Square Cash") + .build() + ) + .build() + ) + .addParameter("logRecord", logRecordName) + .returns(logReceipt) + .build() +``` + +Which generates this: + +```kotlin +@HeaderList( + [ + Header(name = "Accept", value = "application/json; charset=utf-8"), + Header(name = "User-Agent", value = "Square Cash") + ] +) +abstract fun recordEvent(logRecord: LogRecord): LogReceipt +``` + +KotlinPoet supports use-site targets for annotations: + +```kotlin +val utils = FileSpec.builder("com.example", "Utils") + .addAnnotation( + AnnotationSpec.builder(JvmName::class) + .useSiteTarget(UseSiteTarget.FILE) + .build() + ) + .addFunction( + FunSpec.builder("abs") + .receiver(Int::class) + .returns(Int::class) + .addStatement("return if (this < 0) -this else this") + .build() + ) + .build() +``` + +Will output this: + +```kotlin +@file:JvmName + +package com.example + +import kotlin.Int +import kotlin.jvm.JvmName + +fun Int.abs(): Int = if (this < 0) -this else this +``` + +### Type Aliases + +KotlinPoet provides API for creating Type Aliases, which supports simple class names, parameterized +types and lambdas: + +```kotlin +val k = TypeVariableName("K") +val t = TypeVariableName("T") + +val fileTable = Map::class.asClassName() + .parameterizedBy(k, Set::class.parameterizedBy(File::class)) + +val predicate = LambdaTypeName.get( + parameters = arrayOf(t), + returnType = Boolean::class.asClassName() +) +val helloWorld = FileSpec.builder("com.example", "HelloWorld") + .addTypeAlias(TypeAliasSpec.builder("Word", String::class).build()) + .addTypeAlias( + TypeAliasSpec.builder("FileTable", fileTable) + .addTypeVariable(k) + .build() + ) + .addTypeAlias( + TypeAliasSpec.builder("Predicate", predicate) + .addTypeVariable(t) + .build() + ) + .build() +``` + +Which generates the following: + +```kotlin +package com.example + +import java.io.File +import kotlin.Boolean +import kotlin.String +import kotlin.collections.Map +import kotlin.collections.Set + +typealias Word = String + +typealias FileTable<K> = Map<K, Set<File>> + +typealias Predicate<T> = (T) -> Boolean +``` + +### Callable References + +[Callable references](https://kotlinlang.org/docs/reference/reflection.html#callable-references) to +constructors, functions, and properties may be emitted via: + +- `ClassName.constructorReference()` for constructors +- `MemberName.reference()` for functions and properties + +For example, + +```kotlin +val helloClass = ClassName("com.example.hello", "Hello") +val worldFunction: MemberName = helloClass.member("world") +val byeProperty: MemberName = helloClass.nestedClass("World").member("bye") + +val factoriesFun = FunSpec.builder("factories") + .addStatement("val hello = %L", helloClass.constructorReference()) + .addStatement("val world = %L", worldFunction.reference()) + .addStatement("val bye = %L", byeProperty.reference()) + .build() + +FileSpec.builder("com.example", "HelloWorld") + .addFunction(factoriesFun) + .build() +``` + +would generate: + +```kotlin +package com.example + +import com.example.hello.Hello + +fun factories() { + val hello = ::Hello + val world = Hello::world + val bye = Hello.World::bye +} +``` + +Top-level classes and members with conflicting names may require aliased imports, as with +[member names](#m-for-members). + +kotlin-reflect +-------- + +To generate source code from +any [`KType`](https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.reflect/-k-type/), including +information that's not accessible to the builtin reflection APIs, KotlinPoet depends +on [kotlin-reflect](https://kotlinlang.org/docs/reflection.html#jvm-dependency). `kotlin-reflect` +can read the metadata of your classes and access this extra information. KotlinPoet can for an +example, read the type parameters and +their [variance](https://kotlinlang.org/docs/generics.html#variance) from a generic `KType` and +generate appropriate source code. + +`kotlin-reflect` is a relatively big dependency though and in some cases it is desirable to remove +it from the final executable to save some space and/or simplify the proguard/R8 setup (for example +for a Gradle plugin that generates Kotlin code). It is possible to do so and still use most of the +KotlinPoet APIs: + +```kotlin +dependencies { + implementation("com.squareup:kotlinpoet:<version>") { + exclude(module = "kotlin-reflect") + } +} +``` + +The main APIs that require `kotlin-reflect` +are [`KType.asTypeName()`](https://square.github.io/kotlinpoet/1.x/kotlinpoet/kotlinpoet/com.squareup.kotlinpoet/as-type-name.html) +and [`typeNameOf<T>()`](https://square.github.io/kotlinpoet/1.x/kotlinpoet/kotlinpoet/com.squareup.kotlinpoet/type-name-of.html). +If you're calling one of these without `kotlin-reflect` in the classpath and the type is generic +or has annotations you will get a crash. + +You can replace it with code that passes type parameters or annotations explicitly and doesn't +need `kotlin-reflect`. For example: + +```kotlin +// Replace +// kotlin-reflect needed +val typeName = typeNameOf<List<Int?>>() + +// With +// kotlin-reflect not needed +val typeName = + List::class.asClassName().parameterizedBy(Int::class.asClassName().copy(nullable = true)) +``` + +Download +-------- + +![Maven Central][version-shield] + +Download [the latest .jar][dl] or depend via Maven: + +```xml +<dependency> + <groupId>com.squareup</groupId> + <artifactId>kotlinpoet</artifactId> + <version>[version]</version> +</dependency> +``` + +or Gradle: + +```groovy +implementation("com.squareup:kotlinpoet:[version]") +``` + +Snapshots of the development version are available in [Sonatype's `snapshots` repository][snap]. + + +License +------- + + Copyright 2017 Square, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + + + [dl]: https://search.maven.org/remote_content?g=com.squareup&a=kotlinpoet&v=LATEST + [version-shield]: https://img.shields.io/maven-central/v/com.squareup/kotlinpoet + [snap]: https://s01.oss.sonatype.org/content/repositories/snapshots/com/squareup/kotlinpoet/ + [kdoc]: https://square.github.io/kotlinpoet/1.x/kotlinpoet/kotlinpoet/com.squareup.kotlinpoet/ + [javapoet]: https://github.com/square/javapoet/ + [formatter]: https://developer.android.com/reference/java/util/Formatter.html diff --git a/docs/interop-javapoet.md b/docs/interop-javapoet.md new file mode 100644 index 00000000..6b41f0d5 --- /dev/null +++ b/docs/interop-javapoet.md @@ -0,0 +1,54 @@ +JavaPoet Extensions for KotlinPoet +================================== + +`interop:javapoet` is an interop API for converting [JavaPoet](https://github.com/squareup/javapoet) +types to KotlinPoet types. This is particularly useful for projects that support code gen in +multiple languages and want to easily be able to jump between. + +Note that this API is currently in preview and subject to API changes. Usage of them requires opting +in to the `@KotlinPoetJavaPoetPreview` annotation. + +### Examples + +**Typealiases for common conflicting type names** + +```kotlin +// Points to com.squareup.kotlinpoet.TypeName +KTypeName +// Points to com.squareup.javapoet.TypeName +JTypeName +``` + +**Convert between a `JTypeName` and `KTypeName`** + +Most usages of these can run through the `toKTypeName()` and `toJTypeName()` extensions. + +```kotlin +val jType = JTypeName.get("com.example", "Taco") + +// Returns a KotlinPoet `ClassName` of value `com.example.Taco` +val kType = jType.toKTypeName() + +// Returns a JavaPoet `ClassName` of value `com.example.Taco` +val jType2 = kType.toJTypeName() +``` + +### Intrinsics + +Kotlin supports a number of intrinsic types that live in the `kotlin` package, such as primitives, +`List`, `String`, `IntArray`, etc. Where possible, interop will best-effort attempt to convert to +the idiomatic Kotlin type when converting from the Java type. + +### Lossy Conversions + +Kotlin has more expressive types in some regards. These cannot be simply expressed in JavaPoet and +are subject to lossy conversions. + +Examples include: + +- Nullability + - Nullable types in Kotlin will appear as normal types in JavaPoet. +- Collection mutability + - Immutable Kotlin collections will convert to their standard (mutable) Java analogs. + - Java collections will convert to _immutable_ Kotlin analogs, erring on the side of safety in generated public APIs +- Unsigned types diff --git a/docs/interop-kotlinx-metadata.md b/docs/interop-kotlinx-metadata.md new file mode 100644 index 00000000..58a470a6 --- /dev/null +++ b/docs/interop-kotlinx-metadata.md @@ -0,0 +1,84 @@ +KotlinPoet-metadata +=================== + +`interop:kotlinx-metadata` is an API for working with Kotlin `@Metadata` annotations. Its API +sits atop [kotlinx-metadata](https://github.com/JetBrains/kotlin/tree/master/libraries/kotlinx-metadata/jvm), +offering extensions for its types + JVM metadata information. This can be used to read +Kotlin language semantics off of `Class` or `TypeElement` `@Metadata` annotations. + +### Example + +```kotlin +data class Taco(val seasoning: String, val soft: Boolean) { + fun prepare() { + + } +} + +val kmClass = Taco::class.toKmClass() + +// Now you can access misc information about Taco from a Kotlin lens +println(kmClass.name) +kmClass.properties.forEach { println(it.name) } +kmClass.functions.forEach { println(it.name) } +``` + +### Flags + +There are a number of boolean flags available to types as well under `Flags.kt`. These read the +underlying kotlinx-metadata `Flags` property. + +Using the Taco example above, we can glean certain information: + +```kotlin +println("Is class? ${kmClass.isClass}") +println("Is data class? ${kmClass.isData}") +``` + +### Interop with KotlinPoet + +`interop:kotlinx-metadata` offers an API for converting core kotlinx-metadata `Km` types to +KotlinPoet source representations of their APIs. This includes full type resolution, signatures, +enclosed elements, and general stub source representations of the underlying API. + +### Example + +```kotlin +data class Taco(val seasoning: String, val soft: Boolean) { + fun prepare() { + } +} + +val typeSpec = Taco::class.toTypeSpec() + +// Or FileSpec +val fileSpec = Taco::class.toFileSpec() +``` + +### Source representation + +The generated representations are a _best effort_ representation of the underlying source code. +This means that synthetic elements will be excluded from generation. Kotlin-specific language +features like lambdas or delegation will be coerced to their idiomatic source form. + +To aid with this, `toTypeSpec()` and `toFileSpec()` accept optional `ClassInspector` instances +to assist in parsing/understanding the underlying JVM code. This is important for things like +annotations, companion objects, certain JVM modifiers, overrides, and more. While it is optional, +represented sources can be incomplete without this information available. Reflective and javax +`Elements` implementations are available under the +`com.squareup.kotlinpoet.metadata.classinspectors` package. + +Generated sources are solely _stub_ implementations, meaning implementation details of elements +like functions, property getters, and delegated properties are simply stubbed with `TODO()` +placeholders. + +### Known limitations + +- Only `KotlinClassMetadata.Class` and `KotlinClassMetadata.FileFacade` are supported for now. No support for `SyntheticClass`, `MultiFileClassFacade`, or `MultiFileClassPart` +- `@JvmOverloads` annotations are only supported with `ElementsClassInspector` and not reflection. +- Non-const literal values are only supported with `ElementsClassInspector` and not reflection. +- ClassInspector data sourced from `synthetic` constructs are only supported with + `ReflectiveClassInspector` and not elements. This is because the javax Elements API does not model + synthetic constructs. This can yield some missing information, like static companion object properties + or `property:` site target annotations. +- Annotations annotated with `AnnotationRetention.SOURCE` are not parsable in reflection nor javax elements. diff --git a/docs/interop-ksp.md b/docs/interop-ksp.md new file mode 100644 index 00000000..78c752f8 --- /dev/null +++ b/docs/interop-ksp.md @@ -0,0 +1,136 @@ +KSP Extensions for KotlinPoet +============== + +`interop:ksp` is an interop API for converting +[Kotlin Symbol Processing][ksp] (KSP) types to KotlinPoet types and +writing to KSP `CodeGenerator`. + +```kotlin +dependencies { + implementation("com.squareup:kotlinpoet-ksp:<version>") +} +``` + +### Examples + +Examples are based on reading the following property as a `KSProperty`: + +```kotlin +class Taco { + internal inline val seasoning: String get() = "spicy" +} +``` + +**Convert a `KSType` to a `TypeName`** + +```kotlin +// returns a `ClassName` of value `kotlin.String` +seasoningKsProperty.type.toTypeName() +``` + +**Convert a `Modifier` to a `KModifier`** + +```kotlin +// returns `[KModifier.INLINE]` +seasoningKsProperty.modifiers.mapNotNull { it.toKModifier() } +``` + +**Convert a `Visibility` to a `KModifier`** + +```kotlin +// returns `KModifier.INTERNAL` +seasoningKsProperty.getVisibility().toKModifier() +``` + +**Write to `CodeGenerator`** + +To write a `FileSpec` to a KSP `CodeGenerator`, simply call the `FileSpec.writeTo(CodeGenerator, ...)` +extension function. + +```kotlin +fileSpec.writeTo(codeGenerator) +``` + +### Type Parameters + +Type parameters can be declared on classes, functions, and typealiases. These parameters are then +available to all of its enclosed elements. In order for these elements to resolve these in KSP, you +must be able to reference these type parameters by their _index_. + +In `kotlinpoet-ksp` this is orchestrated by the `TypeParameterResolver` API, which can be passed +into most `toTypeName()` (or similar) functions to give them access to enclosing type parameters +that they may reference. + +The canonical way to create an instance of this is to call `toTypeParameterResolver()` on a +`List<KSTypeParameter>`. + +Consider the following class and function + +```kotlin +abstract class Taco<T> { + abstract val seasoning: T +} +``` + +To properly resolve the type of `seasoning`, we need to pass the class `TypeParameterResolver` to +`toTypeName()` so that it can properly resolve it. + +```kotlin +val classTypeParams = ksClassDeclaration.typeParameters.toTypeParameterResolver() +// returns `T` +val seasoningType = seasoningKsProperty.type.toTypeName(classTypeParams) +``` + +`TypeParameterResolver` is also composable to allow for multi-level nesting. `toTypeParameterResolver()` +has an optional `parent` parameter to provide a parent instance. + +Consider our previous example again, but this time with a function that defines its own type parameters. + +```kotlin +class Taco<T> { + fun <E> getShellOfType(param1: E, param2: T) { + + } +} +``` + +To resolve its parameters, we need to create a `TypeParameterResolver` from the function's +`typeParameters` and _compose_ it with the enclosing class's type parameters as a `parent`. + +```kotlin +val classTypeParams = ksClassDeclaration.typeParameters.toTypeParameterResolver() +val functionTypeParams = ksFunction.typeParameters.toTypeParameterResolver(parent = classTypeParams) +// returns `[E, T]` +val seasoningType = ksFunction.parameterTypes.map { it.toTypeName(functionTypeParams) } +``` + +### Incremental Processing + +KSP supports [incremental processing][incremental] as +long as symbol processors properly indicate originating files in generated new files and whether or +not they are `aggregating`. `kotlinpoet-ksp` supports this via `OriginatingKSFiles`, which is a simple +API that sits atop KotlinPoet's `Taggable` API. To use this, simply add relevant originating files to +any `TypeSpec`, `TypeAliasSpec`, `PropertySpec`, or `FunSpec` builders. + +```kotlin +val functionBuilder = FunSpec.builder("sayHello") + .addOriginatingKSFile(sourceKsFile) + .build() +``` + +Like KotlinPoet's _originating elements_ support for javac annotation processors, calling the +`FileSpec.writeTo(CodeGenerator, ...)` function will automatically collect and de-dupe these originating +`KSFile` references and automatically assemble them in the underlying `Dependencies` for KSP's reference. + +Optionally you can define your own collection of files and pass them to the `writeTo` function, but usually +you don't need to do this manually. + +Lastly - `FileSpec.writeTo(CodeGenerator, ...)` also requires you to specify if your processor is +_aggregating_ or not via required parameter by the same name. + +### TypeAlias Handling + +For `typealias` types, KSP interop will store a `TypeAliasTag` in the `TypeName`'s tags with a reference to the abbreviated type. This can be useful for APIs that want to resolve all un-aliased types. + + [ksp]: https://github.com/google/ksp + [incremental]: https://github.com/google/ksp/blob/main/docs/incremental.md diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 00000000..f0543f01 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,19 @@ +org.gradle.jvmargs='-Dfile.encoding=UTF-8' + +GROUP=com.squareup +VERSION_NAME=1.13.0-SNAPSHOT + +POM_URL=https://github.com/square/kotlinpoet +POM_SCM_URL=https://github.com/square/kotlinpoet +POM_SCM_CONNECTION=scm:git:https://github.com/square/kotlinpoet.git +POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/square/kotlinpoet.git + +POM_LICENCE_NAME=The Apache Software License, Version 2.0 +POM_LICENCE_URL=https://www.apache.org/licenses/LICENSE-2.0.txt +POM_LICENCE_DIST=repo + +POM_DEVELOPER_ID=square +POM_DEVELOPER_NAME=Square, Inc. + +SONATYPE_HOST=S01 +RELEASE_SIGNING_ENABLED=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 00000000..523b9bc0 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,51 @@ +# Copyright (C) 2021 Square, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +[versions] +kotlin = "1.7.22" +kct = "1.4.9" +ksp = "1.7.22-1.0.8" +ktlint = "0.48.2" + +[plugins] +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +dokka = { id = "org.jetbrains.dokka", version = "1.7.20" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } +spotless = { id = "com.diffplug.spotless", version = "6.14.0" } +mavenPublish = { id = "com.vanniktech.maven.publish", version = "0.24.0" } +kotlinBinaryCompatibilityValidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version = "0.12.1" } + +[libraries] +autoCommon = { module = "com.google.auto:auto-common", version = "1.2.1" } +guava = { module = "com.google.guava:guava", version = "31.1-jre" } +javapoet = "com.squareup:javapoet:1.13.0" + +autoService = "com.google.auto.service:auto-service-annotations:1.0.1" +autoService-ksp = "dev.zacsweers.autoservice:auto-service-ksp:1.0.0" + +kotlin-compilerEmbeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" } +kotlin-annotationProcessingEmbeddable = { module = "org.jetbrains.kotlin:kotlin-annotation-processing-embeddable", version.ref = "kotlin" } +kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } +kotlin-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } +kotlin-metadata = { module = "org.jetbrains.kotlinx:kotlinx-metadata-jvm", version = "0.5.0" } + +ksp = { module = "com.google.devtools.ksp:symbol-processing", version.ref = "ksp" } +ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } + +truth = { module = "com.google.truth:truth", version = "1.1.3" } +compileTesting = { module = "com.google.testing.compile:compile-testing", version = "0.21.0" } +jimfs = { module = "com.google.jimfs:jimfs", version = "1.2" } +ecj = { module = "org.eclipse.jdt.core.compiler:ecj", version = "4.6.1" } +kotlinCompileTesting = { module = "com.github.tschuchortdev:kotlin-compile-testing", version.ref = "kct" } +kotlinCompileTesting-ksp = { module = "com.github.tschuchortdev:kotlin-compile-testing-ksp", version.ref = "kct" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar Binary files differnew file mode 100644 index 00000000..249e5832 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.jar diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..8fad3f5a --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-all.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 00000000..a69d9cb6 --- /dev/null +++ b/gradlew @@ -0,0 +1,240 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..f127cfd4 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/interop/javapoet/api/javapoet.api b/interop/javapoet/api/javapoet.api new file mode 100644 index 00000000..80ac2d96 --- /dev/null +++ b/interop/javapoet/api/javapoet.api @@ -0,0 +1,22 @@ +public final class com/squareup/kotlinpoet/javapoet/J2kInteropKt { + public static final fun toKClassName (Lcom/squareup/javapoet/ClassName;)Lcom/squareup/kotlinpoet/ClassName; + public static final fun toKParameterizedTypeName (Lcom/squareup/javapoet/ParameterizedTypeName;)Lcom/squareup/kotlinpoet/ParameterizedTypeName; + public static final fun toKTypeName (Lcom/squareup/javapoet/TypeName;)Lcom/squareup/kotlinpoet/TypeName; + public static final fun toKTypeVariableName (Lcom/squareup/javapoet/TypeVariableName;)Lcom/squareup/kotlinpoet/TypeVariableName; + public static final fun toKWildcardTypeName (Lcom/squareup/javapoet/WildcardTypeName;)Lcom/squareup/kotlinpoet/WildcardTypeName; +} + +public final class com/squareup/kotlinpoet/javapoet/K2jInteropKt { + public static final fun toJClassName (Lcom/squareup/kotlinpoet/ClassName;Z)Lcom/squareup/javapoet/TypeName; + public static synthetic fun toJClassName$default (Lcom/squareup/kotlinpoet/ClassName;ZILjava/lang/Object;)Lcom/squareup/javapoet/TypeName; + public static final fun toJParameterizedOrArrayTypeName (Lcom/squareup/kotlinpoet/ParameterizedTypeName;)Lcom/squareup/javapoet/TypeName; + public static final fun toJParameterizedTypeName (Lcom/squareup/kotlinpoet/ParameterizedTypeName;)Lcom/squareup/javapoet/ParameterizedTypeName; + public static final fun toJTypeName (Lcom/squareup/kotlinpoet/TypeName;Z)Lcom/squareup/javapoet/TypeName; + public static synthetic fun toJTypeName$default (Lcom/squareup/kotlinpoet/TypeName;ZILjava/lang/Object;)Lcom/squareup/javapoet/TypeName; + public static final fun toJTypeVariableName (Lcom/squareup/kotlinpoet/TypeVariableName;)Lcom/squareup/javapoet/TypeVariableName; + public static final fun toJWildcardTypeName (Lcom/squareup/kotlinpoet/WildcardTypeName;)Lcom/squareup/javapoet/WildcardTypeName; +} + +public abstract interface annotation class com/squareup/kotlinpoet/javapoet/KotlinPoetJavaPoetPreview : java/lang/annotation/Annotation { +} + diff --git a/interop/javapoet/build.gradle.kts b/interop/javapoet/build.gradle.kts new file mode 100644 index 00000000..9899d9cc --- /dev/null +++ b/interop/javapoet/build.gradle.kts @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +tasks.jar { + manifest { + attributes("Automatic-Module-Name" to "com.squareup.kotlinpoet.javapoet") + } +} + +dependencies { + api(project(":kotlinpoet")) + api(libs.javapoet) + testImplementation(libs.kotlin.junit) + testImplementation(libs.truth) +} diff --git a/interop/javapoet/gradle.properties b/interop/javapoet/gradle.properties new file mode 100644 index 00000000..9584afad --- /dev/null +++ b/interop/javapoet/gradle.properties @@ -0,0 +1,4 @@ +POM_ARTIFACT_ID=kotlinpoet-javapoet +POM_NAME=KotlinPoet (JavaPoet Interop) +POM_DESCRIPTION=Extensions for interop with JavaPoet. +POM_PACKAGING=jar diff --git a/interop/javapoet/src/main/kotlin/com/squareup/kotlinpoet/javapoet/K2jInterop.kt b/interop/javapoet/src/main/kotlin/com/squareup/kotlinpoet/javapoet/K2jInterop.kt new file mode 100644 index 00000000..82d7374a --- /dev/null +++ b/interop/javapoet/src/main/kotlin/com/squareup/kotlinpoet/javapoet/K2jInterop.kt @@ -0,0 +1,147 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.javapoet + +import com.squareup.javapoet.ArrayTypeName +import com.squareup.javapoet.TypeName +import com.squareup.kotlinpoet.ANY +import com.squareup.kotlinpoet.ARRAY +import com.squareup.kotlinpoet.BOOLEAN +import com.squareup.kotlinpoet.BOOLEAN_ARRAY +import com.squareup.kotlinpoet.BYTE +import com.squareup.kotlinpoet.BYTE_ARRAY +import com.squareup.kotlinpoet.CHAR +import com.squareup.kotlinpoet.CHAR_ARRAY +import com.squareup.kotlinpoet.CHAR_SEQUENCE +import com.squareup.kotlinpoet.DOUBLE +import com.squareup.kotlinpoet.DOUBLE_ARRAY +import com.squareup.kotlinpoet.Dynamic +import com.squareup.kotlinpoet.ENUM +import com.squareup.kotlinpoet.FLOAT +import com.squareup.kotlinpoet.FLOAT_ARRAY +import com.squareup.kotlinpoet.INT +import com.squareup.kotlinpoet.INT_ARRAY +import com.squareup.kotlinpoet.LIST +import com.squareup.kotlinpoet.LONG +import com.squareup.kotlinpoet.LONG_ARRAY +import com.squareup.kotlinpoet.LambdaTypeName +import com.squareup.kotlinpoet.MAP +import com.squareup.kotlinpoet.MUTABLE_LIST +import com.squareup.kotlinpoet.MUTABLE_MAP +import com.squareup.kotlinpoet.MUTABLE_SET +import com.squareup.kotlinpoet.SET +import com.squareup.kotlinpoet.SHORT +import com.squareup.kotlinpoet.SHORT_ARRAY +import com.squareup.kotlinpoet.STAR +import com.squareup.kotlinpoet.STRING +import com.squareup.kotlinpoet.U_BYTE +import com.squareup.kotlinpoet.U_BYTE_ARRAY +import com.squareup.kotlinpoet.U_INT +import com.squareup.kotlinpoet.U_INT_ARRAY +import com.squareup.kotlinpoet.U_LONG +import com.squareup.kotlinpoet.U_LONG_ARRAY +import com.squareup.kotlinpoet.U_SHORT +import com.squareup.kotlinpoet.U_SHORT_ARRAY + +@KotlinPoetJavaPoetPreview +public fun KClassName.toJClassName(boxIfPrimitive: Boolean = false): JTypeName { + return when (copy(nullable = false)) { + BOOLEAN -> JTypeName.BOOLEAN.boxIfPrimitive(boxIfPrimitive || isNullable) + BYTE, U_BYTE -> JTypeName.BYTE.boxIfPrimitive(boxIfPrimitive || isNullable) + CHAR -> JTypeName.CHAR.boxIfPrimitive(boxIfPrimitive || isNullable) + SHORT, U_SHORT -> JTypeName.SHORT.boxIfPrimitive(boxIfPrimitive || isNullable) + INT, U_INT -> JTypeName.INT.boxIfPrimitive(boxIfPrimitive || isNullable) + LONG, U_LONG -> JTypeName.LONG.boxIfPrimitive(boxIfPrimitive || isNullable) + FLOAT -> JTypeName.FLOAT.boxIfPrimitive(boxIfPrimitive || isNullable) + DOUBLE -> JTypeName.DOUBLE.boxIfPrimitive(boxIfPrimitive || isNullable) + ANY -> JTypeName.OBJECT + CHAR_SEQUENCE -> PoetInterop.CN_JAVA_CHAR_SEQUENCE + STRING -> PoetInterop.CN_JAVA_STRING + LIST, MUTABLE_LIST -> PoetInterop.CN_JAVA_LIST + SET, MUTABLE_SET -> PoetInterop.CN_JAVA_SET + MAP, MUTABLE_MAP -> PoetInterop.CN_JAVA_MAP + BOOLEAN_ARRAY -> ArrayTypeName.of(JTypeName.BOOLEAN) + BYTE_ARRAY, U_BYTE_ARRAY -> ArrayTypeName.of(JTypeName.BYTE) + CHAR_ARRAY -> ArrayTypeName.of(JTypeName.CHAR) + SHORT_ARRAY, U_SHORT_ARRAY -> ArrayTypeName.of(JTypeName.SHORT) + INT_ARRAY, U_INT_ARRAY -> ArrayTypeName.of(JTypeName.INT) + LONG_ARRAY, U_LONG_ARRAY -> ArrayTypeName.of(JTypeName.LONG) + FLOAT_ARRAY -> ArrayTypeName.of(JTypeName.FLOAT) + DOUBLE_ARRAY -> ArrayTypeName.of(JTypeName.DOUBLE) + ENUM -> PoetInterop.CN_JAVA_ENUM + else -> { + if (simpleNames.size == 1) { + JClassName.get(packageName, simpleName) + } else { + JClassName.get(packageName, simpleNames.first(), *simpleNames.drop(1).toTypedArray()) + } + } + } +} + +@KotlinPoetJavaPoetPreview +public fun KParameterizedTypeName.toJParameterizedOrArrayTypeName(): JTypeName { + return when (rawType) { + ARRAY -> { + val componentType = typeArguments.firstOrNull()?.toJTypeName() + ?: throw IllegalStateException("Array with no type! $this") + ArrayTypeName.of(componentType) + } + else -> { + JParameterizedTypeName.get( + rawType.toJClassName() as JClassName, + *typeArguments.map { it.toJTypeName(boxIfPrimitive = true) }.toTypedArray(), + ) + } + } +} + +@KotlinPoetJavaPoetPreview +public fun KParameterizedTypeName.toJParameterizedTypeName(): JParameterizedTypeName { + check(rawType != ARRAY) { + "Array type! JavaPoet arrays are a custom TypeName. Use this function only for things you know are not arrays" + } + return toJParameterizedOrArrayTypeName() as JParameterizedTypeName +} + +@KotlinPoetJavaPoetPreview +public fun KTypeVariableName.toJTypeVariableName(): JTypeVariableName { + return JTypeVariableName.get(name, *bounds.map { it.toJTypeName(boxIfPrimitive = true) }.toTypedArray()) +} + +@KotlinPoetJavaPoetPreview +public fun KWildcardTypeName.toJWildcardTypeName(): JWildcardTypeName { + return if (this == STAR) { + JWildcardTypeName.subtypeOf(TypeName.OBJECT) + } else if (inTypes.size == 1) { + JWildcardTypeName.supertypeOf(inTypes[0].toJTypeName()) + } else { + JWildcardTypeName.subtypeOf(outTypes[0].toJTypeName()) + } +} + +@KotlinPoetJavaPoetPreview +public fun KTypeName.toJTypeName(boxIfPrimitive: Boolean = false): JTypeName { + return when (this) { + is KClassName -> toJClassName(boxIfPrimitive) + Dynamic -> throw IllegalStateException("Not applicable in Java!") + // TODO should we return a ParameterizedTypeName of the KFunction? + is LambdaTypeName -> throw IllegalStateException("Not applicable in Java!") + is KParameterizedTypeName -> toJParameterizedOrArrayTypeName() + is KTypeVariableName -> toJTypeVariableName() + is KWildcardTypeName -> toJWildcardTypeName() + } +} diff --git a/interop/javapoet/src/main/kotlin/com/squareup/kotlinpoet/javapoet/KotlinPoetJavaPoetPreview.kt b/interop/javapoet/src/main/kotlin/com/squareup/kotlinpoet/javapoet/KotlinPoetJavaPoetPreview.kt new file mode 100644 index 00000000..7fcfd922 --- /dev/null +++ b/interop/javapoet/src/main/kotlin/com/squareup/kotlinpoet/javapoet/KotlinPoetJavaPoetPreview.kt @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.javapoet + +import kotlin.annotation.AnnotationTarget.CLASS +import kotlin.annotation.AnnotationTarget.FUNCTION +import kotlin.annotation.AnnotationTarget.PROPERTY +import kotlin.annotation.AnnotationTarget.TYPEALIAS + +/** + * Indicates that a given API is part of the experimental KotlinPoet JavaPoet support and is + * subject to API changes. + */ +@RequiresOptIn +@Retention(AnnotationRetention.BINARY) +@Target(CLASS, FUNCTION, PROPERTY, TYPEALIAS) +public annotation class KotlinPoetJavaPoetPreview diff --git a/interop/javapoet/src/main/kotlin/com/squareup/kotlinpoet/javapoet/PoetInterop.kt b/interop/javapoet/src/main/kotlin/com/squareup/kotlinpoet/javapoet/PoetInterop.kt new file mode 100644 index 00000000..005e828e --- /dev/null +++ b/interop/javapoet/src/main/kotlin/com/squareup/kotlinpoet/javapoet/PoetInterop.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.javapoet + +/** Various JavaPoet and KotlinPoet representations of some common types. */ +@OptIn(KotlinPoetJavaPoetPreview::class) +internal object PoetInterop { + internal val CN_JAVA_CHAR_SEQUENCE = JClassName.get("java.lang", "CharSequence") + internal val CN_JAVA_STRING = JClassName.get("java.lang", "String") + internal val CN_JAVA_LIST = JClassName.get("java.util", "List") + internal val CN_JAVA_SET = JClassName.get("java.util", "Set") + internal val CN_JAVA_MAP = JClassName.get("java.util", "Map") + internal val CN_JAVA_ENUM = JClassName.get("java.lang", "Enum") +} diff --git a/interop/javapoet/src/main/kotlin/com/squareup/kotlinpoet/javapoet/j2kInterop.kt b/interop/javapoet/src/main/kotlin/com/squareup/kotlinpoet/javapoet/j2kInterop.kt new file mode 100644 index 00000000..7cb8fdb4 --- /dev/null +++ b/interop/javapoet/src/main/kotlin/com/squareup/kotlinpoet/javapoet/j2kInterop.kt @@ -0,0 +1,150 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.javapoet + +import com.squareup.javapoet.ArrayTypeName +import com.squareup.javapoet.TypeName +import com.squareup.kotlinpoet.ANY +import com.squareup.kotlinpoet.ARRAY +import com.squareup.kotlinpoet.BOOLEAN +import com.squareup.kotlinpoet.BYTE +import com.squareup.kotlinpoet.BYTE_ARRAY +import com.squareup.kotlinpoet.CHAR +import com.squareup.kotlinpoet.CHAR_ARRAY +import com.squareup.kotlinpoet.CHAR_SEQUENCE +import com.squareup.kotlinpoet.DOUBLE +import com.squareup.kotlinpoet.DOUBLE_ARRAY +import com.squareup.kotlinpoet.ENUM +import com.squareup.kotlinpoet.FLOAT +import com.squareup.kotlinpoet.FLOAT_ARRAY +import com.squareup.kotlinpoet.INT +import com.squareup.kotlinpoet.INT_ARRAY +import com.squareup.kotlinpoet.LIST +import com.squareup.kotlinpoet.LONG +import com.squareup.kotlinpoet.LONG_ARRAY +import com.squareup.kotlinpoet.MAP +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.SET +import com.squareup.kotlinpoet.SHORT +import com.squareup.kotlinpoet.SHORT_ARRAY +import com.squareup.kotlinpoet.STAR +import com.squareup.kotlinpoet.STRING + +@KotlinPoetJavaPoetPreview +public fun JClassName.toKClassName(): KClassName { + return when (this) { + JTypeName.BOOLEAN.box() -> BOOLEAN + JTypeName.BYTE.box() -> BYTE + JTypeName.CHAR.box() -> CHAR + JTypeName.SHORT.box() -> SHORT + JTypeName.INT.box() -> INT + JTypeName.LONG.box() -> LONG + JTypeName.FLOAT.box() -> FLOAT + JTypeName.DOUBLE.box() -> DOUBLE + JTypeName.OBJECT -> ANY + PoetInterop.CN_JAVA_CHAR_SEQUENCE -> CHAR_SEQUENCE + PoetInterop.CN_JAVA_STRING -> STRING + PoetInterop.CN_JAVA_LIST -> LIST + PoetInterop.CN_JAVA_SET -> SET + PoetInterop.CN_JAVA_MAP -> MAP + PoetInterop.CN_JAVA_ENUM -> ENUM + else -> { + if (simpleNames().size == 1) { + KClassName(packageName(), simpleName()) + } else { + KClassName(packageName(), simpleNames().first(), *simpleNames().drop(1).toTypedArray()) + } + } + } +} + +@KotlinPoetJavaPoetPreview +public fun JParameterizedTypeName.toKParameterizedTypeName(): KParameterizedTypeName { + return rawType.toKClassName() + .parameterizedBy(*typeArguments.map { it.toKTypeName() }.toTypedArray()) +} + +@KotlinPoetJavaPoetPreview +public fun JTypeVariableName.toKTypeVariableName(): KTypeVariableName { + return if (bounds.isEmpty()) { + KTypeVariableName(name) + } else { + KTypeVariableName(name, *bounds.map { it.toKTypeName() }.toTypedArray()) + } +} + +@KotlinPoetJavaPoetPreview +public fun JWildcardTypeName.toKWildcardTypeName(): KWildcardTypeName { + return if (lowerBounds.size == 1) { + KWildcardTypeName.consumerOf(lowerBounds.first().toKTypeName()) + } else { + when (val upperBound = upperBounds[0]) { + TypeName.OBJECT -> STAR + else -> KWildcardTypeName.producerOf(upperBound.toKTypeName()) + } + } +} + +@KotlinPoetJavaPoetPreview +public fun JTypeName.toKTypeName(): KTypeName { + return when (this) { + is JClassName -> toKClassName() + is JParameterizedTypeName -> toKParameterizedTypeName() + is JTypeVariableName -> toKTypeVariableName() + is JWildcardTypeName -> toKWildcardTypeName() + is ArrayTypeName -> { + when (componentType) { + JTypeName.BYTE -> BYTE_ARRAY + JTypeName.CHAR -> CHAR_ARRAY + JTypeName.SHORT -> SHORT_ARRAY + JTypeName.INT -> INT_ARRAY + JTypeName.LONG -> LONG_ARRAY + JTypeName.FLOAT -> FLOAT_ARRAY + JTypeName.DOUBLE -> DOUBLE_ARRAY + else -> ARRAY.parameterizedBy(componentType.toKTypeName()) + } + } + else -> when (unboxIfBoxedPrimitive()) { + JTypeName.BOOLEAN -> BOOLEAN + JTypeName.BYTE -> BYTE + JTypeName.CHAR -> CHAR + JTypeName.SHORT -> SHORT + JTypeName.INT -> INT + JTypeName.LONG -> LONG + JTypeName.FLOAT -> FLOAT + JTypeName.DOUBLE -> DOUBLE + else -> error("Unrecognized type $this") + } + } +} + +@OptIn(KotlinPoetJavaPoetPreview::class) +internal fun JTypeName.unboxIfBoxedPrimitive(): JTypeName { + return if (isBoxedPrimitive) { + unbox() + } else { + this + } +} + +@OptIn(KotlinPoetJavaPoetPreview::class) +internal fun JTypeName.boxIfPrimitive(extraCondition: Boolean = true): JTypeName { + return if (extraCondition && isPrimitive && !isBoxedPrimitive) { + box() + } else { + this + } +} diff --git a/interop/javapoet/src/main/kotlin/com/squareup/kotlinpoet/javapoet/typeAliases.kt b/interop/javapoet/src/main/kotlin/com/squareup/kotlinpoet/javapoet/typeAliases.kt new file mode 100644 index 00000000..4982fef7 --- /dev/null +++ b/interop/javapoet/src/main/kotlin/com/squareup/kotlinpoet/javapoet/typeAliases.kt @@ -0,0 +1,62 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.javapoet + +/* + * Useful typealiases for colliding names + */ + +@KotlinPoetJavaPoetPreview +public typealias KTypeName = com.squareup.kotlinpoet.TypeName + +@KotlinPoetJavaPoetPreview +public typealias KClassName = com.squareup.kotlinpoet.ClassName + +@KotlinPoetJavaPoetPreview +public typealias KTypeVariableName = com.squareup.kotlinpoet.TypeVariableName + +@KotlinPoetJavaPoetPreview +public typealias KParameterizedTypeName = com.squareup.kotlinpoet.ParameterizedTypeName + +@KotlinPoetJavaPoetPreview +public typealias KWildcardTypeName = com.squareup.kotlinpoet.WildcardTypeName + +@KotlinPoetJavaPoetPreview +public typealias KTypeSpec = com.squareup.kotlinpoet.TypeSpec + +@KotlinPoetJavaPoetPreview +public typealias KAnnotationSpec = com.squareup.kotlinpoet.AnnotationSpec + +@KotlinPoetJavaPoetPreview +public typealias JTypeName = com.squareup.javapoet.TypeName + +@KotlinPoetJavaPoetPreview +public typealias JClassName = com.squareup.javapoet.ClassName + +@KotlinPoetJavaPoetPreview +public typealias JTypeVariableName = com.squareup.javapoet.TypeVariableName + +@KotlinPoetJavaPoetPreview +public typealias JParameterizedTypeName = com.squareup.javapoet.ParameterizedTypeName + +@KotlinPoetJavaPoetPreview +public typealias JWildcardTypeName = com.squareup.javapoet.WildcardTypeName + +@KotlinPoetJavaPoetPreview +public typealias JTypeSpec = com.squareup.javapoet.TypeSpec + +@KotlinPoetJavaPoetPreview +public typealias JAnnotationSpec = com.squareup.javapoet.AnnotationSpec diff --git a/interop/javapoet/src/test/kotlin/com/squareup/kotlinpoet/javapoet/PoetInteropTest.kt b/interop/javapoet/src/test/kotlin/com/squareup/kotlinpoet/javapoet/PoetInteropTest.kt new file mode 100644 index 00000000..b622e6b4 --- /dev/null +++ b/interop/javapoet/src/test/kotlin/com/squareup/kotlinpoet/javapoet/PoetInteropTest.kt @@ -0,0 +1,197 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.javapoet + +import com.google.common.truth.Truth.assertThat +import com.squareup.javapoet.ArrayTypeName +import com.squareup.kotlinpoet.ANY +import com.squareup.kotlinpoet.ARRAY +import com.squareup.kotlinpoet.BOOLEAN +import com.squareup.kotlinpoet.BYTE +import com.squareup.kotlinpoet.CHAR +import com.squareup.kotlinpoet.DOUBLE +import com.squareup.kotlinpoet.ENUM +import com.squareup.kotlinpoet.FLOAT +import com.squareup.kotlinpoet.INT +import com.squareup.kotlinpoet.INT_ARRAY +import com.squareup.kotlinpoet.LIST +import com.squareup.kotlinpoet.LONG +import com.squareup.kotlinpoet.MAP +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.SET +import com.squareup.kotlinpoet.SHORT +import com.squareup.kotlinpoet.STAR +import com.squareup.kotlinpoet.STRING +import com.squareup.kotlinpoet.U_BYTE +import com.squareup.kotlinpoet.U_INT +import com.squareup.kotlinpoet.U_LONG +import com.squareup.kotlinpoet.U_SHORT +import com.squareup.kotlinpoet.asClassName +import com.squareup.kotlinpoet.typeNameOf +import org.junit.Test + +@OptIn(KotlinPoetJavaPoetPreview::class) +class PoetInteropTest { + + @Test + fun classNamesMatch() { + val kotlinPoetCN = PoetInteropTest::class.asClassName() + val javapoetCN = kotlinPoetCN.toJClassName() + + assertThat(javapoetCN.toKTypeName()).isEqualTo(kotlinPoetCN) + assertThat(JClassName.get(PoetInteropTest::class.java)).isEqualTo(javapoetCN) + } + + @Test + fun nestedClassNamesMatch() { + val kotlinPoetCN = PoetInteropTest::class.asClassName().nestedClass("Foo").nestedClass("Bar") + val javapoetCN = kotlinPoetCN.toJClassName() + + assertThat(javapoetCN.toKTypeName()).isEqualTo(kotlinPoetCN) + assertThat(JClassName.get(PoetInteropTest::class.java).nestedClass("Foo").nestedClass("Bar")) + .isEqualTo(javapoetCN) + } + + @Test + fun kotlinIntrinsicsMapCorrectlyToJava() { + // To Java + assertThat(LIST.toJTypeName()).isEqualTo(PoetInterop.CN_JAVA_LIST) + assertThat(SET.toJTypeName()).isEqualTo(PoetInterop.CN_JAVA_SET) + assertThat(MAP.toJTypeName()).isEqualTo(PoetInterop.CN_JAVA_MAP) + assertThat(STRING.toJTypeName()).isEqualTo(PoetInterop.CN_JAVA_STRING) + assertThat(ANY.toJTypeName()).isEqualTo(JTypeName.OBJECT) + + // To Kotlin + assertThat(PoetInterop.CN_JAVA_LIST.toKTypeName()).isEqualTo(LIST) + assertThat(PoetInterop.CN_JAVA_SET.toKTypeName()).isEqualTo(SET) + assertThat(PoetInterop.CN_JAVA_MAP.toKTypeName()).isEqualTo(MAP) + assertThat(PoetInterop.CN_JAVA_STRING.toKTypeName()).isEqualTo(STRING) + assertThat(JTypeName.OBJECT.toKTypeName()).isEqualTo(ANY) + } + + @Test + fun boxIfPrimitiveRequestReturnsBoxedPrimitive() { + assertThat(BOOLEAN.toJTypeName(boxIfPrimitive = true)).isEqualTo(JTypeName.BOOLEAN.box()) + assertThat(BYTE.toJTypeName(boxIfPrimitive = true)).isEqualTo(JTypeName.BYTE.box()) + assertThat(CHAR.toJTypeName(boxIfPrimitive = true)).isEqualTo(JTypeName.CHAR.box()) + assertThat(SHORT.toJTypeName(boxIfPrimitive = true)).isEqualTo(JTypeName.SHORT.box()) + assertThat(INT.toJTypeName(boxIfPrimitive = true)).isEqualTo(JTypeName.INT.box()) + assertThat(LONG.toJTypeName(boxIfPrimitive = true)).isEqualTo(JTypeName.LONG.box()) + assertThat(FLOAT.toJTypeName(boxIfPrimitive = true)).isEqualTo(JTypeName.FLOAT.box()) + assertThat(DOUBLE.toJTypeName(boxIfPrimitive = true)).isEqualTo(JTypeName.DOUBLE.box()) + } + + @Test + fun primitivesAreUnboxedByDefault() { + assertThat(BOOLEAN.toJTypeName()).isEqualTo(JTypeName.BOOLEAN) + assertThat(BYTE.toJTypeName()).isEqualTo(JTypeName.BYTE) + assertThat(CHAR.toJTypeName()).isEqualTo(JTypeName.CHAR) + assertThat(SHORT.toJTypeName()).isEqualTo(JTypeName.SHORT) + assertThat(INT.toJTypeName()).isEqualTo(JTypeName.INT) + assertThat(LONG.toJTypeName()).isEqualTo(JTypeName.LONG) + assertThat(FLOAT.toJTypeName()).isEqualTo(JTypeName.FLOAT) + assertThat(DOUBLE.toJTypeName()).isEqualTo(JTypeName.DOUBLE) + } + + @Test + fun nullablePrimitiveBoxedByDefault() { + assertThat(BOOLEAN.copy(nullable = true).toJTypeName()).isEqualTo(JTypeName.BOOLEAN.box()) + assertThat(BYTE.copy(nullable = true).toJTypeName()).isEqualTo(JTypeName.BYTE.box()) + assertThat(CHAR.copy(nullable = true).toJTypeName()).isEqualTo(JTypeName.CHAR.box()) + assertThat(SHORT.copy(nullable = true).toJTypeName()).isEqualTo(JTypeName.SHORT.box()) + assertThat(INT.copy(nullable = true).toJTypeName()).isEqualTo(JTypeName.INT.box()) + assertThat(LONG.copy(nullable = true).toJTypeName()).isEqualTo(JTypeName.LONG.box()) + assertThat(FLOAT.copy(nullable = true).toJTypeName()).isEqualTo(JTypeName.FLOAT.box()) + assertThat(DOUBLE.copy(nullable = true).toJTypeName()).isEqualTo(JTypeName.DOUBLE.box()) + } + + @Test + fun arrayTypesConversion() { + assertThat(ARRAY.parameterizedBy(INT).toJParameterizedOrArrayTypeName()) + .isEqualTo(ArrayTypeName.of(JTypeName.INT)) + assertThat(ARRAY.parameterizedBy(INT.copy(nullable = true)).toJParameterizedOrArrayTypeName()) + .isEqualTo(ArrayTypeName.of(JTypeName.INT.box())) + assertThat(ArrayTypeName.of(JTypeName.INT).toKTypeName()).isEqualTo(INT_ARRAY) + assertThat(ArrayTypeName.of(JTypeName.INT.box()).toKTypeName()) + .isEqualTo(ARRAY.parameterizedBy(INT)) + } + + class GenericType<T> + + @Test + fun wildcards() { + val inKType = typeNameOf<GenericType<in String>>() + val superJType = JParameterizedTypeName.get( + JClassName.get(GenericType::class.java), + JWildcardTypeName.supertypeOf(String::class.java), + ) + assertThat(inKType.toJTypeName()).isEqualTo(superJType) + assertThat(superJType.toKTypeName()).isEqualTo(inKType) + + val outKType = typeNameOf<GenericType<out String>>() + val extendsJType = JParameterizedTypeName.get( + JClassName.get(GenericType::class.java), + JWildcardTypeName.subtypeOf(String::class.java), + ) + assertThat(outKType.toJTypeName()).isEqualTo(extendsJType) + assertThat(extendsJType.toKTypeName()).isEqualTo(outKType) + + val star = typeNameOf<GenericType<*>>() + val extendsObjectJType = JParameterizedTypeName.get( + JClassName.get(GenericType::class.java), + JWildcardTypeName.subtypeOf(JTypeName.OBJECT), + ) + assertThat(star.toJTypeName()).isEqualTo(extendsObjectJType) + assertThat(extendsObjectJType.toKTypeName()).isEqualTo(star) + assertThat(STAR.toJTypeName()).isEqualTo(JWildcardTypeName.subtypeOf(JTypeName.OBJECT)) + assertThat(JWildcardTypeName.subtypeOf(JTypeName.OBJECT).toKTypeName()).isEqualTo(STAR) + } + + @Test + fun complex() { + val complexType = typeNameOf<Map<String?, List<MutableMap<Int, IntArray>>>>() + val jType = JParameterizedTypeName.get( + JClassName.get(Map::class.java), + JClassName.get(String::class.java), + JParameterizedTypeName.get( + JClassName.get(List::class.java), + JParameterizedTypeName.get( + JClassName.get(Map::class.java), + JClassName.INT.box(), + ArrayTypeName.of(JClassName.INT), + ), + ), + ) + assertThat(complexType.toJTypeName()).isEqualTo(jType) + + assertThat(jType.toKTypeName()) + .isEqualTo(typeNameOf<Map<String, List<MutableMap<Int, IntArray>>>>()) + } + + @Test + fun uTypesAreJustNormalTypesInJava() { + assertThat(U_BYTE.toJTypeName()).isEqualTo(JTypeName.BYTE) + assertThat(U_SHORT.toJTypeName()).isEqualTo(JTypeName.SHORT) + assertThat(U_INT.toJTypeName()).isEqualTo(JTypeName.INT) + assertThat(U_LONG.toJTypeName()).isEqualTo(JTypeName.LONG) + } + + @Test + fun enums() { + assertThat(ENUM.toJTypeName()).isEqualTo(PoetInterop.CN_JAVA_ENUM) + assertThat(PoetInterop.CN_JAVA_ENUM.toKTypeName()).isEqualTo(ENUM) + } +} diff --git a/interop/kotlinx-metadata/api/kotlinx-metadata.api b/interop/kotlinx-metadata/api/kotlinx-metadata.api new file mode 100644 index 00000000..96f47c0c --- /dev/null +++ b/interop/kotlinx-metadata/api/kotlinx-metadata.api @@ -0,0 +1,369 @@ +public final class com/squareup/kotlinpoet/metadata/FlagsKt { + public static final fun getDeclaresDefaultValue (Lkotlinx/metadata/KmValueParameter;)Z + public static final fun getGetterPropertyAccessorFlags (Lkotlinx/metadata/KmProperty;)Ljava/util/Set; + public static final fun getHasAnnotations (I)Z + public static final fun getHasConstant (Lkotlinx/metadata/KmProperty;)Z + public static final fun getHasGetter (Lkotlinx/metadata/KmProperty;)Z + public static final fun getHasSetter (Lkotlinx/metadata/KmProperty;)Z + public static final fun getPropertyAccessorFlags (I)Ljava/util/Set; + public static final fun getSetterPropertyAccessorFlags (Lkotlinx/metadata/KmProperty;)Ljava/util/Set; + public static final fun isAbstract (I)Z + public static final fun isAnnotation (Lkotlinx/metadata/KmClass;)Z + public static final fun isAnnotationClass (I)Z + public static final fun isClass (I)Z + public static final fun isClass (Lkotlinx/metadata/KmClass;)Z + public static final fun isCompanionObject (Lkotlinx/metadata/KmClass;)Z + public static final fun isCompanionObjectClass (I)Z + public static final fun isConst (Lkotlinx/metadata/KmProperty;)Z + public static final fun isCrossInline (Lkotlinx/metadata/KmValueParameter;)Z + public static final fun isData (Lkotlinx/metadata/KmClass;)Z + public static final fun isDataClass (I)Z + public static final fun isDeclaration (Lkotlinx/metadata/KmFunction;)Z + public static final fun isDeclaration (Lkotlinx/metadata/KmProperty;)Z + public static final fun isDeclarationFunction (I)Z + public static final fun isDelegated (Lkotlinx/metadata/KmProperty;)Z + public static final fun isDelegation (Lkotlinx/metadata/KmFunction;)Z + public static final fun isDelegation (Lkotlinx/metadata/KmProperty;)Z + public static final fun isDelegationFunction (I)Z + public static final fun isEnum (Lkotlinx/metadata/KmClass;)Z + public static final fun isEnumClass (I)Z + public static final fun isEnumEntry (Lkotlinx/metadata/KmClass;)Z + public static final fun isEnumEntryClass (I)Z + public static final fun isExpect (Lkotlinx/metadata/KmClass;)Z + public static final fun isExpect (Lkotlinx/metadata/KmFunction;)Z + public static final fun isExpect (Lkotlinx/metadata/KmProperty;)Z + public static final fun isExpectClass (I)Z + public static final fun isExpectFunction (I)Z + public static final fun isExternal (Lkotlinx/metadata/KmClass;)Z + public static final fun isExternal (Lkotlinx/metadata/KmFunction;)Z + public static final fun isExternal (Lkotlinx/metadata/KmProperty;)Z + public static final fun isExternalClass (I)Z + public static final fun isExternalFunction (I)Z + public static final fun isFakeOverride (Lkotlinx/metadata/KmFunction;)Z + public static final fun isFakeOverride (Lkotlinx/metadata/KmProperty;)Z + public static final fun isFakeOverrideFunction (I)Z + public static final fun isFakeOverrideProperty (I)Z + public static final fun isFinal (I)Z + public static final fun isFun (I)Z + public static final fun isFun (Lkotlinx/metadata/KmClass;)Z + public static final fun isInfix (Lkotlinx/metadata/KmFunction;)Z + public static final fun isInfixFunction (I)Z + public static final fun isInline (Lkotlinx/metadata/KmFunction;)Z + public static final fun isInlineFunction (I)Z + public static final fun isInner (Lkotlinx/metadata/KmClass;)Z + public static final fun isInnerClass (I)Z + public static final fun isInterface (I)Z + public static final fun isInterface (Lkotlinx/metadata/KmClass;)Z + public static final fun isInternal (I)Z + public static final fun isLateinit (Lkotlinx/metadata/KmProperty;)Z + public static final fun isLocal (I)Z + public static final fun isNoInline (Lkotlinx/metadata/KmValueParameter;)Z + public static final fun isNullable (Lkotlinx/metadata/KmType;)Z + public static final fun isNullableType (I)Z + public static final fun isObject (Lkotlinx/metadata/KmClass;)Z + public static final fun isObjectClass (I)Z + public static final fun isOpen (I)Z + public static final fun isOperator (Lkotlinx/metadata/KmFunction;)Z + public static final fun isOperatorFunction (I)Z + public static final fun isPrimary (Lkotlinx/metadata/KmConstructor;)Z + public static final fun isPrimaryConstructor (I)Z + public static final fun isPrivate (I)Z + public static final fun isPrivate_to_this (I)Z + public static final fun isPropertyAccessorExternal (I)Z + public static final fun isPropertyAccessorInline (I)Z + public static final fun isPropertyAccessorNotDefault (I)Z + public static final fun isProtected (I)Z + public static final fun isPublic (I)Z + public static final fun isReified (Lkotlinx/metadata/KmTypeParameter;)Z + public static final fun isSealed (I)Z + public static final fun isSecondary (Lkotlinx/metadata/KmConstructor;)Z + public static final fun isSuspend (Lkotlinx/metadata/KmFunction;)Z + public static final fun isSuspend (Lkotlinx/metadata/KmType;)Z + public static final fun isSuspendFunction (I)Z + public static final fun isSuspendType (I)Z + public static final fun isSynthesized (Lkotlinx/metadata/KmFunction;)Z + public static final fun isSynthesized (Lkotlinx/metadata/KmProperty;)Z + public static final fun isSynthesizedFunction (I)Z + public static final fun isTailRec (Lkotlinx/metadata/KmFunction;)Z + public static final fun isTailRecFunction (I)Z + public static final fun isVal (Lkotlinx/metadata/KmProperty;)Z + public static final fun isValue (Lkotlinx/metadata/KmClass;)Z + public static final fun isValueClass (I)Z + public static final fun isVar (Lkotlinx/metadata/KmProperty;)Z +} + +public final class com/squareup/kotlinpoet/metadata/KotlinPoetMetadata { + public static final fun readKotlinClassMetadata (Lkotlin/Metadata;)Lkotlinx/metadata/jvm/KotlinClassMetadata; + public static final fun toKmClass (Ljava/lang/Class;)Lkotlinx/metadata/KmClass; + public static final fun toKmClass (Ljavax/lang/model/element/TypeElement;)Lkotlinx/metadata/KmClass; + public static final fun toKmClass (Lkotlin/Metadata;)Lkotlinx/metadata/KmClass; + public static final fun toKmClass (Lkotlin/reflect/KClass;)Lkotlinx/metadata/KmClass; +} + +public abstract interface annotation class com/squareup/kotlinpoet/metadata/KotlinPoetMetadataPreview : java/lang/annotation/Annotation { +} + +public final class com/squareup/kotlinpoet/metadata/PropertyAccessorFlag : java/lang/Enum { + public static final field IS_EXTERNAL Lcom/squareup/kotlinpoet/metadata/PropertyAccessorFlag; + public static final field IS_INLINE Lcom/squareup/kotlinpoet/metadata/PropertyAccessorFlag; + public static final field IS_NOT_DEFAULT Lcom/squareup/kotlinpoet/metadata/PropertyAccessorFlag; + public static fun valueOf (Ljava/lang/String;)Lcom/squareup/kotlinpoet/metadata/PropertyAccessorFlag; + public static fun values ()[Lcom/squareup/kotlinpoet/metadata/PropertyAccessorFlag; +} + +public final class com/squareup/kotlinpoet/metadata/classinspectors/ElementsClassInspector : com/squareup/kotlinpoet/metadata/specs/ClassInspector { + public static final field Companion Lcom/squareup/kotlinpoet/metadata/classinspectors/ElementsClassInspector$Companion; + public synthetic fun <init> (Ljavax/lang/model/util/Elements;Ljavax/lang/model/util/Types;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun containerData (Lkotlinx/metadata/KmDeclarationContainer;Lcom/squareup/kotlinpoet/ClassName;Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/metadata/specs/ContainerData; + public static final fun create (Ljavax/lang/model/util/Elements;Ljavax/lang/model/util/Types;)Lcom/squareup/kotlinpoet/metadata/specs/ClassInspector; + public fun declarationContainerFor (Lcom/squareup/kotlinpoet/ClassName;)Lkotlinx/metadata/KmDeclarationContainer; + public fun enumEntry (Lcom/squareup/kotlinpoet/ClassName;Ljava/lang/String;)Lcom/squareup/kotlinpoet/metadata/specs/EnumEntryData; + public fun getSupportsNonRuntimeRetainedAnnotations ()Z + public fun isInterface (Lcom/squareup/kotlinpoet/ClassName;)Z + public fun methodExists (Lcom/squareup/kotlinpoet/ClassName;Lkotlinx/metadata/jvm/JvmMethodSignature;)Z +} + +public final class com/squareup/kotlinpoet/metadata/classinspectors/ElementsClassInspector$Companion { + public final fun create (Ljavax/lang/model/util/Elements;Ljavax/lang/model/util/Types;)Lcom/squareup/kotlinpoet/metadata/specs/ClassInspector; +} + +public final class com/squareup/kotlinpoet/metadata/classinspectors/ReflectiveClassInspector : com/squareup/kotlinpoet/metadata/specs/ClassInspector { + public static final field Companion Lcom/squareup/kotlinpoet/metadata/classinspectors/ReflectiveClassInspector$Companion; + public synthetic fun <init> (Ljava/lang/ClassLoader;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun containerData (Lkotlinx/metadata/KmDeclarationContainer;Lcom/squareup/kotlinpoet/ClassName;Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/metadata/specs/ContainerData; + public static final fun create (Ljava/lang/ClassLoader;)Lcom/squareup/kotlinpoet/metadata/specs/ClassInspector; + public fun declarationContainerFor (Lcom/squareup/kotlinpoet/ClassName;)Lkotlinx/metadata/KmDeclarationContainer; + public fun enumEntry (Lcom/squareup/kotlinpoet/ClassName;Ljava/lang/String;)Lcom/squareup/kotlinpoet/metadata/specs/EnumEntryData; + public fun getSupportsNonRuntimeRetainedAnnotations ()Z + public fun isInterface (Lcom/squareup/kotlinpoet/ClassName;)Z + public fun methodExists (Lcom/squareup/kotlinpoet/ClassName;Lkotlinx/metadata/jvm/JvmMethodSignature;)Z +} + +public final class com/squareup/kotlinpoet/metadata/classinspectors/ReflectiveClassInspector$Companion { + public final fun create (Ljava/lang/ClassLoader;)Lcom/squareup/kotlinpoet/metadata/specs/ClassInspector; + public static synthetic fun create$default (Lcom/squareup/kotlinpoet/metadata/classinspectors/ReflectiveClassInspector$Companion;Ljava/lang/ClassLoader;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/metadata/specs/ClassInspector; +} + +public final class com/squareup/kotlinpoet/metadata/specs/ClassData : com/squareup/kotlinpoet/metadata/specs/ContainerData { + public fun <init> (Lkotlinx/metadata/KmClass;Lcom/squareup/kotlinpoet/ClassName;Ljava/util/Collection;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)V + public final fun component1 ()Lkotlinx/metadata/KmClass; + public final fun component2 ()Lcom/squareup/kotlinpoet/ClassName; + public final fun component3 ()Ljava/util/Collection; + public final fun component4 ()Ljava/util/Map; + public final fun component5 ()Ljava/util/Map; + public final fun component6 ()Ljava/util/Map; + public final fun copy (Lkotlinx/metadata/KmClass;Lcom/squareup/kotlinpoet/ClassName;Ljava/util/Collection;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;)Lcom/squareup/kotlinpoet/metadata/specs/ClassData; + public static synthetic fun copy$default (Lcom/squareup/kotlinpoet/metadata/specs/ClassData;Lkotlinx/metadata/KmClass;Lcom/squareup/kotlinpoet/ClassName;Ljava/util/Collection;Ljava/util/Map;Ljava/util/Map;Ljava/util/Map;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/metadata/specs/ClassData; + public fun equals (Ljava/lang/Object;)Z + public fun getAnnotations ()Ljava/util/Collection; + public final fun getClassName ()Lcom/squareup/kotlinpoet/ClassName; + public final fun getConstructors ()Ljava/util/Map; + public fun getDeclarationContainer ()Lkotlinx/metadata/KmClass; + public synthetic fun getDeclarationContainer ()Lkotlinx/metadata/KmDeclarationContainer; + public fun getMethods ()Ljava/util/Map; + public fun getProperties ()Ljava/util/Map; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public abstract interface class com/squareup/kotlinpoet/metadata/specs/ClassInspector { + public abstract fun containerData (Lkotlinx/metadata/KmDeclarationContainer;Lcom/squareup/kotlinpoet/ClassName;Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/metadata/specs/ContainerData; + public abstract fun declarationContainerFor (Lcom/squareup/kotlinpoet/ClassName;)Lkotlinx/metadata/KmDeclarationContainer; + public abstract fun enumEntry (Lcom/squareup/kotlinpoet/ClassName;Ljava/lang/String;)Lcom/squareup/kotlinpoet/metadata/specs/EnumEntryData; + public abstract fun getSupportsNonRuntimeRetainedAnnotations ()Z + public abstract fun isInterface (Lcom/squareup/kotlinpoet/ClassName;)Z + public abstract fun methodExists (Lcom/squareup/kotlinpoet/ClassName;Lkotlinx/metadata/jvm/JvmMethodSignature;)Z +} + +public final class com/squareup/kotlinpoet/metadata/specs/ClassInspectorKt { + public static final fun classFor (Lcom/squareup/kotlinpoet/metadata/specs/ClassInspector;Lcom/squareup/kotlinpoet/ClassName;)Lkotlinx/metadata/KmClass; + public static final fun containerData (Lcom/squareup/kotlinpoet/metadata/specs/ClassInspector;Lcom/squareup/kotlinpoet/ClassName;Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/metadata/specs/ContainerData; +} + +public final class com/squareup/kotlinpoet/metadata/specs/ConstructorData { + public static final field Companion Lcom/squareup/kotlinpoet/metadata/specs/ConstructorData$Companion; + public fun <init> (Ljava/util/List;Ljava/util/Map;ZLjava/util/Set;Ljava/util/List;)V + public final fun component2 ()Ljava/util/Map; + public final fun component3 ()Z + public final fun component4 ()Ljava/util/Set; + public final fun component5 ()Ljava/util/List; + public final fun copy (Ljava/util/List;Ljava/util/Map;ZLjava/util/Set;Ljava/util/List;)Lcom/squareup/kotlinpoet/metadata/specs/ConstructorData; + public static synthetic fun copy$default (Lcom/squareup/kotlinpoet/metadata/specs/ConstructorData;Ljava/util/List;Ljava/util/Map;ZLjava/util/Set;Ljava/util/List;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/metadata/specs/ConstructorData; + public fun equals (Ljava/lang/Object;)Z + public final fun getAllAnnotations ()Ljava/util/Collection; + public final fun getExceptions ()Ljava/util/List; + public final fun getJvmModifiers ()Ljava/util/Set; + public final fun getParameterAnnotations ()Ljava/util/Map; + public fun hashCode ()I + public final fun isSynthetic ()Z + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/kotlinpoet/metadata/specs/ConstructorData$Companion { + public final fun getEMPTY ()Lcom/squareup/kotlinpoet/metadata/specs/ConstructorData; +} + +public abstract interface class com/squareup/kotlinpoet/metadata/specs/ContainerData { + public abstract fun getAnnotations ()Ljava/util/Collection; + public abstract fun getDeclarationContainer ()Lkotlinx/metadata/KmDeclarationContainer; + public abstract fun getMethods ()Ljava/util/Map; + public abstract fun getProperties ()Ljava/util/Map; +} + +public final class com/squareup/kotlinpoet/metadata/specs/EnumEntryData { + public fun <init> (Lkotlinx/metadata/KmClass;Ljava/util/Collection;)V + public final fun component1 ()Lkotlinx/metadata/KmClass; + public final fun component2 ()Ljava/util/Collection; + public final fun copy (Lkotlinx/metadata/KmClass;Ljava/util/Collection;)Lcom/squareup/kotlinpoet/metadata/specs/EnumEntryData; + public static synthetic fun copy$default (Lcom/squareup/kotlinpoet/metadata/specs/EnumEntryData;Lkotlinx/metadata/KmClass;Ljava/util/Collection;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/metadata/specs/EnumEntryData; + public fun equals (Ljava/lang/Object;)Z + public final fun getAnnotations ()Ljava/util/Collection; + public final fun getDeclarationContainer ()Lkotlinx/metadata/KmClass; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/kotlinpoet/metadata/specs/FieldData { + public static final field Companion Lcom/squareup/kotlinpoet/metadata/specs/FieldData$Companion; + public fun <init> (Ljava/util/List;ZLjava/util/Set;Lcom/squareup/kotlinpoet/CodeBlock;)V + public final fun component2 ()Z + public final fun component3 ()Ljava/util/Set; + public final fun component4 ()Lcom/squareup/kotlinpoet/CodeBlock; + public final fun copy (Ljava/util/List;ZLjava/util/Set;Lcom/squareup/kotlinpoet/CodeBlock;)Lcom/squareup/kotlinpoet/metadata/specs/FieldData; + public static synthetic fun copy$default (Lcom/squareup/kotlinpoet/metadata/specs/FieldData;Ljava/util/List;ZLjava/util/Set;Lcom/squareup/kotlinpoet/CodeBlock;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/metadata/specs/FieldData; + public fun equals (Ljava/lang/Object;)Z + public final fun getAllAnnotations ()Ljava/util/Collection; + public final fun getConstant ()Lcom/squareup/kotlinpoet/CodeBlock; + public final fun getJvmModifiers ()Ljava/util/Set; + public fun hashCode ()I + public final fun isSynthetic ()Z + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/kotlinpoet/metadata/specs/FieldData$Companion { + public final fun getSYNTHETIC ()Lcom/squareup/kotlinpoet/metadata/specs/FieldData; +} + +public final class com/squareup/kotlinpoet/metadata/specs/FileData : com/squareup/kotlinpoet/metadata/specs/ContainerData { + public fun <init> (Lkotlinx/metadata/KmPackage;Ljava/util/Collection;Ljava/util/Map;Ljava/util/Map;Lcom/squareup/kotlinpoet/ClassName;Ljava/lang/String;)V + public synthetic fun <init> (Lkotlinx/metadata/KmPackage;Ljava/util/Collection;Ljava/util/Map;Ljava/util/Map;Lcom/squareup/kotlinpoet/ClassName;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun component1 ()Lkotlinx/metadata/KmPackage; + public final fun component2 ()Ljava/util/Collection; + public final fun component3 ()Ljava/util/Map; + public final fun component4 ()Ljava/util/Map; + public final fun component5 ()Lcom/squareup/kotlinpoet/ClassName; + public final fun component6 ()Ljava/lang/String; + public final fun copy (Lkotlinx/metadata/KmPackage;Ljava/util/Collection;Ljava/util/Map;Ljava/util/Map;Lcom/squareup/kotlinpoet/ClassName;Ljava/lang/String;)Lcom/squareup/kotlinpoet/metadata/specs/FileData; + public static synthetic fun copy$default (Lcom/squareup/kotlinpoet/metadata/specs/FileData;Lkotlinx/metadata/KmPackage;Ljava/util/Collection;Ljava/util/Map;Ljava/util/Map;Lcom/squareup/kotlinpoet/ClassName;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/metadata/specs/FileData; + public fun equals (Ljava/lang/Object;)Z + public fun getAnnotations ()Ljava/util/Collection; + public final fun getClassName ()Lcom/squareup/kotlinpoet/ClassName; + public synthetic fun getDeclarationContainer ()Lkotlinx/metadata/KmDeclarationContainer; + public fun getDeclarationContainer ()Lkotlinx/metadata/KmPackage; + public final fun getFileName ()Ljava/lang/String; + public final fun getJvmName ()Ljava/lang/String; + public fun getMethods ()Ljava/util/Map; + public fun getProperties ()Ljava/util/Map; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public class com/squareup/kotlinpoet/metadata/specs/JvmFieldModifier : java/lang/Enum, com/squareup/kotlinpoet/metadata/specs/JvmModifier { + public static final field STATIC Lcom/squareup/kotlinpoet/metadata/specs/JvmFieldModifier; + public static final field TRANSIENT Lcom/squareup/kotlinpoet/metadata/specs/JvmFieldModifier; + public static final field VOLATILE Lcom/squareup/kotlinpoet/metadata/specs/JvmFieldModifier; + public synthetic fun <init> (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun annotationSpec ()Lcom/squareup/kotlinpoet/AnnotationSpec; + public static fun valueOf (Ljava/lang/String;)Lcom/squareup/kotlinpoet/metadata/specs/JvmFieldModifier; + public static fun values ()[Lcom/squareup/kotlinpoet/metadata/specs/JvmFieldModifier; +} + +public class com/squareup/kotlinpoet/metadata/specs/JvmMethodModifier : java/lang/Enum, com/squareup/kotlinpoet/metadata/specs/JvmModifier { + public static final field DEFAULT Lcom/squareup/kotlinpoet/metadata/specs/JvmMethodModifier; + public static final field STATIC Lcom/squareup/kotlinpoet/metadata/specs/JvmMethodModifier; + public static final field SYNCHRONIZED Lcom/squareup/kotlinpoet/metadata/specs/JvmMethodModifier; + public synthetic fun <init> (Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V + public fun annotationSpec ()Lcom/squareup/kotlinpoet/AnnotationSpec; + public static fun valueOf (Ljava/lang/String;)Lcom/squareup/kotlinpoet/metadata/specs/JvmMethodModifier; + public static fun values ()[Lcom/squareup/kotlinpoet/metadata/specs/JvmMethodModifier; +} + +public abstract interface class com/squareup/kotlinpoet/metadata/specs/JvmModifier { + public abstract fun annotationSpec ()Lcom/squareup/kotlinpoet/AnnotationSpec; +} + +public final class com/squareup/kotlinpoet/metadata/specs/JvmModifier$DefaultImpls { + public static fun annotationSpec (Lcom/squareup/kotlinpoet/metadata/specs/JvmModifier;)Lcom/squareup/kotlinpoet/AnnotationSpec; +} + +public final class com/squareup/kotlinpoet/metadata/specs/KmTypesKt { + public static final fun isExtensionType (Lkotlinx/metadata/KmType;)Z +} + +public final class com/squareup/kotlinpoet/metadata/specs/KotlinPoetMetadataSpecs { + public static final fun getPackageName (Ljavax/lang/model/element/Element;)Ljava/lang/String; + public static final fun toFileSpec (Ljava/lang/Class;Lcom/squareup/kotlinpoet/metadata/specs/ClassInspector;)Lcom/squareup/kotlinpoet/FileSpec; + public static final fun toFileSpec (Ljavax/lang/model/element/TypeElement;Lcom/squareup/kotlinpoet/metadata/specs/ClassInspector;)Lcom/squareup/kotlinpoet/FileSpec; + public static final fun toFileSpec (Lkotlin/reflect/KClass;Lcom/squareup/kotlinpoet/metadata/specs/ClassInspector;)Lcom/squareup/kotlinpoet/FileSpec; + public static final fun toFileSpec (Lkotlinx/metadata/KmClass;Lcom/squareup/kotlinpoet/metadata/specs/ClassInspector;Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/FileSpec; + public static final fun toFileSpec (Lkotlinx/metadata/KmPackage;Lcom/squareup/kotlinpoet/metadata/specs/ClassInspector;Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/FileSpec; + public static synthetic fun toFileSpec$default (Ljava/lang/Class;Lcom/squareup/kotlinpoet/metadata/specs/ClassInspector;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/FileSpec; + public static synthetic fun toFileSpec$default (Ljavax/lang/model/element/TypeElement;Lcom/squareup/kotlinpoet/metadata/specs/ClassInspector;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/FileSpec; + public static synthetic fun toFileSpec$default (Lkotlin/reflect/KClass;Lcom/squareup/kotlinpoet/metadata/specs/ClassInspector;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/FileSpec; + public static synthetic fun toFileSpec$default (Lkotlinx/metadata/KmClass;Lcom/squareup/kotlinpoet/metadata/specs/ClassInspector;Lcom/squareup/kotlinpoet/ClassName;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/FileSpec; + public static final fun toTypeSpec (Ljava/lang/Class;Lcom/squareup/kotlinpoet/metadata/specs/ClassInspector;)Lcom/squareup/kotlinpoet/TypeSpec; + public static final fun toTypeSpec (Ljavax/lang/model/element/TypeElement;Lcom/squareup/kotlinpoet/metadata/specs/ClassInspector;)Lcom/squareup/kotlinpoet/TypeSpec; + public static final fun toTypeSpec (Lkotlin/reflect/KClass;Lcom/squareup/kotlinpoet/metadata/specs/ClassInspector;)Lcom/squareup/kotlinpoet/TypeSpec; + public static final fun toTypeSpec (Lkotlinx/metadata/KmClass;Lcom/squareup/kotlinpoet/metadata/specs/ClassInspector;Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/TypeSpec; + public static synthetic fun toTypeSpec$default (Ljava/lang/Class;Lcom/squareup/kotlinpoet/metadata/specs/ClassInspector;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/TypeSpec; + public static synthetic fun toTypeSpec$default (Ljavax/lang/model/element/TypeElement;Lcom/squareup/kotlinpoet/metadata/specs/ClassInspector;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/TypeSpec; + public static synthetic fun toTypeSpec$default (Lkotlin/reflect/KClass;Lcom/squareup/kotlinpoet/metadata/specs/ClassInspector;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/TypeSpec; + public static synthetic fun toTypeSpec$default (Lkotlinx/metadata/KmClass;Lcom/squareup/kotlinpoet/metadata/specs/ClassInspector;Lcom/squareup/kotlinpoet/ClassName;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/TypeSpec; +} + +public final class com/squareup/kotlinpoet/metadata/specs/MethodData { + public static final field Companion Lcom/squareup/kotlinpoet/metadata/specs/MethodData$Companion; + public fun <init> (Ljava/util/List;Ljava/util/Map;ZLjava/util/Set;ZLjava/util/List;)V + public final fun allAnnotations (Lcom/squareup/kotlinpoet/AnnotationSpec$UseSiteTarget;Z)Ljava/util/Collection; + public static synthetic fun allAnnotations$default (Lcom/squareup/kotlinpoet/metadata/specs/MethodData;Lcom/squareup/kotlinpoet/AnnotationSpec$UseSiteTarget;ZILjava/lang/Object;)Ljava/util/Collection; + public final fun component2 ()Ljava/util/Map; + public final fun component3 ()Z + public final fun component4 ()Ljava/util/Set; + public final fun component5 ()Z + public final fun component6 ()Ljava/util/List; + public final fun copy (Ljava/util/List;Ljava/util/Map;ZLjava/util/Set;ZLjava/util/List;)Lcom/squareup/kotlinpoet/metadata/specs/MethodData; + public static synthetic fun copy$default (Lcom/squareup/kotlinpoet/metadata/specs/MethodData;Ljava/util/List;Ljava/util/Map;ZLjava/util/Set;ZLjava/util/List;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/metadata/specs/MethodData; + public fun equals (Ljava/lang/Object;)Z + public final fun getExceptions ()Ljava/util/List; + public final fun getJvmModifiers ()Ljava/util/Set; + public final fun getParameterAnnotations ()Ljava/util/Map; + public fun hashCode ()I + public final fun isOverride ()Z + public final fun isSynthetic ()Z + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/kotlinpoet/metadata/specs/MethodData$Companion { + public final fun getEMPTY ()Lcom/squareup/kotlinpoet/metadata/specs/MethodData; + public final fun getSYNTHETIC ()Lcom/squareup/kotlinpoet/metadata/specs/MethodData; +} + +public final class com/squareup/kotlinpoet/metadata/specs/PropertyData { + public fun <init> (Ljava/util/List;Lcom/squareup/kotlinpoet/metadata/specs/FieldData;Lcom/squareup/kotlinpoet/metadata/specs/MethodData;Lcom/squareup/kotlinpoet/metadata/specs/MethodData;Z)V + public final fun component2 ()Lcom/squareup/kotlinpoet/metadata/specs/FieldData; + public final fun component3 ()Lcom/squareup/kotlinpoet/metadata/specs/MethodData; + public final fun component4 ()Lcom/squareup/kotlinpoet/metadata/specs/MethodData; + public final fun component5 ()Z + public final fun copy (Ljava/util/List;Lcom/squareup/kotlinpoet/metadata/specs/FieldData;Lcom/squareup/kotlinpoet/metadata/specs/MethodData;Lcom/squareup/kotlinpoet/metadata/specs/MethodData;Z)Lcom/squareup/kotlinpoet/metadata/specs/PropertyData; + public static synthetic fun copy$default (Lcom/squareup/kotlinpoet/metadata/specs/PropertyData;Ljava/util/List;Lcom/squareup/kotlinpoet/metadata/specs/FieldData;Lcom/squareup/kotlinpoet/metadata/specs/MethodData;Lcom/squareup/kotlinpoet/metadata/specs/MethodData;ZILjava/lang/Object;)Lcom/squareup/kotlinpoet/metadata/specs/PropertyData; + public fun equals (Ljava/lang/Object;)Z + public final fun getAllAnnotations ()Ljava/util/Collection; + public final fun getFieldData ()Lcom/squareup/kotlinpoet/metadata/specs/FieldData; + public final fun getGetterData ()Lcom/squareup/kotlinpoet/metadata/specs/MethodData; + public final fun getSetterData ()Lcom/squareup/kotlinpoet/metadata/specs/MethodData; + public fun hashCode ()I + public final fun isJvmField ()Z + public final fun isOverride ()Z + public fun toString ()Ljava/lang/String; +} + diff --git a/interop/kotlinx-metadata/build.gradle.kts b/interop/kotlinx-metadata/build.gradle.kts new file mode 100644 index 00000000..df75e1ad --- /dev/null +++ b/interop/kotlinx-metadata/build.gradle.kts @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +tasks.jar { + manifest { + attributes("Automatic-Module-Name" to "com.squareup.kotlinpoet.metadata") + } +} + +tasks.compileTestKotlin { + kotlinOptions { + freeCompilerArgs = listOf( + "-Xjvm-default=all", + "-opt-in=com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview", + ) + } +} + +dependencies { + implementation(libs.autoCommon) + implementation(libs.guava) + api(libs.kotlin.metadata) + api(project(":kotlinpoet")) + + testImplementation(libs.kotlin.junit) + testImplementation(libs.truth) + testImplementation(libs.compileTesting) + testImplementation(libs.kotlinCompileTesting) + testImplementation(libs.kotlin.annotationProcessingEmbeddable) + testImplementation(libs.kotlin.compilerEmbeddable) +} diff --git a/interop/kotlinx-metadata/gradle.properties b/interop/kotlinx-metadata/gradle.properties new file mode 100644 index 00000000..dba6fb43 --- /dev/null +++ b/interop/kotlinx-metadata/gradle.properties @@ -0,0 +1,4 @@ +POM_ARTIFACT_ID=kotlinpoet-metadata +POM_NAME=KotlinPoet (Kotlin Metadata Extensions) +POM_DESCRIPTION=Extensions for reading Kotlin metadata from Metadata annotations. +POM_PACKAGING=jar diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/Flags.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/Flags.kt new file mode 100644 index 00000000..4745dda0 --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/Flags.kt @@ -0,0 +1,338 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:Suppress("unused") + +package com.squareup.kotlinpoet.metadata + +import kotlinx.metadata.Flag +import kotlinx.metadata.Flags +import kotlinx.metadata.KmClass +import kotlinx.metadata.KmConstructor +import kotlinx.metadata.KmFunction +import kotlinx.metadata.KmProperty +import kotlinx.metadata.KmType +import kotlinx.metadata.KmTypeParameter +import kotlinx.metadata.KmValueParameter + +// Common flags for any element with flags. +@KotlinPoetMetadataPreview +public val Flags.hasAnnotations: Boolean get() = Flag.HAS_ANNOTATIONS(this) + +@KotlinPoetMetadataPreview +public val Flags.isAbstract: Boolean get() = Flag.IS_ABSTRACT(this) + +@KotlinPoetMetadataPreview +public val Flags.isFinal: Boolean get() = Flag.IS_FINAL(this) + +@KotlinPoetMetadataPreview +public val Flags.isInternal: Boolean get() = Flag.IS_INTERNAL(this) + +@KotlinPoetMetadataPreview +public val Flags.isLocal: Boolean get() = Flag.IS_LOCAL(this) + +@KotlinPoetMetadataPreview +public val Flags.isOpen: Boolean get() = Flag.IS_OPEN(this) + +@KotlinPoetMetadataPreview +public val Flags.isPrivate: Boolean get() = Flag.IS_PRIVATE(this) + +@KotlinPoetMetadataPreview +public val Flags.isPrivate_to_this: Boolean get() = Flag.IS_PRIVATE_TO_THIS(this) + +@KotlinPoetMetadataPreview +public val Flags.isProtected: Boolean get() = Flag.IS_PROTECTED(this) + +@KotlinPoetMetadataPreview +public val Flags.isPublic: Boolean get() = Flag.IS_PUBLIC(this) + +@KotlinPoetMetadataPreview +public val Flags.isSealed: Boolean get() = Flag.IS_SEALED(this) + +// Type flags. +@KotlinPoetMetadataPreview +public val Flags.isNullableType: Boolean get() = Flag.Type.IS_NULLABLE(this) + +@KotlinPoetMetadataPreview +public val Flags.isSuspendType: Boolean get() = Flag.Type.IS_SUSPEND(this) + +// Class flags. +@KotlinPoetMetadataPreview +public val Flags.isAnnotationClass: Boolean get() = Flag.Class.IS_ANNOTATION_CLASS(this) + +@KotlinPoetMetadataPreview +public val Flags.isClass: Boolean get() = Flag.Class.IS_CLASS(this) + +@KotlinPoetMetadataPreview +public val Flags.isCompanionObjectClass: Boolean get() = Flag.Class.IS_COMPANION_OBJECT(this) + +@KotlinPoetMetadataPreview +public val Flags.isDataClass: Boolean get() = Flag.Class.IS_DATA(this) + +@KotlinPoetMetadataPreview +public val Flags.isEnumClass: Boolean get() = Flag.Class.IS_ENUM_CLASS(this) + +@KotlinPoetMetadataPreview +public val Flags.isEnumEntryClass: Boolean get() = Flag.Class.IS_ENUM_ENTRY(this) + +@KotlinPoetMetadataPreview +public val Flags.isExpectClass: Boolean get() = Flag.Class.IS_EXPECT(this) + +@KotlinPoetMetadataPreview +public val Flags.isExternalClass: Boolean get() = Flag.Class.IS_EXTERNAL(this) + +@KotlinPoetMetadataPreview +public val Flags.isValueClass: Boolean get() = Flag.Class.IS_VALUE(this) + +@KotlinPoetMetadataPreview +public val Flags.isInnerClass: Boolean get() = Flag.Class.IS_INNER(this) + +@KotlinPoetMetadataPreview +public val Flags.isObjectClass: Boolean get() = Flag.Class.IS_OBJECT(this) + +@KotlinPoetMetadataPreview +public val Flags.isInterface: Boolean get() = Flag.Class.IS_INTERFACE(this) + +@KotlinPoetMetadataPreview +public val Flags.isFun: Boolean get() = Flag.Class.IS_FUN(this) + +@KotlinPoetMetadataPreview +public val KmClass.isAnnotation: Boolean get() = flags.isAnnotationClass + +@KotlinPoetMetadataPreview +public val KmClass.isClass: Boolean get() = flags.isClass + +@KotlinPoetMetadataPreview +public val KmClass.isCompanionObject: Boolean get() = flags.isCompanionObjectClass + +@KotlinPoetMetadataPreview +public val KmClass.isData: Boolean get() = flags.isDataClass + +@KotlinPoetMetadataPreview +public val KmClass.isEnum: Boolean get() = flags.isEnumClass + +@KotlinPoetMetadataPreview +public val KmClass.isEnumEntry: Boolean get() = flags.isEnumEntryClass + +@KotlinPoetMetadataPreview +public val KmClass.isExpect: Boolean get() = flags.isExpectClass + +@KotlinPoetMetadataPreview +public val KmClass.isExternal: Boolean get() = flags.isExternalClass + +@KotlinPoetMetadataPreview +public val KmClass.isValue: Boolean get() = flags.isValueClass + +@KotlinPoetMetadataPreview +public val KmClass.isInner: Boolean get() = flags.isInnerClass + +@KotlinPoetMetadataPreview +public val KmClass.isObject: Boolean get() = flags.isObjectClass + +@KotlinPoetMetadataPreview +public val KmClass.isInterface: Boolean get() = flags.isInterface + +@KotlinPoetMetadataPreview +public val KmClass.isFun: Boolean get() = flags.isFun + +@KotlinPoetMetadataPreview +public val KmType.isSuspend: Boolean get() = flags.isSuspendType + +@KotlinPoetMetadataPreview +public val KmType.isNullable: Boolean get() = flags.isNullableType + +// Constructor flags. +@KotlinPoetMetadataPreview +public val Flags.isPrimaryConstructor: Boolean get() = !Flag.Constructor.IS_SECONDARY(this) + +@KotlinPoetMetadataPreview +public val KmConstructor.isPrimary: Boolean get() = flags.isPrimaryConstructor + +@KotlinPoetMetadataPreview +public val KmConstructor.isSecondary: Boolean get() = !isPrimary + +// Function flags. +@KotlinPoetMetadataPreview +public val Flags.isDeclarationFunction: Boolean get() = Flag.Function.IS_DECLARATION(this) + +@KotlinPoetMetadataPreview +public val Flags.isFakeOverrideFunction: Boolean get() = Flag.Function.IS_FAKE_OVERRIDE(this) + +@KotlinPoetMetadataPreview +public val Flags.isDelegationFunction: Boolean get() = Flag.Function.IS_DELEGATION(this) + +@KotlinPoetMetadataPreview +public val Flags.isSynthesizedFunction: Boolean get() = Flag.Function.IS_SYNTHESIZED(this) + +@KotlinPoetMetadataPreview +public val Flags.isOperatorFunction: Boolean get() = Flag.Function.IS_OPERATOR(this) + +@KotlinPoetMetadataPreview +public val Flags.isInfixFunction: Boolean get() = Flag.Function.IS_INFIX(this) + +@KotlinPoetMetadataPreview +public val Flags.isInlineFunction: Boolean get() = Flag.Function.IS_INLINE(this) + +@KotlinPoetMetadataPreview +public val Flags.isTailRecFunction: Boolean get() = Flag.Function.IS_TAILREC(this) + +@KotlinPoetMetadataPreview +public val Flags.isExternalFunction: Boolean get() = Flag.Function.IS_EXTERNAL(this) + +@KotlinPoetMetadataPreview +public val Flags.isSuspendFunction: Boolean get() = Flag.Function.IS_SUSPEND(this) + +@KotlinPoetMetadataPreview +public val Flags.isExpectFunction: Boolean get() = Flag.Function.IS_EXPECT(this) + +@KotlinPoetMetadataPreview +public val KmFunction.isDeclaration: Boolean get() = flags.isDeclarationFunction + +@KotlinPoetMetadataPreview +public val KmFunction.isFakeOverride: Boolean get() = flags.isFakeOverrideFunction + +@KotlinPoetMetadataPreview +public val KmFunction.isDelegation: Boolean get() = flags.isDelegationFunction + +@KotlinPoetMetadataPreview +public val KmFunction.isSynthesized: Boolean get() = flags.isSynthesizedFunction + +@KotlinPoetMetadataPreview +public val KmFunction.isOperator: Boolean get() = flags.isOperatorFunction + +@KotlinPoetMetadataPreview +public val KmFunction.isInfix: Boolean get() = flags.isInfixFunction + +@KotlinPoetMetadataPreview +public val KmFunction.isInline: Boolean get() = flags.isInlineFunction + +@KotlinPoetMetadataPreview +public val KmFunction.isTailRec: Boolean get() = flags.isTailRecFunction + +@KotlinPoetMetadataPreview +public val KmFunction.isExternal: Boolean get() = flags.isExternalFunction + +@KotlinPoetMetadataPreview +public val KmFunction.isSuspend: Boolean get() = flags.isSuspendFunction + +@KotlinPoetMetadataPreview +public val KmFunction.isExpect: Boolean get() = flags.isExpectFunction + +// Parameter flags. +@KotlinPoetMetadataPreview +public val KmValueParameter.declaresDefaultValue: Boolean get() = + Flag.ValueParameter.DECLARES_DEFAULT_VALUE(flags) + +@KotlinPoetMetadataPreview +public val KmValueParameter.isCrossInline: Boolean get() = Flag.ValueParameter.IS_CROSSINLINE(flags) + +@KotlinPoetMetadataPreview +public val KmValueParameter.isNoInline: Boolean get() = Flag.ValueParameter.IS_NOINLINE(flags) + +// Property flags. +@KotlinPoetMetadataPreview +public val Flags.isFakeOverrideProperty: Boolean get() = Flag.Property.IS_FAKE_OVERRIDE(this) + +@KotlinPoetMetadataPreview +public val KmProperty.hasConstant: Boolean get() = Flag.Property.HAS_CONSTANT(flags) + +@KotlinPoetMetadataPreview +public val KmProperty.hasGetter: Boolean get() = Flag.Property.HAS_GETTER(flags) + +@KotlinPoetMetadataPreview +public val KmProperty.hasSetter: Boolean get() = Flag.Property.HAS_SETTER(flags) + +@KotlinPoetMetadataPreview +public val KmProperty.isConst: Boolean get() = Flag.Property.IS_CONST(flags) + +@KotlinPoetMetadataPreview +public val KmProperty.isDeclaration: Boolean get() = Flag.Property.IS_DECLARATION(flags) + +@KotlinPoetMetadataPreview +public val KmProperty.isDelegated: Boolean get() = Flag.Property.IS_DELEGATED(flags) + +@KotlinPoetMetadataPreview +public val KmProperty.isDelegation: Boolean get() = Flag.Property.IS_DELEGATION(flags) + +@KotlinPoetMetadataPreview +public val KmProperty.isExpect: Boolean get() = Flag.Property.IS_EXPECT(flags) + +@KotlinPoetMetadataPreview +public val KmProperty.isExternal: Boolean get() = Flag.Property.IS_EXTERNAL(flags) + +@KotlinPoetMetadataPreview +public val KmProperty.isFakeOverride: Boolean get() = flags.isFakeOverrideProperty + +@KotlinPoetMetadataPreview +public val KmProperty.isLateinit: Boolean get() = Flag.Property.IS_LATEINIT(flags) + +@KotlinPoetMetadataPreview +public val KmProperty.isSynthesized: Boolean get() = Flag.Property.IS_SYNTHESIZED(flags) + +@KotlinPoetMetadataPreview +public val KmProperty.isVar: Boolean get() = Flag.Property.IS_VAR(flags) + +@KotlinPoetMetadataPreview +public val KmProperty.isVal: Boolean get() = !isVar + +// Property Accessor Flags +@KotlinPoetMetadataPreview +public val Flags.isPropertyAccessorExternal: Boolean + get() = Flag.PropertyAccessor.IS_EXTERNAL(this) + +@KotlinPoetMetadataPreview +public val Flags.isPropertyAccessorInline: Boolean + get() = Flag.PropertyAccessor.IS_INLINE(this) + +@KotlinPoetMetadataPreview +public val Flags.isPropertyAccessorNotDefault: Boolean + get() = Flag.PropertyAccessor.IS_NOT_DEFAULT(this) + +// TypeParameter flags. +@KotlinPoetMetadataPreview +public val KmTypeParameter.isReified: Boolean get() = Flag.TypeParameter.IS_REIFIED(flags) + +// Property Accessor Flags +public enum class PropertyAccessorFlag { + IS_EXTERNAL, + IS_INLINE, + IS_NOT_DEFAULT, +} + +@KotlinPoetMetadataPreview +public val KmProperty.setterPropertyAccessorFlags: Set<PropertyAccessorFlag> + get() = setterFlags.propertyAccessorFlags + +@KotlinPoetMetadataPreview +public val KmProperty.getterPropertyAccessorFlags: Set<PropertyAccessorFlag> + get() = getterFlags.propertyAccessorFlags + +@KotlinPoetMetadataPreview +public val Flags.propertyAccessorFlags: Set<PropertyAccessorFlag> + get() = setOf { + if (Flag.PropertyAccessor.IS_EXTERNAL(this@propertyAccessorFlags)) { + add(PropertyAccessorFlag.IS_EXTERNAL) + } + if (Flag.PropertyAccessor.IS_INLINE(this@propertyAccessorFlags)) { + add(PropertyAccessorFlag.IS_INLINE) + } + if (Flag.PropertyAccessor.IS_NOT_DEFAULT(this@propertyAccessorFlags)) { + add(PropertyAccessorFlag.IS_NOT_DEFAULT) + } + } + +internal inline fun <E> setOf(body: MutableSet<E>.() -> Unit): Set<E> { + return mutableSetOf<E>().apply(body).toSet() +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/KotlinPoetMetadata.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/KotlinPoetMetadata.kt new file mode 100644 index 00000000..6cb3c91d --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/KotlinPoetMetadata.kt @@ -0,0 +1,112 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:JvmName("KotlinPoetMetadata") +@file:Suppress("unused") + +package com.squareup.kotlinpoet.metadata + +import javax.lang.model.element.TypeElement +import kotlin.annotation.AnnotationTarget.CLASS +import kotlin.annotation.AnnotationTarget.FUNCTION +import kotlin.annotation.AnnotationTarget.PROPERTY +import kotlin.reflect.KClass +import kotlinx.metadata.KmClass +import kotlinx.metadata.jvm.KotlinClassHeader +import kotlinx.metadata.jvm.KotlinClassMetadata + +/** + * Indicates that a given API is part of the experimental KotlinPoet metadata support. This exists + * because kotlinx-metadata is not a stable API, and will remain in place until it is. + */ +@RequiresOptIn +@Retention(AnnotationRetention.BINARY) +@Target(CLASS, FUNCTION, PROPERTY) +public annotation class KotlinPoetMetadataPreview + +/** @return a new [KmClass] representation of the Kotlin metadata for [this] class. */ +@KotlinPoetMetadataPreview +public fun KClass<*>.toKmClass(): KmClass = java.toKmClass() + +/** @return a new [KmClass] representation of the Kotlin metadata for [this] class. */ +@KotlinPoetMetadataPreview +public fun Class<*>.toKmClass(): KmClass = readMetadata(::getAnnotation).toKmClass() + +/** @return a new [KmClass] representation of the Kotlin metadata for [this] type. */ +@KotlinPoetMetadataPreview +public fun TypeElement.toKmClass(): KmClass = readMetadata(::getAnnotation).toKmClass() + +@KotlinPoetMetadataPreview +public fun Metadata.toKmClass(): KmClass { + return toKotlinClassMetadata<KotlinClassMetadata.Class>() + .toKmClass() +} + +@KotlinPoetMetadataPreview +public inline fun <reified T : KotlinClassMetadata> Metadata.toKotlinClassMetadata(): T { + val expectedType = T::class + val metadata = readKotlinClassMetadata() + return when (expectedType) { + KotlinClassMetadata.Class::class -> { + check(metadata is KotlinClassMetadata.Class) + metadata as T + } + KotlinClassMetadata.FileFacade::class -> { + check(metadata is KotlinClassMetadata.FileFacade) + metadata as T + } + KotlinClassMetadata.SyntheticClass::class -> + throw UnsupportedOperationException("SyntheticClass isn't supported yet!") + KotlinClassMetadata.MultiFileClassFacade::class -> + throw UnsupportedOperationException("MultiFileClassFacade isn't supported yet!") + KotlinClassMetadata.MultiFileClassPart::class -> + throw UnsupportedOperationException("MultiFileClassPart isn't supported yet!") + KotlinClassMetadata.Unknown::class -> + throw RuntimeException("Recorded unknown metadata type! $metadata") + else -> TODO("Unrecognized KotlinClassMetadata type: $expectedType") + } +} + +/** + * Returns the [KotlinClassMetadata] this represents. In general you should only use this function + * when you don't know what the underlying [KotlinClassMetadata] subtype is, otherwise you should + * use one of the more direct functions like [toKmClass]. + */ +@KotlinPoetMetadataPreview +public fun Metadata.readKotlinClassMetadata(): KotlinClassMetadata { + val metadata = KotlinClassMetadata.read(asClassHeader()) + checkNotNull(metadata) { + "Could not parse metadata! Try bumping kotlinpoet and/or kotlinx-metadata version." + } + return metadata +} + +private inline fun readMetadata(lookup: ((Class<Metadata>) -> Metadata?)): Metadata { + return checkNotNull(lookup.invoke(Metadata::class.java)) { + "No Metadata annotation found! Must be Kotlin code built with the standard library on the classpath." + } +} + +private fun Metadata.asClassHeader(): KotlinClassHeader { + return KotlinClassHeader( + kind = kind, + metadataVersion = metadataVersion, + data1 = data1, + data2 = data2, + extraString = extraString, + packageName = packageName, + extraInt = extraInt, + ) +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/ClassInspectorUtil.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/ClassInspectorUtil.kt new file mode 100644 index 00000000..d9caa834 --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/ClassInspectorUtil.kt @@ -0,0 +1,241 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.metadata.classinspectors + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget +import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget.FIELD +import com.squareup.kotlinpoet.CHAR_SEQUENCE +import com.squareup.kotlinpoet.COLLECTION +import com.squareup.kotlinpoet.COMPARABLE +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.ITERABLE +import com.squareup.kotlinpoet.LIST +import com.squareup.kotlinpoet.MAP +import com.squareup.kotlinpoet.MAP_ENTRY +import com.squareup.kotlinpoet.MUTABLE_COLLECTION +import com.squareup.kotlinpoet.MUTABLE_ITERABLE +import com.squareup.kotlinpoet.MUTABLE_LIST +import com.squareup.kotlinpoet.MUTABLE_MAP +import com.squareup.kotlinpoet.MUTABLE_MAP_ENTRY +import com.squareup.kotlinpoet.MUTABLE_SET +import com.squareup.kotlinpoet.SET +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.asClassName +import com.squareup.kotlinpoet.joinToCode +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import com.squareup.kotlinpoet.metadata.isConst +import com.squareup.kotlinpoet.metadata.specs.ClassInspector +import java.util.Collections +import java.util.TreeSet +import kotlinx.metadata.KmProperty +import kotlinx.metadata.isLocal +import org.jetbrains.annotations.NotNull +import org.jetbrains.annotations.Nullable + +@KotlinPoetMetadataPreview +internal object ClassInspectorUtil { + val JVM_NAME: ClassName = JvmName::class.asClassName() + private val JVM_FIELD = JvmField::class.asClassName() + internal val JVM_FIELD_SPEC = AnnotationSpec.builder(JVM_FIELD).build() + internal val JVM_SYNTHETIC = JvmSynthetic::class.asClassName() + internal val JVM_SYNTHETIC_SPEC = AnnotationSpec.builder(JVM_SYNTHETIC).build() + internal val JAVA_DEPRECATED = java.lang.Deprecated::class.asClassName() + private val JVM_TRANSIENT = Transient::class.asClassName() + private val JVM_VOLATILE = Volatile::class.asClassName() + private val IMPLICIT_FIELD_ANNOTATIONS = setOf( + JVM_FIELD, + JVM_TRANSIENT, + JVM_VOLATILE, + ) + private val NOT_NULL = NotNull::class.asClassName() + private val NULLABLE = Nullable::class.asClassName() + private val EXTENSION_FUNCTION_TYPE = ExtensionFunctionType::class.asClassName() + private val KOTLIN_INTRINSIC_ANNOTATIONS = setOf( + NOT_NULL, + NULLABLE, + EXTENSION_FUNCTION_TYPE, + ) + + val KOTLIN_INTRINSIC_INTERFACES: Set<ClassName> = setOf( + CHAR_SEQUENCE, + COMPARABLE, + ITERABLE, + COLLECTION, + LIST, + SET, + MAP, + MAP_ENTRY, + MUTABLE_ITERABLE, + MUTABLE_COLLECTION, + MUTABLE_LIST, + MUTABLE_SET, + MUTABLE_MAP, + MUTABLE_MAP_ENTRY, + ) + + private val KOTLIN_NULLABILITY_ANNOTATIONS = setOf( + "org.jetbrains.annotations.NotNull", + "org.jetbrains.annotations.Nullable", + ) + + fun filterOutNullabilityAnnotations( + annotations: List<AnnotationSpec>, + ): List<AnnotationSpec> { + return annotations.filterNot { + val typeName = it.typeName + return@filterNot typeName is ClassName && + typeName.canonicalName in KOTLIN_NULLABILITY_ANNOTATIONS + } + } + + /** @return a [CodeBlock] representation of a [literal] value. */ + fun codeLiteralOf(literal: Any): CodeBlock { + return when (literal) { + is String -> CodeBlock.of("%S", literal) + is Long -> CodeBlock.of("%LL", literal) + is Float -> CodeBlock.of("%LF", literal) + else -> CodeBlock.of("%L", literal) + } + } + + /** + * Infers if [property] is a jvm field and should be annotated as such given the input + * parameters. + */ + fun computeIsJvmField( + property: KmProperty, + classInspector: ClassInspector, + isCompanionObject: Boolean, + hasGetter: Boolean, + hasSetter: Boolean, + hasField: Boolean, + ): Boolean { + return if (!hasGetter && + !hasSetter && + hasField && + !property.isConst + ) { + !(classInspector.supportsNonRuntimeRetainedAnnotations && !isCompanionObject) + } else { + false + } + } + + /** + * @return a new collection of [AnnotationSpecs][AnnotationSpec] with sorting and de-duping + * input annotations from [body]. + */ + fun createAnnotations( + siteTarget: UseSiteTarget? = null, + body: MutableCollection<AnnotationSpec>.() -> Unit, + ): Collection<AnnotationSpec> { + val result = mutableSetOf<AnnotationSpec>() + .apply(body) + .filterNot { spec -> + spec.typeName in KOTLIN_INTRINSIC_ANNOTATIONS + } + val withUseSiteTarget = if (siteTarget != null) { + result.map { + if (!(siteTarget == FIELD && it.typeName in IMPLICIT_FIELD_ANNOTATIONS)) { + // Some annotations are implicitly only for FIELD, so don't emit those site targets + it.toBuilder().useSiteTarget(siteTarget).build() + } else { + it + } + } + } else { + result + } + + val sorted = withUseSiteTarget.toTreeSet() + + return Collections.unmodifiableCollection(sorted) + } + + /** + * @return a [@Throws][Throws] [AnnotationSpec] representation of a given collection of + * [exceptions]. + */ + fun createThrowsSpec( + exceptions: Collection<TypeName>, + useSiteTarget: UseSiteTarget? = null, + ): AnnotationSpec { + return AnnotationSpec.builder(Throws::class) + .addMember( + "exceptionClasses = %L", + exceptions.map { CodeBlock.of("%T::class", it) } + .joinToCode(prefix = "[", suffix = "]"), + ) + .useSiteTarget(useSiteTarget) + .build() + } + + /** + * Best guesses a [ClassName] as represented in Metadata's [kotlinx.metadata.ClassName], where + * package names in this name are separated by '/' and class names are separated by '.'. + * + * For example: `"org/foo/bar/Baz.Nested"`. + * + * Local classes are prefixed with ".", but for KotlinPoetMetadataSpecs' use case we don't deal + * with those. + */ + fun createClassName(kotlinMetadataName: String): ClassName { + require(!kotlinMetadataName.isLocal) { + "Local/anonymous classes are not supported!" + } + // Top-level: package/of/class/MyClass + // Nested A: package/of/class/MyClass.NestedClass + val simpleName = kotlinMetadataName.substringAfterLast( + '/', // Drop the package name, e.g. "package/of/class/" + '.', // Drop any enclosing classes, e.g. "MyClass." + ) + val packageName = kotlinMetadataName.substringBeforeLast( + delimiter = "/", + missingDelimiterValue = "", + ) + val simpleNames = kotlinMetadataName.removeSuffix(simpleName) + .removeSuffix(".") // Trailing "." if any + .removePrefix(packageName) + .removePrefix("/") + .let { + if (it.isNotEmpty()) { + it.split(".") + } else { + // Don't split, otherwise we end up with an empty string as the first element! + emptyList() + } + } + .plus(simpleName) + + return ClassName( + packageName = packageName.replace("/", "."), + simpleNames = simpleNames, + ) + } + + fun Iterable<AnnotationSpec>.toTreeSet(): TreeSet<AnnotationSpec> { + return TreeSet<AnnotationSpec>(compareBy { it.toString() }).apply { + addAll(this@toTreeSet) + } + } + + private fun String.substringAfterLast(vararg delimiters: Char): String { + val index = lastIndexOfAny(delimiters) + return if (index == -1) this else substring(index + 1, length) + } +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/ElementsClassInspector.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/ElementsClassInspector.kt new file mode 100644 index 00000000..d4f77860 --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/ElementsClassInspector.kt @@ -0,0 +1,585 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.metadata.classinspectors + +import com.google.auto.common.MoreElements +import com.google.auto.common.MoreTypes +import com.google.auto.common.Visibility +import com.google.common.collect.LinkedHashMultimap +import com.google.common.collect.SetMultimap +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget.FILE +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.asClassName +import com.squareup.kotlinpoet.asTypeName +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil.JAVA_DEPRECATED +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil.JVM_NAME +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil.filterOutNullabilityAnnotations +import com.squareup.kotlinpoet.metadata.hasAnnotations +import com.squareup.kotlinpoet.metadata.hasConstant +import com.squareup.kotlinpoet.metadata.isAnnotation +import com.squareup.kotlinpoet.metadata.isCompanionObject +import com.squareup.kotlinpoet.metadata.isConst +import com.squareup.kotlinpoet.metadata.isDeclaration +import com.squareup.kotlinpoet.metadata.isSynthesized +import com.squareup.kotlinpoet.metadata.isValue +import com.squareup.kotlinpoet.metadata.readKotlinClassMetadata +import com.squareup.kotlinpoet.metadata.specs.ClassData +import com.squareup.kotlinpoet.metadata.specs.ClassInspector +import com.squareup.kotlinpoet.metadata.specs.ConstructorData +import com.squareup.kotlinpoet.metadata.specs.ContainerData +import com.squareup.kotlinpoet.metadata.specs.EnumEntryData +import com.squareup.kotlinpoet.metadata.specs.FieldData +import com.squareup.kotlinpoet.metadata.specs.FileData +import com.squareup.kotlinpoet.metadata.specs.JvmFieldModifier +import com.squareup.kotlinpoet.metadata.specs.JvmFieldModifier.TRANSIENT +import com.squareup.kotlinpoet.metadata.specs.JvmFieldModifier.VOLATILE +import com.squareup.kotlinpoet.metadata.specs.JvmMethodModifier +import com.squareup.kotlinpoet.metadata.specs.JvmMethodModifier.DEFAULT +import com.squareup.kotlinpoet.metadata.specs.JvmMethodModifier.STATIC +import com.squareup.kotlinpoet.metadata.specs.JvmMethodModifier.SYNCHRONIZED +import com.squareup.kotlinpoet.metadata.specs.KM_CONSTRUCTOR_COMPARATOR +import com.squareup.kotlinpoet.metadata.specs.KM_FUNCTION_COMPARATOR +import com.squareup.kotlinpoet.metadata.specs.KM_PROPERTY_COMPARATOR +import com.squareup.kotlinpoet.metadata.specs.MethodData +import com.squareup.kotlinpoet.metadata.specs.PropertyData +import com.squareup.kotlinpoet.metadata.toKmClass +import java.util.TreeMap +import java.util.concurrent.ConcurrentHashMap +import javax.lang.model.element.Element +import javax.lang.model.element.ElementKind.INTERFACE +import javax.lang.model.element.ExecutableElement +import javax.lang.model.element.PackageElement +import javax.lang.model.element.TypeElement +import javax.lang.model.element.VariableElement +import javax.lang.model.type.TypeKind +import javax.lang.model.util.ElementFilter +import javax.lang.model.util.Elements +import javax.lang.model.util.Types +import kotlin.LazyThreadSafetyMode.NONE +import kotlinx.metadata.KmClass +import kotlinx.metadata.KmDeclarationContainer +import kotlinx.metadata.KmPackage +import kotlinx.metadata.jvm.JvmFieldSignature +import kotlinx.metadata.jvm.JvmMethodSignature +import kotlinx.metadata.jvm.KotlinClassMetadata +import kotlinx.metadata.jvm.fieldSignature +import kotlinx.metadata.jvm.getterSignature +import kotlinx.metadata.jvm.setterSignature +import kotlinx.metadata.jvm.signature +import kotlinx.metadata.jvm.syntheticMethodForAnnotations + +private typealias ElementsModifier = javax.lang.model.element.Modifier + +/** + * An [Elements]-based implementation of [ClassInspector]. + */ +@KotlinPoetMetadataPreview +public class ElementsClassInspector private constructor( + private val elements: Elements, + private val types: Types, +) : ClassInspector { + private val typeElementCache = ConcurrentHashMap<ClassName, Optional<TypeElement>>() + private val methodCache = ConcurrentHashMap<Pair<TypeElement, String>, Optional<ExecutableElement>>() + private val variableElementCache = ConcurrentHashMap<Pair<TypeElement, String>, Optional<VariableElement>>() + private val jvmNameType = elements.getTypeElement(JVM_NAME.canonicalName) + private val jvmNameName = ElementFilter.methodsIn(jvmNameType.enclosedElements) + .first { it.simpleName.toString() == "name" } + + private fun lookupTypeElement(className: ClassName): TypeElement? { + return typeElementCache.getOrPut(className) { + elements.getTypeElement(className.canonicalName).toOptional() + }.nullableValue + } + + override val supportsNonRuntimeRetainedAnnotations: Boolean = true + + override fun declarationContainerFor(className: ClassName): KmDeclarationContainer { + val typeElement = lookupTypeElement(className) + ?: error("No type element found for: $className.") + + val metadata = typeElement.getAnnotation(Metadata::class.java) + return when (val kotlinClassMetadata = metadata.readKotlinClassMetadata()) { + is KotlinClassMetadata.Class -> kotlinClassMetadata.toKmClass() + is KotlinClassMetadata.FileFacade -> kotlinClassMetadata.toKmPackage() + else -> TODO("Not implemented yet: ${kotlinClassMetadata.javaClass.simpleName}") + } + } + + override fun isInterface(className: ClassName): Boolean { + if (className in ClassInspectorUtil.KOTLIN_INTRINSIC_INTERFACES) { + return true + } + return lookupTypeElement(className)?.kind == INTERFACE + } + + private fun TypeElement.lookupField(fieldSignature: JvmFieldSignature): VariableElement? { + val signatureString = fieldSignature.asString() + return variableElementCache.getOrPut(this to signatureString) { + ElementFilter.fieldsIn(enclosedElements) + .find { signatureString == it.jvmFieldSignature(types) }.toOptional() + }.nullableValue + } + + private fun lookupMethod( + className: ClassName, + methodSignature: JvmMethodSignature, + elementFilter: (Iterable<Element>) -> List<ExecutableElement>, + ): ExecutableElement? { + return lookupTypeElement(className)?.lookupMethod(methodSignature, elementFilter) + } + + private fun TypeElement.lookupMethod( + methodSignature: JvmMethodSignature, + elementFilter: (Iterable<Element>) -> List<ExecutableElement>, + ): ExecutableElement? { + val signatureString = methodSignature.asString() + return methodCache.getOrPut(this to signatureString) { + elementFilter(enclosedElements) + .find { signatureString == it.jvmMethodSignature(types) }.toOptional() + }.nullableValue + } + + private fun VariableElement.jvmModifiers(isJvmField: Boolean): Set<JvmFieldModifier> { + return modifiers.mapNotNullTo(mutableSetOf()) { + when { + it == ElementsModifier.TRANSIENT -> TRANSIENT + it == ElementsModifier.VOLATILE -> VOLATILE + !isJvmField && it == ElementsModifier.STATIC -> JvmFieldModifier.STATIC + else -> null + } + } + } + + private fun VariableElement.annotationSpecs(): List<AnnotationSpec> { + @Suppress("DEPRECATION") + return filterOutNullabilityAnnotations( + annotationMirrors.map { AnnotationSpec.get(it) }, + ) + } + + private fun ExecutableElement.jvmModifiers(): Set<JvmMethodModifier> { + return modifiers.mapNotNullTo(mutableSetOf()) { + when (it) { + ElementsModifier.SYNCHRONIZED -> SYNCHRONIZED + ElementsModifier.STATIC -> STATIC + ElementsModifier.DEFAULT -> DEFAULT + else -> null + } + } + } + + private fun ExecutableElement.annotationSpecs(): List<AnnotationSpec> { + @Suppress("DEPRECATION") + return filterOutNullabilityAnnotations( + annotationMirrors.map { AnnotationSpec.get(it) }, + ) + } + + private fun ExecutableElement.exceptionTypeNames(): List<TypeName> { + @Suppress("DEPRECATION") + return thrownTypes.map { it.asTypeName() } + } + + override fun enumEntry(enumClassName: ClassName, memberName: String): EnumEntryData { + val enumType = lookupTypeElement(enumClassName) + ?: error("No type element found for: $enumClassName.") + val enumTypeAsType = enumType.asType() + val member = typeElementCache.getOrPut(enumClassName.nestedClass(memberName)) { + ElementFilter.typesIn(enumType.enclosedElements) + .asSequence() + .filter { types.isSubtype(enumTypeAsType, it.superclass) } + .find { it.simpleName.contentEquals(memberName) }.toOptional() + }.nullableValue + val declarationContainer = member?.getAnnotation(Metadata::class.java)?.toKmClass() + + val entry = ElementFilter.fieldsIn(enumType.enclosedElements) + .find { it.simpleName.contentEquals(memberName) } + ?: error("Could not find the enum entry for: $enumClassName") + + return EnumEntryData( + declarationContainer = declarationContainer, + annotations = entry.annotationSpecs(), + ) + } + + private fun VariableElement.constantValue(): CodeBlock? { + return constantValue?.let(ClassInspectorUtil::codeLiteralOf) + } + + override fun methodExists(className: ClassName, methodSignature: JvmMethodSignature): Boolean { + return lookupMethod(className, methodSignature, ElementFilter::methodsIn) != null + } + + /** + * Detects whether [this] given method is overridden in [type]. + * + * Adapted and simplified from AutoCommon's private + * [MoreElements.getLocalAndInheritedMethods] methods implementations for detecting + * overrides. + */ + private fun ExecutableElement.isOverriddenIn(type: TypeElement): Boolean { + val methodMap = LinkedHashMultimap.create<String, ExecutableElement>() + type.getAllMethods(MoreElements.getPackage(type), methodMap) + // Find methods that are overridden using `Elements.overrides`. We reduce the performance + // impact by: + // (a) grouping methods by name, since a method cannot override another method with a + // different name. Since we know the target name, we just inspect the methods with + // that name. + // (b) making sure that methods in ancestor types precede those in descendant types, + // which means we only have to check a method against the ones that follow it in + // that order. Below, this means we just need to find the index of our target method + // and compare against only preceding ones. + val methodList = methodMap.asMap()[simpleName.toString()]?.toList() + ?: return false + val signature = jvmMethodSignature(types) + return methodList.asSequence() + .filter { it.jvmMethodSignature(types) == signature } + .take(1) + .any { elements.overrides(this, it, type) } + } + + /** + * Add to [methodsAccumulator] the instance methods from [this] that are visible to code in + * the package [pkg]. This means all the instance methods from [this] itself and all + * instance methods it inherits from its ancestors, except private methods and + * package-private methods in other packages. This method does not take overriding into + * account, so it will add both an ancestor method and a descendant method that overrides + * it. [methodsAccumulator] is a multimap from a method name to all of the methods with + * that name, including methods that override or overload one another. Within those + * methods, those in ancestor types always precede those in descendant types. + * + * Adapted from AutoCommon's private [MoreElements.getLocalAndInheritedMethods] methods' + * implementations, before overridden methods are stripped. + */ + private fun TypeElement.getAllMethods( + pkg: PackageElement, + methodsAccumulator: SetMultimap<String, ExecutableElement>, + ) { + for (superInterface in interfaces) { + MoreTypes.asTypeElement(superInterface).getAllMethods(pkg, methodsAccumulator) + } + if (superclass.kind != TypeKind.NONE) { + // Visit the superclass after superinterfaces so we will always see the implementation of a + // method after any interfaces that declared it. + MoreTypes.asTypeElement(superclass).getAllMethods(pkg, methodsAccumulator) + } + for (method in ElementFilter.methodsIn(enclosedElements)) { + if (ElementsModifier.STATIC !in method.modifiers && + ElementsModifier.FINAL !in method.modifiers && + ElementsModifier.PRIVATE !in method.modifiers && + method.isVisibleFrom(pkg) + ) { + methodsAccumulator.put(method.simpleName.toString(), method) + } + } + } + + private fun ExecutableElement.isVisibleFrom(pkg: PackageElement): Boolean { + // We use Visibility.ofElement rather than [MoreElements.effectiveVisibilityOfElement] + // because it doesn't really matter whether the containing class is visible. If you + // inherit a public method then you have a public method, regardless of whether you + // inherit it from a public class. + return when (Visibility.ofElement(this)) { + Visibility.PRIVATE -> false + Visibility.DEFAULT -> MoreElements.getPackage(this) == pkg + else -> true + } + } + + override fun containerData( + declarationContainer: KmDeclarationContainer, + className: ClassName, + parentClassName: ClassName?, + ): ContainerData { + val typeElement: TypeElement = lookupTypeElement(className) ?: error("No class found for: $className.") + val isCompanionObject = when (declarationContainer) { + is KmClass -> { + declarationContainer.isCompanionObject + } + is KmPackage -> { + false + } + else -> TODO("Not implemented yet: ${declarationContainer.javaClass.simpleName}") + } + + // Should only be called if parentName has been null-checked + val classIfCompanion by lazy(NONE) { + if (isCompanionObject && parentClassName != null) { + lookupTypeElement(parentClassName) + ?: error("No class found for: $parentClassName.") + } else { + typeElement + } + } + + val propertyData = declarationContainer.properties + .asSequence() + .filter { it.isDeclaration } + .filterNot { it.isSynthesized } + .associateWithTo(TreeMap(KM_PROPERTY_COMPARATOR)) { property -> + val isJvmField = ClassInspectorUtil.computeIsJvmField( + property = property, + classInspector = this, + isCompanionObject = isCompanionObject, + hasGetter = property.getterSignature != null, + hasSetter = property.setterSignature != null, + hasField = property.fieldSignature != null, + ) + + val fieldData = property.fieldSignature?.let fieldDataLet@{ fieldSignature -> + // Check the field in the parent first. For const/static/jvmField elements, these only + // exist in the parent and we want to check that if necessary to avoid looking up a + // non-existent field in the companion. + val parentModifiers = if (isCompanionObject && parentClassName != null) { + classIfCompanion.lookupField(fieldSignature)?.jvmModifiers(isJvmField).orEmpty() + } else { + emptySet() + } + + val isStatic = JvmFieldModifier.STATIC in parentModifiers + + // TODO we looked up field once, let's reuse it + val classForOriginalField = typeElement.takeUnless { + isCompanionObject && + (property.isConst || isJvmField || isStatic) + } ?: classIfCompanion + + val field = classForOriginalField.lookupField(fieldSignature) + ?: return@fieldDataLet FieldData.SYNTHETIC + val constant = if (property.hasConstant) { + val fieldWithConstant = classIfCompanion.takeIf { it != typeElement }?.let { + if (it.kind.isInterface) { + field + } else { + // const properties are relocated to the enclosing class + it.lookupField(fieldSignature) + ?: return@fieldDataLet FieldData.SYNTHETIC + } + } ?: field + fieldWithConstant.constantValue() + } else { + null + } + + val jvmModifiers = field.jvmModifiers(isJvmField) + parentModifiers + + FieldData( + annotations = field.annotationSpecs(), + isSynthetic = false, + jvmModifiers = jvmModifiers.filterNotTo(mutableSetOf()) { + // JvmField companion objects don't need JvmStatic, it's implicit + isCompanionObject && isJvmField && it == JvmFieldModifier.STATIC + }, + constant = constant, + ) + } + + val getterData = property.getterSignature?.let { getterSignature -> + val method = classIfCompanion.lookupMethod(getterSignature, ElementFilter::methodsIn) + method?.methodData( + typeElement = typeElement, + hasAnnotations = property.getterFlags.hasAnnotations, + jvmInformationMethod = classIfCompanion.takeIf { it != typeElement } + ?.lookupMethod(getterSignature, ElementFilter::methodsIn) + ?: method, + ) + ?: return@let MethodData.SYNTHETIC + } + + val setterData = property.setterSignature?.let { setterSignature -> + val method = classIfCompanion.lookupMethod(setterSignature, ElementFilter::methodsIn) + method?.methodData( + typeElement = typeElement, + hasAnnotations = property.setterFlags.hasAnnotations, + jvmInformationMethod = classIfCompanion.takeIf { it != typeElement } + ?.lookupMethod(setterSignature, ElementFilter::methodsIn) + ?: method, + knownIsOverride = getterData?.isOverride, + ) + ?: return@let MethodData.SYNTHETIC + } + + val annotations = mutableListOf<AnnotationSpec>() + if (property.flags.hasAnnotations) { + property.syntheticMethodForAnnotations?.let { annotationsHolderSignature -> + val method = typeElement.lookupMethod(annotationsHolderSignature, ElementFilter::methodsIn) + ?: return@let MethodData.SYNTHETIC + annotations += method.annotationSpecs() + // Cover for https://github.com/square/kotlinpoet/issues/1046 + .filterNot { it.typeName == JAVA_DEPRECATED } + } + } + + // If a field is static in a companion object, remove the modifier and add the annotation + // directly on the top level. Otherwise this will generate `@field:JvmStatic`, which is + // not legal + var finalFieldData = fieldData + fieldData?.jvmModifiers?.let { modifiers -> + if (isCompanionObject && JvmFieldModifier.STATIC in modifiers) { + finalFieldData = fieldData.copy( + jvmModifiers = fieldData.jvmModifiers + .filterNotTo(LinkedHashSet()) { it == JvmFieldModifier.STATIC }, + ) + annotations += AnnotationSpec.builder( + JVM_STATIC, + ).build() + } + } + + PropertyData( + annotations = annotations, + fieldData = finalFieldData, + getterData = getterData, + setterData = setterData, + isJvmField = isJvmField, + ) + } + + val methodData = declarationContainer.functions + .associateWithTo(TreeMap(KM_FUNCTION_COMPARATOR)) { kmFunction -> + val signature = kmFunction.signature + if (signature != null) { + val method = typeElement.lookupMethod(signature, ElementFilter::methodsIn) + method?.methodData( + typeElement = typeElement, + hasAnnotations = kmFunction.flags.hasAnnotations, + jvmInformationMethod = classIfCompanion.takeIf { it != typeElement } + ?.lookupMethod(signature, ElementFilter::methodsIn) + ?: method, + ) + ?: return@associateWithTo MethodData.SYNTHETIC + } else { + MethodData.EMPTY + } + } + + when (declarationContainer) { + is KmClass -> { + val constructorData = declarationContainer.constructors + .associateWithTo(TreeMap(KM_CONSTRUCTOR_COMPARATOR)) { kmConstructor -> + if (declarationContainer.isAnnotation || declarationContainer.isValue) { + // + // Annotations are interfaces in bytecode, but kotlin metadata will still report a + // constructor signature + // + // Inline classes have no constructors at runtime + // + return@associateWithTo ConstructorData.EMPTY + } + val signature = kmConstructor.signature + if (signature != null) { + val constructor = typeElement.lookupMethod(signature, ElementFilter::constructorsIn) + ?: return@associateWithTo ConstructorData.EMPTY + ConstructorData( + annotations = if (kmConstructor.flags.hasAnnotations) { + constructor.annotationSpecs() + } else { + emptyList() + }, + parameterAnnotations = constructor.parameters.indexedAnnotationSpecs(), + isSynthetic = false, + jvmModifiers = constructor.jvmModifiers(), + exceptions = constructor.exceptionTypeNames(), + ) + } else { + ConstructorData.EMPTY + } + } + return ClassData( + declarationContainer = declarationContainer, + className = className, + annotations = if (declarationContainer.flags.hasAnnotations) { + ClassInspectorUtil.createAnnotations { + @Suppress("DEPRECATION") + addAll(typeElement.annotationMirrors.map { AnnotationSpec.get(it) }) + } + } else { + emptyList() + }, + properties = propertyData, + constructors = constructorData, + methods = methodData, + ) + } + is KmPackage -> { + // There's no flag for checking if there are annotations, so we just eagerly check in this + // case. All annotations on this class are file: site targets in source. This includes + // @JvmName. + var jvmName: String? = null + val fileAnnotations = ClassInspectorUtil.createAnnotations(FILE) { + addAll( + typeElement.annotationMirrors.map { + if (it.annotationType == jvmNameType) { + val nameValue = requireNotNull(it.elementValues[jvmNameName]) { + "No name property found on $it" + } + jvmName = nameValue.value as String + } + @Suppress("DEPRECATION") + AnnotationSpec.get(it) + }, + ) + } + return FileData( + declarationContainer = declarationContainer, + annotations = fileAnnotations, + properties = propertyData, + methods = methodData, + className = className, + jvmName = jvmName, + ) + } + else -> TODO("Not implemented yet: ${declarationContainer.javaClass.simpleName}") + } + } + + private fun List<VariableElement>.indexedAnnotationSpecs(): Map<Int, Collection<AnnotationSpec>> { + return withIndex().associate { (index, parameter) -> + index to ClassInspectorUtil.createAnnotations { addAll(parameter.annotationSpecs()) } + } + } + + private fun ExecutableElement.methodData( + typeElement: TypeElement, + hasAnnotations: Boolean, + jvmInformationMethod: ExecutableElement = this, + knownIsOverride: Boolean? = null, + ): MethodData { + return MethodData( + annotations = if (hasAnnotations) annotationSpecs() else emptyList(), + parameterAnnotations = parameters.indexedAnnotationSpecs(), + isSynthetic = false, + jvmModifiers = jvmInformationMethod.jvmModifiers(), + isOverride = knownIsOverride ?: isOverriddenIn(typeElement), + exceptions = exceptionTypeNames(), + ) + } + + public companion object { + /** @return an [Elements]-based implementation of [ClassInspector]. */ + @JvmStatic + @KotlinPoetMetadataPreview + public fun create(elements: Elements, types: Types): ClassInspector { + return ElementsClassInspector(elements, types) + } + + private val JVM_STATIC = JvmStatic::class.asClassName() + } +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/JvmDescriptorUtils.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/JvmDescriptorUtils.kt new file mode 100644 index 00000000..235e5e87 --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/JvmDescriptorUtils.kt @@ -0,0 +1,194 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.metadata.classinspectors + +import javax.lang.model.element.Element +import javax.lang.model.element.ExecutableElement +import javax.lang.model.element.NestingKind +import javax.lang.model.element.QualifiedNameable +import javax.lang.model.element.TypeElement +import javax.lang.model.element.VariableElement +import javax.lang.model.type.ArrayType +import javax.lang.model.type.DeclaredType +import javax.lang.model.type.ErrorType +import javax.lang.model.type.ExecutableType +import javax.lang.model.type.NoType +import javax.lang.model.type.NullType +import javax.lang.model.type.PrimitiveType +import javax.lang.model.type.TypeKind.BOOLEAN +import javax.lang.model.type.TypeKind.BYTE +import javax.lang.model.type.TypeKind.CHAR +import javax.lang.model.type.TypeKind.DOUBLE +import javax.lang.model.type.TypeKind.FLOAT +import javax.lang.model.type.TypeKind.INT +import javax.lang.model.type.TypeKind.LONG +import javax.lang.model.type.TypeKind.SHORT +import javax.lang.model.type.TypeMirror +import javax.lang.model.type.TypeVariable +import javax.lang.model.type.WildcardType +import javax.lang.model.util.AbstractTypeVisitor6 +import javax.lang.model.util.Types +import kotlinx.metadata.jvm.JvmFieldSignature +import kotlinx.metadata.jvm.JvmMethodSignature + +/* + * Adapted from + * - https://github.com/Takhion/kotlin-metadata/blob/e6de126575ad6ca10b093129b7c30d000c9b0c37/lib/src/main/kotlin/me/eugeniomarletti/kotlin/metadata/jvm/JvmDescriptorUtils.kt + * - https://github.com/Takhion/kotlin-metadata/pull/13 + */ + +/** + * For reference, see the [JVM specification, section 4.2](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.2). + * + * @return the name of this [Element] in its "internal form". + */ +internal val Element.internalName: String + get() = when (this) { + is TypeElement -> { + when (nestingKind) { + NestingKind.TOP_LEVEL -> + qualifiedName.toString().replace('.', '/') + NestingKind.MEMBER -> + enclosingElement.internalName + "$" + simpleName + NestingKind.LOCAL, NestingKind.ANONYMOUS -> + error("Unsupported nesting $nestingKind") + null -> + error("Unsupported, nestingKind == null") + } + } + is QualifiedNameable -> qualifiedName.toString().replace('.', '/') + else -> simpleName.toString() + } + +/** + * @return the "field descriptor" of this type. + * @see [JvmDescriptorTypeVisitor] + */ +@Suppress("unused") +internal val NoType.descriptor: String + get() = "V" + +/** + * @return the "field descriptor" of this type. + * @see [JvmDescriptorTypeVisitor] + */ +internal val DeclaredType.descriptor: String + get() = "L" + asElement().internalName + ";" + +/** + * @return the "field descriptor" of this type. + * @see [JvmDescriptorTypeVisitor] + */ +internal val PrimitiveType.descriptor: String + get() = when (this.kind) { + BYTE -> "B" + CHAR -> "C" + DOUBLE -> "D" + FLOAT -> "F" + INT -> "I" + LONG -> "J" + SHORT -> "S" + BOOLEAN -> "Z" + else -> error("Unknown primitive type $this") + } + +/** + * @see [JvmDescriptorTypeVisitor] + */ +internal fun TypeMirror.descriptor(types: Types): String = + accept(JvmDescriptorTypeVisitor, types) + +/** + * @return the "field descriptor" of this type. + * @see [JvmDescriptorTypeVisitor] + */ +internal fun WildcardType.descriptor(types: Types): String = + types.erasure(this).descriptor(types) + +/** + * @return the "field descriptor" of this type. + * @see [JvmDescriptorTypeVisitor] + */ +internal fun TypeVariable.descriptor(types: Types): String = + types.erasure(this).descriptor(types) + +/** + * @return the "field descriptor" of this type. + * @see [JvmDescriptorTypeVisitor] + */ +internal fun ArrayType.descriptor(types: Types): String = + "[" + componentType.descriptor(types) + +/** + * @return the "method descriptor" of this type. + * @see [JvmDescriptorTypeVisitor] + */ +internal fun ExecutableType.descriptor(types: Types): String { + val parameterDescriptors = parameterTypes.joinToString(separator = "") { it.descriptor(types) } + val returnDescriptor = returnType.descriptor(types) + return "($parameterDescriptors)$returnDescriptor" +} + +/** + * Returns the JVM signature in the form "$Name$MethodDescriptor", for example: `equals(Ljava/lang/Object;)Z`. + * + * Useful for comparing with [JvmMethodSignature]. + * + * For reference, see the [JVM specification, section 4.3](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3). + */ +internal fun ExecutableElement.jvmMethodSignature(types: Types): String { + return "$simpleName${asType().descriptor(types)}" +} + +/** + * Returns the JVM signature in the form "$Name:$FieldDescriptor", for example: `"value:Ljava/lang/String;"`. + * + * Useful for comparing with [JvmFieldSignature]. + * + * For reference, see the [JVM specification, section 4.3](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3). + */ +internal fun VariableElement.jvmFieldSignature(types: Types): String { + return "$simpleName:${asType().descriptor(types)}" +} + +/** + * When applied over a type, it returns either: + * - a "field descriptor", for example: `Ljava/lang/Object;` + * - a "method descriptor", for example: `(Ljava/lang/Object;)Z` + * + * For reference, see the [JVM specification, section 4.3](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3). + */ +internal object JvmDescriptorTypeVisitor : AbstractTypeVisitor6<String, Types>() { + override fun visitNoType(t: NoType, types: Types): String = t.descriptor + override fun visitDeclared(t: DeclaredType, types: Types): String = t.descriptor + override fun visitPrimitive(t: PrimitiveType, types: Types): String = t.descriptor + + override fun visitArray(t: ArrayType, types: Types): String = t.descriptor(types) + override fun visitWildcard(t: WildcardType, types: Types): String = t.descriptor(types) + override fun visitExecutable(t: ExecutableType, types: Types): String = t.descriptor(types) + override fun visitTypeVariable(t: TypeVariable, types: Types): String = t.descriptor(types) + + override fun visitNull(t: NullType, types: Types): String = visitUnknown( + t, + types, + ) + override fun visitError(t: ErrorType, types: Types): String = visitUnknown( + t, + types, + ) + + override fun visitUnknown(t: TypeMirror, types: Types): String = error("Unsupported type $t") +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/Optional.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/Optional.kt new file mode 100644 index 00000000..41935e36 --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/Optional.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.metadata.classinspectors + +/** + * Simple `Optional` implementation for use in collections that don't allow `null` values. + */ +internal data class Optional<out T : Any>(val nullableValue: T?) + +internal fun <T : Any> T?.toOptional(): Optional<T> = Optional( + this, +) diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/ReflectiveClassInspector.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/ReflectiveClassInspector.kt new file mode 100644 index 00000000..fc31ba09 --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/classinspectors/ReflectiveClassInspector.kt @@ -0,0 +1,604 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.metadata.classinspectors + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget.FILE +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.asTypeName +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil.JAVA_DEPRECATED +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil.filterOutNullabilityAnnotations +import com.squareup.kotlinpoet.metadata.hasAnnotations +import com.squareup.kotlinpoet.metadata.hasConstant +import com.squareup.kotlinpoet.metadata.isAnnotation +import com.squareup.kotlinpoet.metadata.isCompanionObject +import com.squareup.kotlinpoet.metadata.isConst +import com.squareup.kotlinpoet.metadata.isDeclaration +import com.squareup.kotlinpoet.metadata.isSynthesized +import com.squareup.kotlinpoet.metadata.isValue +import com.squareup.kotlinpoet.metadata.readKotlinClassMetadata +import com.squareup.kotlinpoet.metadata.specs.ClassData +import com.squareup.kotlinpoet.metadata.specs.ClassInspector +import com.squareup.kotlinpoet.metadata.specs.ConstructorData +import com.squareup.kotlinpoet.metadata.specs.ContainerData +import com.squareup.kotlinpoet.metadata.specs.EnumEntryData +import com.squareup.kotlinpoet.metadata.specs.FieldData +import com.squareup.kotlinpoet.metadata.specs.FileData +import com.squareup.kotlinpoet.metadata.specs.JvmFieldModifier +import com.squareup.kotlinpoet.metadata.specs.JvmFieldModifier.TRANSIENT +import com.squareup.kotlinpoet.metadata.specs.JvmFieldModifier.VOLATILE +import com.squareup.kotlinpoet.metadata.specs.JvmMethodModifier +import com.squareup.kotlinpoet.metadata.specs.JvmMethodModifier.DEFAULT +import com.squareup.kotlinpoet.metadata.specs.JvmMethodModifier.STATIC +import com.squareup.kotlinpoet.metadata.specs.JvmMethodModifier.SYNCHRONIZED +import com.squareup.kotlinpoet.metadata.specs.KM_CONSTRUCTOR_COMPARATOR +import com.squareup.kotlinpoet.metadata.specs.KM_FUNCTION_COMPARATOR +import com.squareup.kotlinpoet.metadata.specs.KM_PROPERTY_COMPARATOR +import com.squareup.kotlinpoet.metadata.specs.MethodData +import com.squareup.kotlinpoet.metadata.specs.PropertyData +import com.squareup.kotlinpoet.metadata.toKmClass +import java.lang.reflect.Constructor +import java.lang.reflect.Field +import java.lang.reflect.Method +import java.lang.reflect.Modifier +import java.lang.reflect.Parameter +import java.util.TreeMap +import java.util.concurrent.ConcurrentHashMap +import kotlin.LazyThreadSafetyMode.NONE +import kotlinx.metadata.KmClass +import kotlinx.metadata.KmDeclarationContainer +import kotlinx.metadata.KmPackage +import kotlinx.metadata.jvm.JvmFieldSignature +import kotlinx.metadata.jvm.JvmMethodSignature +import kotlinx.metadata.jvm.KotlinClassMetadata +import kotlinx.metadata.jvm.fieldSignature +import kotlinx.metadata.jvm.getterSignature +import kotlinx.metadata.jvm.setterSignature +import kotlinx.metadata.jvm.signature +import kotlinx.metadata.jvm.syntheticMethodForAnnotations + +@KotlinPoetMetadataPreview +public class ReflectiveClassInspector private constructor( + private val classLoader: ClassLoader?, +) : ClassInspector { + + private val classCache = ConcurrentHashMap<ClassName, Optional<Class<*>>>() + private val methodCache = ConcurrentHashMap<Pair<Class<*>, String>, Optional<Method>>() + private val constructorCache = ConcurrentHashMap<Pair<Class<*>, String>, Optional<Constructor<*>>>() + private val fieldCache = ConcurrentHashMap<Pair<Class<*>, String>, Optional<Field>>() + private val enumCache = ConcurrentHashMap<Pair<Class<*>, String>, Optional<Enum<*>>>() + + private fun lookupClass(className: ClassName): Class<*>? { + return classCache.getOrPut(className) { + try { + if (classLoader == null) { + Class.forName(className.reflectionName()) + } else { + Class.forName(className.reflectionName(), true, classLoader) + } + } catch (e: ClassNotFoundException) { + null + }.toOptional() + }.nullableValue + } + + override val supportsNonRuntimeRetainedAnnotations: Boolean = false + + override fun declarationContainerFor(className: ClassName): KmDeclarationContainer { + val clazz = lookupClass(className) + ?: error("No type element found for: $className.") + + val metadata = clazz.getAnnotation(Metadata::class.java) + return when (val kotlinClassMetadata = metadata.readKotlinClassMetadata()) { + is KotlinClassMetadata.Class -> kotlinClassMetadata.toKmClass() + is KotlinClassMetadata.FileFacade -> kotlinClassMetadata.toKmPackage() + else -> TODO("Not implemented yet: ${kotlinClassMetadata.javaClass.simpleName}") + } + } + + override fun isInterface(className: ClassName): Boolean { + if (className in ClassInspectorUtil.KOTLIN_INTRINSIC_INTERFACES) { + return true + } + return lookupClass(className)?.isInterface ?: false + } + + private fun Class<*>.lookupField(fieldSignature: JvmFieldSignature): Field? { + return try { + val signatureString = fieldSignature.asString() + fieldCache.getOrPut(this to signatureString) { + declaredFields + .asSequence() + .onEach { it.isAccessible = true } + .find { signatureString == it.jvmFieldSignature }.toOptional() + }.nullableValue + } catch (e: ClassNotFoundException) { + null + } + } + + private fun Class<*>.lookupMethod( + methodSignature: JvmMethodSignature, + ): Method? { + val signatureString = methodSignature.asString() + return methodCache.getOrPut(this to signatureString) { + declaredMethods + .asSequence() + .onEach { it.isAccessible = true } + .find { signatureString == it.jvmMethodSignature }.toOptional() + }.nullableValue + } + + private fun Class<*>.lookupConstructor( + constructorSignature: JvmMethodSignature, + ): Constructor<*>? { + val signatureString = constructorSignature.asString() + return constructorCache.getOrPut(this to signatureString) { + declaredConstructors + .asSequence() + .onEach { it.isAccessible = true } + .find { signatureString == it.jvmMethodSignature }.toOptional() + }.nullableValue + } + + private fun Field.jvmModifiers(): Set<JvmFieldModifier> { + return mutableSetOf<JvmFieldModifier>().apply { + if (Modifier.isTransient(modifiers)) { + add(TRANSIENT) + } + if (Modifier.isVolatile(modifiers)) { + add(VOLATILE) + } + if (Modifier.isStatic(modifiers)) { + add(JvmFieldModifier.STATIC) + } + } + } + + private fun Field.annotationSpecs(): List<AnnotationSpec> { + return filterOutNullabilityAnnotations( + declaredAnnotations.orEmpty().map { AnnotationSpec.get(it, includeDefaultValues = true) }, + ) + } + + private fun Constructor<*>.annotationSpecs(): List<AnnotationSpec> { + return filterOutNullabilityAnnotations( + declaredAnnotations.orEmpty().map { AnnotationSpec.get(it, true) }, + ) + } + + private fun Method.jvmModifiers(): Set<JvmMethodModifier> { + return methodJvmModifiers(modifiers, isDefault) + } + + private fun Constructor<*>.jvmModifiers(): Set<JvmMethodModifier> { + return methodJvmModifiers(modifiers, false) + } + + private fun methodJvmModifiers(modifiers: Int, isDefault: Boolean): Set<JvmMethodModifier> { + val jvmMethodModifiers = mutableSetOf<JvmMethodModifier>() + if (Modifier.isSynchronized(modifiers)) { + jvmMethodModifiers += SYNCHRONIZED + } + if (Modifier.isStatic(modifiers)) { + jvmMethodModifiers += STATIC + } + if (isDefault) { + jvmMethodModifiers += DEFAULT + } + return jvmMethodModifiers + } + + private fun Method.annotationSpecs(): List<AnnotationSpec> { + return filterOutNullabilityAnnotations( + declaredAnnotations.orEmpty().map { AnnotationSpec.get(it, includeDefaultValues = true) }, + ) + } + + private fun Parameter.annotationSpecs(): List<AnnotationSpec> { + return filterOutNullabilityAnnotations( + declaredAnnotations.map { AnnotationSpec.get(it, includeDefaultValues = true) }, + ) + } + + private fun Method.exceptionTypeNames(): List<TypeName> { + return exceptionTypes.orEmpty().mapTo(mutableListOf()) { it.asTypeName() } + } + + private fun Constructor<*>.exceptionTypeNames(): List<TypeName> { + return exceptionTypes.orEmpty().mapTo(mutableListOf()) { it.asTypeName() } + } + + override fun enumEntry(enumClassName: ClassName, memberName: String): EnumEntryData { + val clazz = lookupClass(enumClassName) + ?: error("No class found for: $enumClassName.") + check(clazz.isEnum) { + "Class must be an enum but isn't: $clazz" + } + val enumEntry = enumCache.getOrPut(clazz to memberName) { + clazz.enumConstants + .asSequence() + .map { it as Enum<*> } + .find { it.name == memberName } + .toOptional() + }.nullableValue + checkNotNull(enumEntry) { + "Could not find $memberName on $enumClassName" + } + return EnumEntryData( + declarationContainer = if (enumEntry.javaClass == clazz) { + // For simple enums with no class bodies, the entry class will be the same as the original + // class. + null + } else { + enumEntry.javaClass.getAnnotation(Metadata::class.java)?.toKmClass() + }, + annotations = clazz.getField(enumEntry.name).annotationSpecs(), + ) + } + + private fun Field.constantValue(): CodeBlock? { + if (!Modifier.isStatic(modifiers)) { + return null + } + return get(null) // Constant means we can do a static get on it. + .let(ClassInspectorUtil::codeLiteralOf) + } + + private fun JvmMethodSignature.isOverriddenIn(clazz: Class<*>): Boolean { + val signatureString = asString() + val classPackage = clazz.`package`.name + val interfaceMethods = clazz.interfaces.asSequence() + .flatMap { it.methods.asSequence() } + val superClassMethods = clazz.superclass?.methods.orEmpty().asSequence() + return interfaceMethods.plus(superClassMethods) + .filterNot { Modifier.isFinal(it.modifiers) } + .filterNot { Modifier.isStatic(it.modifiers) } + .filterNot { Modifier.isPrivate(it.modifiers) } + .filter { + Modifier.isPublic(it.modifiers) || + Modifier.isProtected(it.modifiers) || + // Package private + it.declaringClass.`package`.name == classPackage + } + .map { it.jvmMethodSignature } + .any { it == signatureString } + } + + override fun methodExists(className: ClassName, methodSignature: JvmMethodSignature): Boolean { + return lookupClass(className)?.lookupMethod(methodSignature) != null + } + + override fun containerData( + declarationContainer: KmDeclarationContainer, + className: ClassName, + parentClassName: ClassName?, + ): ContainerData { + val targetClass = lookupClass(className) ?: error("No class found for: $className.") + val isCompanionObject: Boolean = when (declarationContainer) { + is KmClass -> { + declarationContainer.isCompanionObject + } + is KmPackage -> { + false + } + else -> TODO("Not implemented yet: ${declarationContainer.javaClass.simpleName}") + } + + // Should only be called if parentName has been null-checked + val classIfCompanion by lazy(NONE) { + if (isCompanionObject && parentClassName != null) { + lookupClass(parentClassName) + ?: error("No class found for: $parentClassName.") + } else { + targetClass + } + } + + val propertyData = declarationContainer.properties + .asSequence() + .filter { it.isDeclaration } + .filterNot { it.isSynthesized } + .associateWithTo(TreeMap(KM_PROPERTY_COMPARATOR)) { property -> + val isJvmField = ClassInspectorUtil.computeIsJvmField( + property = property, + classInspector = this, + isCompanionObject = isCompanionObject, + hasGetter = property.getterSignature != null, + hasSetter = property.setterSignature != null, + hasField = property.fieldSignature != null, + ) + + val fieldData = property.fieldSignature?.let { fieldSignature -> + // Check the field in the parent first. For const/static/jvmField elements, these only + // exist in the parent and we want to check that if necessary to avoid looking up a + // non-existent field in the companion. + val parentModifiers = if (isCompanionObject && parentClassName != null) { + classIfCompanion.lookupField(fieldSignature)?.jvmModifiers().orEmpty() + } else { + emptySet() + } + + val isStatic = JvmFieldModifier.STATIC in parentModifiers + + // TODO we looked up field once, let's reuse it + val classForOriginalField = targetClass.takeUnless { + isCompanionObject && + (property.isConst || isJvmField || isStatic) + } ?: classIfCompanion + + val field = classForOriginalField.lookupField(fieldSignature) + ?: error("No field $fieldSignature found in $classForOriginalField.") + val constant = if (property.hasConstant) { + val fieldWithConstant = classIfCompanion.takeIf { it != targetClass }?.let { + if (it.isInterface) { + field + } else { + // const properties are relocated to the enclosing class + it.lookupField(fieldSignature) + ?: error("No field $fieldSignature found in $it.") + } + } ?: field + fieldWithConstant.constantValue() + } else { + null + } + + val jvmModifiers = field.jvmModifiers() + parentModifiers + + // For static, const, or JvmField fields in a companion object, the companion + // object's field is marked as synthetic to hide it from Java, but in this case + // it's a false positive for this check in kotlin. + val isSynthetic = field.isSynthetic && + !( + isCompanionObject && + (property.isConst || isJvmField || JvmFieldModifier.STATIC in jvmModifiers) + ) + + FieldData( + annotations = field.annotationSpecs(), + isSynthetic = isSynthetic, + jvmModifiers = jvmModifiers.filterNotTo(mutableSetOf()) { + // JvmField companion objects don't need JvmStatic, it's implicit + isCompanionObject && isJvmField && it == JvmFieldModifier.STATIC + }, + constant = constant, + ) + } + + val getterData = property.getterSignature?.let { getterSignature -> + val method = classIfCompanion.lookupMethod(getterSignature) + method?.methodData( + clazz = targetClass, + signature = getterSignature, + hasAnnotations = property.getterFlags.hasAnnotations, + jvmInformationMethod = classIfCompanion.takeIf { it != targetClass } + ?.lookupMethod(getterSignature) ?: method, + ) + ?: error("No getter method $getterSignature found in $classIfCompanion.") + } + + val setterData = property.setterSignature?.let { setterSignature -> + val method = classIfCompanion.lookupMethod(setterSignature) + method?.methodData( + clazz = targetClass, + signature = setterSignature, + hasAnnotations = property.setterFlags.hasAnnotations, + jvmInformationMethod = classIfCompanion.takeIf { it != targetClass } + ?.lookupMethod(setterSignature) ?: method, + knownIsOverride = getterData?.isOverride, + ) + ?: error("No setter method $setterSignature found in $classIfCompanion.") + } + + val annotations = mutableListOf<AnnotationSpec>() + if (property.flags.hasAnnotations) { + property.syntheticMethodForAnnotations?.let { annotationsHolderSignature -> + targetClass.lookupMethod(annotationsHolderSignature)?.let { method -> + annotations += method.annotationSpecs() + // Cover for https://github.com/square/kotlinpoet/issues/1046 + .filterNot { it.typeName == JAVA_DEPRECATED } + } + } + } + + PropertyData( + annotations = annotations, + fieldData = fieldData, + getterData = getterData, + setterData = setterData, + isJvmField = isJvmField, + ) + } + + val methodData = declarationContainer.functions + .associateWithTo(TreeMap(KM_FUNCTION_COMPARATOR)) { kmFunction -> + val signature = kmFunction.signature + if (signature != null) { + val method = targetClass.lookupMethod(signature) + method?.methodData( + clazz = targetClass, + signature = signature, + hasAnnotations = kmFunction.flags.hasAnnotations, + jvmInformationMethod = classIfCompanion.takeIf { it != targetClass }?.lookupMethod(signature) + ?: method, + ) + ?: error("No method $signature found in $targetClass.") + } else { + MethodData.EMPTY + } + } + + when (declarationContainer) { + is KmClass -> { + val classAnnotations = if (declarationContainer.flags.hasAnnotations) { + ClassInspectorUtil.createAnnotations { + addAll(targetClass.annotations.map { AnnotationSpec.get(it, includeDefaultValues = true) }) + } + } else { + emptyList() + } + val constructorData = declarationContainer.constructors + .associateWithTo(TreeMap(KM_CONSTRUCTOR_COMPARATOR)) { kmConstructor -> + if (declarationContainer.isAnnotation || declarationContainer.isValue) { + // + // Annotations are interfaces in reflection, but kotlin metadata will still report a + // constructor signature + // + // Inline classes have no constructors at runtime + // + return@associateWithTo ConstructorData.EMPTY + } + val signature = kmConstructor.signature + if (signature != null) { + val constructor = targetClass.lookupConstructor(signature) + ?: error("No constructor $signature found in $targetClass.") + ConstructorData( + annotations = if (kmConstructor.flags.hasAnnotations) { + constructor.annotationSpecs() + } else { + emptyList() + }, + parameterAnnotations = constructor.parameters.indexedAnnotationSpecs(), + isSynthetic = constructor.isSynthetic, + jvmModifiers = constructor.jvmModifiers(), + exceptions = constructor.exceptionTypeNames(), + ) + } else { + ConstructorData.EMPTY + } + } + return ClassData( + declarationContainer = declarationContainer, + className = className, + annotations = classAnnotations, + properties = propertyData, + constructors = constructorData, + methods = methodData, + ) + } + is KmPackage -> { + // There's no flag for checking if there are annotations, so we just eagerly check in this + // case. All annotations on this class are file: site targets in source. This does not + // include @JvmName since it does not have RUNTIME retention. In practice this doesn't + // really matter, but it does mean we can't know for certain if the file should be called + // FooKt.kt or Foo.kt. + val fileAnnotations = ClassInspectorUtil.createAnnotations(FILE) { + addAll(targetClass.annotations.map { AnnotationSpec.get(it, includeDefaultValues = true) }) + } + return FileData( + declarationContainer = declarationContainer, + annotations = fileAnnotations, + properties = propertyData, + methods = methodData, + className = className, + ) + } + else -> TODO("Not implemented yet: ${declarationContainer.javaClass.simpleName}") + } + } + + private fun Array<Parameter>.indexedAnnotationSpecs(): Map<Int, Collection<AnnotationSpec>> { + return withIndex().associate { (index, parameter) -> + index to ClassInspectorUtil.createAnnotations { addAll(parameter.annotationSpecs()) } + } + } + + private fun Method.methodData( + clazz: Class<*>, + signature: JvmMethodSignature, + hasAnnotations: Boolean, + jvmInformationMethod: Method = this, + knownIsOverride: Boolean? = null, + ): MethodData { + return MethodData( + annotations = if (hasAnnotations) annotationSpecs() else emptyList(), + parameterAnnotations = parameters.indexedAnnotationSpecs(), + isSynthetic = isSynthetic, + jvmModifiers = jvmInformationMethod.jvmModifiers(), + isOverride = knownIsOverride ?: signature.isOverriddenIn(clazz), + exceptions = exceptionTypeNames(), + ) + } + + public companion object { + @JvmStatic + @KotlinPoetMetadataPreview + public fun create(classLoader: ClassLoader? = null): ClassInspector { + return ReflectiveClassInspector(classLoader) + } + + private val Class<*>.descriptor: String + get() { + return when { + isPrimitive -> when (kotlin) { + Byte::class -> "B" + Char::class -> "C" + Double::class -> "D" + Float::class -> "F" + Int::class -> "I" + Long::class -> "J" + Short::class -> "S" + Boolean::class -> "Z" + Void::class -> "V" + else -> throw RuntimeException("Unrecognized primitive $this") + } + isArray -> name.replace('.', '/') + else -> "L$name;".replace('.', '/') + } + } + + private val Method.descriptor: String + get() = parameterTypes.joinToString( + separator = "", + prefix = "(", + postfix = ")${returnType.descriptor}", + ) { it.descriptor } + + /** + * Returns the JVM signature in the form "$Name$MethodDescriptor", for example: `equals(Ljava/lang/Object;)Z`. + * + * Useful for comparing with [JvmMethodSignature]. + * + * For reference, see the [JVM specification, section 4.3](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3). + */ + private val Method.jvmMethodSignature: String get() = "$name$descriptor" + + private val Constructor<*>.descriptor: String + get() = parameterTypes.joinToString(separator = "", prefix = "(", postfix = ")V") { it.descriptor } + + /** + * Returns the JVM signature in the form "<init>$MethodDescriptor", for example: `"<init>(Ljava/lang/Object;)V")`. + * + * Useful for comparing with [JvmMethodSignature]. + * + * For reference, see the [JVM specification, section 4.3](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3). + */ + private val Constructor<*>.jvmMethodSignature: String get() = "<init>$descriptor" + + /** + * Returns the JVM signature in the form "$Name:$FieldDescriptor", for example: `"value:Ljava/lang/String;"`. + * + * Useful for comparing with [JvmFieldSignature]. + * + * For reference, see the [JVM specification, section 4.3](https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.3). + */ + private val Field.jvmFieldSignature: String get() = "$name:${type.descriptor}" + } +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/ClassInspector.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/ClassInspector.kt new file mode 100644 index 00000000..14c019fd --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/ClassInspector.kt @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.metadata.specs + +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import kotlinx.metadata.KmClass +import kotlinx.metadata.KmDeclarationContainer +import kotlinx.metadata.jvm.JvmMethodSignature + +/** A basic interface for looking up JVM information about a given Class. */ +@KotlinPoetMetadataPreview +public interface ClassInspector { + + /** + * Indicates if this [ClassInspector] supports [AnnotationRetention.RUNTIME]-retained annotations. + * This is used to indicate if manual inference of certain non-RUNTIME-retained annotations should + * be done, such as [JvmName]. + */ + public val supportsNonRuntimeRetainedAnnotations: Boolean + + /** + * Creates a new [ContainerData] instance for a given [declarationContainer]. + * + * @param declarationContainer the source [KmDeclarationContainer] to read from. + * @param className the [ClassName] of the target class to to read from. + * @param parentClassName the parent [ClassName] name if [declarationContainer] is nested, inner, + * or is a companion object. + */ + public fun containerData( + declarationContainer: KmDeclarationContainer, + className: ClassName, + parentClassName: ClassName?, + ): ContainerData + + /** + * Looks up other declaration containers, such as for nested members. Note that this class would + * always be Kotlin, so Metadata can be relied on for this. + * + * @param className The [ClassName] representation of the class. + * @return the read [KmDeclarationContainer] from its metadata. If no class or facade + * file was found, this should throw an exception. + */ + public fun declarationContainerFor(className: ClassName): KmDeclarationContainer + + /** + * Looks up a class and returns whether or not it is an interface. Note that this class can be + * Java or Kotlin, so Metadata should not be relied on for this. + * + * @param className The [ClassName] representation of the class. + * @return whether or not it is an interface. + */ + public fun isInterface(className: ClassName): Boolean + + /** + * Looks up the enum entry on a given enum given its member name. + * + * @param enumClassName The [ClassName] representation of the enum class. + * @param memberName The simple member name. + * @return the [EnumEntryData] + */ + public fun enumEntry(enumClassName: ClassName, memberName: String): EnumEntryData + + /** + * Looks up if a given [methodSignature] within [className] exists. + * + * @param className The [ClassName] representation of the class. + * @param methodSignature The method signature to check. + * @return whether or not the method exists. + */ + public fun methodExists(className: ClassName, methodSignature: JvmMethodSignature): Boolean +} + +/** + * Creates a new [ContainerData] instance for a given [className]. + * + * @param className the [ClassName] of the target class to to read from. + * @param parentClassName the parent [ClassName] name if [className] is nested, inner, or is a + * companion object. + */ +@KotlinPoetMetadataPreview +public fun ClassInspector.containerData( + className: ClassName, + parentClassName: ClassName?, +): ContainerData { + return containerData(declarationContainerFor(className), className, parentClassName) +} + +/** + * Looks up other classes, such as for nested members. Note that this class would always be + * Kotlin, so Metadata can be relied on for this. + * + * @param className The [ClassName] representation of the class. + * @return the read [KmClass] from its metadata. If no class was found, this should throw + * an exception. + */ +@KotlinPoetMetadataPreview +public fun ClassInspector.classFor(className: ClassName): KmClass { + val container = declarationContainerFor(className) + check(container is KmClass) { + "Container is not a class! Was ${container.javaClass.simpleName}" + } + return container +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/ConstructorData.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/ConstructorData.kt new file mode 100644 index 00000000..01c98390 --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/ConstructorData.kt @@ -0,0 +1,67 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.metadata.specs + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil + +/** + * Represents relevant information on a constructor used for [ClassInspector]. Should only be + * associated with constructors of a [ClassData]. + * + * @param annotations declared annotations on this constructor. + * @property parameterAnnotations a mapping of parameter indices to annotations on them. + * @property isSynthetic indicates if this constructor is synthetic or not. + * @property jvmModifiers set of [JvmMethodModifiers][JvmMethodModifier] on this constructor. + * @property exceptions list of exceptions thrown by this constructor. + */ +@KotlinPoetMetadataPreview +public data class ConstructorData( + private val annotations: List<AnnotationSpec>, + val parameterAnnotations: Map<Int, Collection<AnnotationSpec>>, + val isSynthetic: Boolean, + val jvmModifiers: Set<JvmMethodModifier>, + val exceptions: List<TypeName>, +) { + + /** + * A collection of all annotations on this constructor, including any derived from [jvmModifiers], + * [isSynthetic], and [exceptions]. + */ + val allAnnotations: Collection<AnnotationSpec> = ClassInspectorUtil.createAnnotations { + addAll(annotations) + if (isSynthetic) { + add(ClassInspectorUtil.JVM_SYNTHETIC_SPEC) + } + addAll(jvmModifiers.mapNotNull { it.annotationSpec() }) + exceptions.takeIf { it.isNotEmpty() } + ?.let { + add(ClassInspectorUtil.createThrowsSpec(it)) + } + } + + public companion object { + public val EMPTY: ConstructorData = ConstructorData( + annotations = emptyList(), + parameterAnnotations = emptyMap(), + isSynthetic = false, + jvmModifiers = emptySet(), + exceptions = emptyList(), + ) + } +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/ContainerData.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/ContainerData.kt new file mode 100644 index 00000000..f1006d01 --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/ContainerData.kt @@ -0,0 +1,104 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.metadata.specs + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import kotlinx.metadata.KmClass +import kotlinx.metadata.KmConstructor +import kotlinx.metadata.KmDeclarationContainer +import kotlinx.metadata.KmFunction +import kotlinx.metadata.KmPackage +import kotlinx.metadata.KmProperty + +/** + * Represents relevant information on a declaration container used for [ClassInspector]. Can only + * ever be applied on a Kotlin type (i.e. is annotated with [Metadata]). + * + * @property declarationContainer the [KmDeclarationContainer] as parsed from the class's + * [@Metadata][Metadata] annotation. + * @property annotations declared annotations on this class. + * @property properties the mapping of [declarationContainer]'s properties to parsed [PropertyData]. + * @property methods the mapping of [declarationContainer]'s methods to parsed [MethodData]. + */ +@KotlinPoetMetadataPreview +public interface ContainerData { + public val declarationContainer: KmDeclarationContainer + public val annotations: Collection<AnnotationSpec> + public val properties: Map<KmProperty, PropertyData> + public val methods: Map<KmFunction, MethodData> +} + +/** + * Represents relevant information on a Kotlin class used for [ClassInspector]. Can only ever be + * applied on a class and not file facades. + * + * @property declarationContainer the [KmClass] as parsed from the class's + * [@Metadata][Metadata] annotation. + * @property className the KotlinPoet [ClassName] of the class. + * @property constructors the mapping of [declarationContainer]'s constructors to parsed + * [ConstructorData]. + */ +@KotlinPoetMetadataPreview +public data class ClassData( + override val declarationContainer: KmClass, + val className: ClassName, + override val annotations: Collection<AnnotationSpec>, + override val properties: Map<KmProperty, PropertyData>, + val constructors: Map<KmConstructor, ConstructorData>, + override val methods: Map<KmFunction, MethodData>, +) : ContainerData + +/** + * Represents relevant information on a file facade used for [ClassInspector]. + * + * @property declarationContainer the [KmClass] as parsed from the class's + * [@Metadata][Metadata] annotation. + * @property className the KotlinPoet [ClassName] of the underlying facade class in JVM. + * @property jvmName the `@JvmName` of the class or null if it does not have a custom name. + * Default will try to infer from the [className]. + */ +@KotlinPoetMetadataPreview +public data class FileData( + override val declarationContainer: KmPackage, + override val annotations: Collection<AnnotationSpec>, + override val properties: Map<KmProperty, PropertyData>, + override val methods: Map<KmFunction, MethodData>, + val className: ClassName, + val jvmName: String? = + if (!className.simpleName.endsWith("Kt")) className.simpleName else null, +) : ContainerData { + + /** + * The file name of the container, defaults to [className]'s simple name + "Kt". If a [jvmName] is + * specified, it will always defer to that. + */ + val fileName: String = jvmName ?: className.simpleName.removeSuffix("Kt") +} + +/** + * Represents relevant information on a Kotlin enum entry. + * + * @property declarationContainer the [KmClass] as parsed from the entry's + * [@Metadata][Metadata] annotation. + * @property annotations the annotations for the entry + */ +@KotlinPoetMetadataPreview +public data class EnumEntryData( + val declarationContainer: KmClass?, + val annotations: Collection<AnnotationSpec>, +) diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/FieldData.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/FieldData.kt new file mode 100644 index 00000000..a000ddef --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/FieldData.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.metadata.specs + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget.FIELD +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil + +/** + * Represents relevant information on a field used for [ClassInspector]. Should only be + * associated with a [PropertyData]. + * + * @param annotations declared annotations on this field. + * @property isSynthetic indicates if this field is synthetic or not. + * @property jvmModifiers set of [JvmMethodModifiers][JvmMethodModifier] on this field. + * @property constant the constant value of this field, if available. Note that this is does not + * strictly imply that the associated property is `const`. + */ +@KotlinPoetMetadataPreview +public data class FieldData( + private val annotations: List<AnnotationSpec>, + val isSynthetic: Boolean, + val jvmModifiers: Set<JvmFieldModifier>, + val constant: CodeBlock?, +) { + + /** + * A collection of all annotations on this method, including any derived from [jvmModifiers] + * and [isSynthetic]. + */ + val allAnnotations: Collection<AnnotationSpec> = ClassInspectorUtil.createAnnotations( + FIELD, + ) { + addAll(annotations) + if (isSynthetic) { + add(ClassInspectorUtil.JVM_SYNTHETIC_SPEC) + } + addAll(jvmModifiers.mapNotNull(JvmFieldModifier::annotationSpec)) + } + + public companion object { + public val SYNTHETIC: FieldData = FieldData( + annotations = emptyList(), + isSynthetic = true, + jvmModifiers = emptySet(), + constant = null, + ) + } +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/JvmFieldModifier.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/JvmFieldModifier.kt new file mode 100644 index 00000000..dc2a55bb --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/JvmFieldModifier.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.metadata.specs + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.asClassName +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview + +/** Modifiers that are annotations in Kotlin but modifier keywords in bytecode. */ +@KotlinPoetMetadataPreview +public enum class JvmFieldModifier : JvmModifier { + STATIC { + override fun annotationSpec(): AnnotationSpec = AnnotationSpec.builder( + JvmStatic::class.asClassName(), + ).build() + }, + TRANSIENT { + override fun annotationSpec(): AnnotationSpec = AnnotationSpec.builder( + Transient::class.asClassName(), + ).build() + }, + VOLATILE { + override fun annotationSpec(): AnnotationSpec = AnnotationSpec.builder( + Volatile::class.asClassName(), + ).build() + }, +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/JvmMethodModifier.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/JvmMethodModifier.kt new file mode 100644 index 00000000..5d63738b --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/JvmMethodModifier.kt @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.metadata.specs + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.asClassName +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview + +/** Modifiers that are annotations or implicit in Kotlin but modifier keywords in bytecode. */ +@KotlinPoetMetadataPreview +public enum class JvmMethodModifier : JvmModifier { + STATIC { + override fun annotationSpec(): AnnotationSpec = AnnotationSpec.builder( + JvmStatic::class.asClassName(), + ).build() + }, + SYNCHRONIZED { + override fun annotationSpec(): AnnotationSpec = AnnotationSpec.builder( + Synchronized::class.asClassName(), + ).build() + }, + DEFAULT, +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/JvmModifier.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/JvmModifier.kt new file mode 100644 index 00000000..f09e6add --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/JvmModifier.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.metadata.specs + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview + +/** + * Represents a JVM modifier that is represented as an annotation in Kotlin but as a modifier in + * bytecode. Examples include annotations such as [@JvmStatic][JvmStatic] or + * [@JvmSynthetic][JvmSynthetic]. + * + * This API is considered read-only and should not be implemented outside of KotlinPoet. + */ +@KotlinPoetMetadataPreview +public interface JvmModifier { + public fun annotationSpec(): AnnotationSpec? { + return null + } +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/KmTypes.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/KmTypes.kt new file mode 100644 index 00000000..cd12479c --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/KmTypes.kt @@ -0,0 +1,294 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.metadata.specs + +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.LambdaTypeName +import com.squareup.kotlinpoet.ParameterizedTypeName +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.STAR +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.TypeVariableName +import com.squareup.kotlinpoet.WildcardTypeName +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil +import com.squareup.kotlinpoet.metadata.isNullable +import com.squareup.kotlinpoet.metadata.isPrimary +import com.squareup.kotlinpoet.metadata.isReified +import com.squareup.kotlinpoet.metadata.isSuspend +import com.squareup.kotlinpoet.tags.TypeAliasTag +import kotlinx.metadata.KmClass +import kotlinx.metadata.KmClassifier +import kotlinx.metadata.KmClassifier.Class +import kotlinx.metadata.KmClassifier.TypeAlias +import kotlinx.metadata.KmClassifier.TypeParameter +import kotlinx.metadata.KmConstructor +import kotlinx.metadata.KmFlexibleTypeUpperBound +import kotlinx.metadata.KmFunction +import kotlinx.metadata.KmProperty +import kotlinx.metadata.KmType +import kotlinx.metadata.KmTypeParameter +import kotlinx.metadata.KmTypeProjection +import kotlinx.metadata.KmVariance +import kotlinx.metadata.KmVariance.IN +import kotlinx.metadata.KmVariance.INVARIANT +import kotlinx.metadata.KmVariance.OUT +import kotlinx.metadata.jvm.annotations +import kotlinx.metadata.jvm.signature + +/** + * `true` if this is an extension type (i.e. String.() -> Unit vs (String) -> Unit). + * + * See details: https://discuss.kotlinlang.org/t/announcing-kotlinx-metadata-jvm-library-for-reading-modifying-metadata-of-kotlin-jvm-class-files/7980/27 + */ +public val KmType.isExtensionType: Boolean get() { + return annotations.any { it.className == "kotlin/ExtensionFunctionType" } +} + +@KotlinPoetMetadataPreview +internal val KmClass.primaryConstructor: KmConstructor? + get() = constructors.find { it.isPrimary } + +internal fun KmVariance.toKModifier(): KModifier? { + return when (this) { + IN -> KModifier.IN + OUT -> KModifier.OUT + INVARIANT -> null + } +} + +@KotlinPoetMetadataPreview +internal fun KmTypeProjection.toTypeName( + typeParamResolver: TypeParameterResolver, +): TypeName { + val typename = type?.toTypeName(typeParamResolver) ?: STAR + return when (variance) { + IN -> WildcardTypeName.consumerOf(typename) + OUT -> WildcardTypeName.producerOf(typename) + INVARIANT -> typename + null -> STAR + } +} + +/** + * Converts a given [KmType] into a KotlinPoet representation, attempting to give a correct + * "source" representation. This includes converting [functions][kotlin.Function] and `suspend` + * types to appropriate [lambda representations][LambdaTypeName]. + */ +@KotlinPoetMetadataPreview +internal fun KmType.toTypeName( + typeParamResolver: TypeParameterResolver, +): TypeName { + val argumentList = arguments.map { it.toTypeName(typeParamResolver) } + val type: TypeName = when (val valClassifier = classifier) { + is TypeParameter -> { + typeParamResolver[valClassifier.id] + } + is KmClassifier.Class -> { + flexibleTypeUpperBound?.toTypeName(typeParamResolver)?.let { return it } + outerType?.toTypeName(typeParamResolver)?.let { return it } + var finalType: TypeName = ClassInspectorUtil.createClassName(valClassifier.name) + if (argumentList.isNotEmpty()) { + val finalTypeString = finalType.toString() + if (finalTypeString.startsWith("kotlin.Function")) { + // It's a lambda type! + finalType = if (finalTypeString == "kotlin.FunctionN") { + TODO("unclear how to express this one since it has arity") + } else { + val (parameters, returnType) = if (isSuspend) { + // Coroutines always adds an `Any?` return type, but we kind of just want the + // source representation, so we trick it here and ignore the last. + argumentList.dropLast(2).toTypedArray() to argumentList.dropLast(1).last().let { + // Coroutines makes these a `Continuation<T>` of the type, so we want the parameterized type + check(it is ParameterizedTypeName) + it.typeArguments[0] + } + } else { + argumentList.dropLast(1).toTypedArray() to argumentList.last() + } + val lambdaType = if (isExtensionType) { + // Extension function type! T.(). First parameter is actually the receiver. + LambdaTypeName.get( + receiver = parameters[0], + parameters = parameters.drop(1).toTypedArray(), + returnType = returnType, + ) + } else { + LambdaTypeName.get( + receiver = null, + parameters = parameters, + returnType = returnType, + ) + } + lambdaType.copy(suspending = isSuspend) + } + } else { + finalType = (finalType as ClassName).parameterizedBy(argumentList) + } + } + finalType + } + is TypeAlias -> { + ClassInspectorUtil.createClassName(valClassifier.name) + } + } + + val annotations = ClassInspectorUtil.createAnnotations { + for (annotation in annotations) { + add(annotation.toAnnotationSpec()) + } + }.toList() + val finalType = type.copy(nullable = isNullable, annotations = annotations) + return abbreviatedType?.let { + // This is actually an alias! The "abbreviated type" is the alias and how it's actually + // represented in source. So instead - we'll return the abbreviated type but store the "real" + // type in tags for reference. + val abbreviatedTypeName = it.toTypeName(typeParamResolver) + abbreviatedTypeName.copy( + tags = mapOf(TypeAliasTag::class to TypeAliasTag(finalType)), + ) + } ?: finalType +} + +@KotlinPoetMetadataPreview +internal fun KmTypeParameter.toTypeVariableName( + typeParamResolver: TypeParameterResolver, +): TypeVariableName { + val finalVariance = variance.toKModifier() + val typeVariableName = TypeVariableName( + name = name, + bounds = upperBounds.map { it.toTypeName(typeParamResolver) }, + variance = finalVariance, + ) + val annotations = ClassInspectorUtil.createAnnotations { + for (annotation in annotations) { + add(annotation.toAnnotationSpec()) + } + }.toList() + return typeVariableName.copy( + reified = isReified, + tags = mapOf(KmTypeParameter::class to this), + annotations = annotations, + ) +} + +@KotlinPoetMetadataPreview +private fun KmFlexibleTypeUpperBound.toTypeName( + typeParamResolver: TypeParameterResolver, +): TypeName { + // TODO tag typeFlexibilityId somehow? + return WildcardTypeName.producerOf(type.toTypeName(typeParamResolver)) +} + +internal interface TypeParameterResolver { + val parametersMap: Map<Int, TypeVariableName> + operator fun get(index: Int): TypeVariableName + + companion object { + val EMPTY = object : TypeParameterResolver { + override val parametersMap: Map<Int, TypeVariableName> = emptyMap() + + override fun get(index: Int): TypeVariableName = throw NoSuchElementException("No type parameters!") + } + } +} + +@KotlinPoetMetadataPreview +internal fun List<KmTypeParameter>.toTypeParameterResolver( + fallback: TypeParameterResolver? = null, +): TypeParameterResolver { + val parametersMap = LinkedHashMap<Int, TypeVariableName>() + val typeParamResolver = { id: Int -> + parametersMap[id] + ?: fallback?.get(id) + ?: throw IllegalStateException("No type argument found for $id!") + } + + val resolver = object : TypeParameterResolver { + override val parametersMap: Map<Int, TypeVariableName> = parametersMap + + override operator fun get(index: Int): TypeVariableName = typeParamResolver(index) + } + + // Fill the parametersMap. Need to do sequentially and allow for referencing previously defined params + for (typeParam in this) { + // Put the simple typevar in first, then it can be referenced in the full toTypeVariable() + // replacement later that may add bounds referencing this. + parametersMap[typeParam.id] = TypeVariableName(typeParam.name) + } + + for (typeParam in this) { + // Now replace it with the full version. + parametersMap[typeParam.id] = typeParam.toTypeVariableName(resolver) + } + + return resolver +} + +internal val KM_PROPERTY_COMPARATOR = Comparator<KmProperty> { o1, o2 -> + // No need to check fields, getters, etc as properties must have distinct names + o1.name.compareTo(o2.name) +} + +internal val KM_FUNCTION_COMPARATOR = Comparator<KmFunction> { o1, o2 -> + var result = o1.name.compareTo(o2.name) + if (result != 0) return@Comparator result + + val signature1 = o1.signature + val signature2 = o2.signature + if (signature1 != null && signature2 != null) { + result = signature1.asString().compareTo(signature2.asString()) + if (result != 0) return@Comparator result + } + + // Fallback - calculate signature + val manualSignature1 = o1.computeSignature() + val manualSignature2 = o2.computeSignature() + manualSignature1.compareTo(manualSignature2) +} + +internal val KM_CONSTRUCTOR_COMPARATOR = Comparator<KmConstructor> { o1, o2 -> + val signature1 = o1.signature + val signature2 = o2.signature + if (signature1 != null && signature2 != null) { + val result = signature1.asString().compareTo(signature2.asString()) + if (result != 0) return@Comparator result + } + + // Fallback - calculate signature + val manualSignature1 = o1.computeSignature() + val manualSignature2 = o2.computeSignature() + manualSignature1.compareTo(manualSignature2) +} + +// Computes a simple signature string good enough for hashing +private fun KmFunction.computeSignature(): String { + return "$name(${valueParameters.joinToString(",") { it.type.simpleName }})${returnType.simpleName}" +} + +private fun KmConstructor.computeSignature(): String { + return "$<init>(${valueParameters.joinToString(",") { it.type.simpleName }})" +} + +private val KmType?.simpleName: String get() { + if (this == null) return "void" + return when (val c = classifier) { + is Class -> c.name + is TypeParameter -> "Object" + is TypeAlias -> c.name + } +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/KotlinPoetMetadataSpecs.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/KotlinPoetMetadataSpecs.kt new file mode 100644 index 00000000..6c0730a6 --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/KotlinPoetMetadataSpecs.kt @@ -0,0 +1,948 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:JvmName("KotlinPoetMetadataSpecs") + +package com.squareup.kotlinpoet.metadata.specs + +import com.squareup.kotlinpoet.ANY +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget +import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget.FILE +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.FunSpec.Builder +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.KModifier.ABSTRACT +import com.squareup.kotlinpoet.KModifier.CONST +import com.squareup.kotlinpoet.KModifier.CROSSINLINE +import com.squareup.kotlinpoet.KModifier.DATA +import com.squareup.kotlinpoet.KModifier.EXPECT +import com.squareup.kotlinpoet.KModifier.EXTERNAL +import com.squareup.kotlinpoet.KModifier.FINAL +import com.squareup.kotlinpoet.KModifier.INFIX +import com.squareup.kotlinpoet.KModifier.INLINE +import com.squareup.kotlinpoet.KModifier.INNER +import com.squareup.kotlinpoet.KModifier.INTERNAL +import com.squareup.kotlinpoet.KModifier.LATEINIT +import com.squareup.kotlinpoet.KModifier.NOINLINE +import com.squareup.kotlinpoet.KModifier.OPEN +import com.squareup.kotlinpoet.KModifier.OPERATOR +import com.squareup.kotlinpoet.KModifier.PRIVATE +import com.squareup.kotlinpoet.KModifier.PROTECTED +import com.squareup.kotlinpoet.KModifier.PUBLIC +import com.squareup.kotlinpoet.KModifier.SEALED +import com.squareup.kotlinpoet.KModifier.SUSPEND +import com.squareup.kotlinpoet.KModifier.TAILREC +import com.squareup.kotlinpoet.KModifier.VALUE +import com.squareup.kotlinpoet.KModifier.VARARG +import com.squareup.kotlinpoet.MemberName +import com.squareup.kotlinpoet.ParameterSpec +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeAliasSpec +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.UNIT +import com.squareup.kotlinpoet.asClassName +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import com.squareup.kotlinpoet.metadata.PropertyAccessorFlag +import com.squareup.kotlinpoet.metadata.PropertyAccessorFlag.IS_EXTERNAL +import com.squareup.kotlinpoet.metadata.PropertyAccessorFlag.IS_INLINE +import com.squareup.kotlinpoet.metadata.PropertyAccessorFlag.IS_NOT_DEFAULT +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil.createAnnotations +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil.createClassName +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil.toTreeSet +import com.squareup.kotlinpoet.metadata.declaresDefaultValue +import com.squareup.kotlinpoet.metadata.hasAnnotations +import com.squareup.kotlinpoet.metadata.hasGetter +import com.squareup.kotlinpoet.metadata.hasSetter +import com.squareup.kotlinpoet.metadata.isAbstract +import com.squareup.kotlinpoet.metadata.isAnnotation +import com.squareup.kotlinpoet.metadata.isClass +import com.squareup.kotlinpoet.metadata.isCompanionObject +import com.squareup.kotlinpoet.metadata.isConst +import com.squareup.kotlinpoet.metadata.isCrossInline +import com.squareup.kotlinpoet.metadata.isData +import com.squareup.kotlinpoet.metadata.isDeclaration +import com.squareup.kotlinpoet.metadata.isDelegated +import com.squareup.kotlinpoet.metadata.isDelegation +import com.squareup.kotlinpoet.metadata.isEnum +import com.squareup.kotlinpoet.metadata.isEnumEntry +import com.squareup.kotlinpoet.metadata.isExpect +import com.squareup.kotlinpoet.metadata.isExternal +import com.squareup.kotlinpoet.metadata.isFinal +import com.squareup.kotlinpoet.metadata.isFun +import com.squareup.kotlinpoet.metadata.isInfix +import com.squareup.kotlinpoet.metadata.isInline +import com.squareup.kotlinpoet.metadata.isInner +import com.squareup.kotlinpoet.metadata.isInterface +import com.squareup.kotlinpoet.metadata.isInternal +import com.squareup.kotlinpoet.metadata.isLateinit +import com.squareup.kotlinpoet.metadata.isNoInline +import com.squareup.kotlinpoet.metadata.isObject +import com.squareup.kotlinpoet.metadata.isOpen +import com.squareup.kotlinpoet.metadata.isOperator +import com.squareup.kotlinpoet.metadata.isPrimary +import com.squareup.kotlinpoet.metadata.isPrivate +import com.squareup.kotlinpoet.metadata.isProtected +import com.squareup.kotlinpoet.metadata.isPublic +import com.squareup.kotlinpoet.metadata.isReified +import com.squareup.kotlinpoet.metadata.isSealed +import com.squareup.kotlinpoet.metadata.isSuspend +import com.squareup.kotlinpoet.metadata.isSynthesized +import com.squareup.kotlinpoet.metadata.isTailRec +import com.squareup.kotlinpoet.metadata.isVal +import com.squareup.kotlinpoet.metadata.isValue +import com.squareup.kotlinpoet.metadata.isVar +import com.squareup.kotlinpoet.metadata.propertyAccessorFlags +import com.squareup.kotlinpoet.metadata.specs.JvmMethodModifier.DEFAULT +import com.squareup.kotlinpoet.metadata.toKmClass +import com.squareup.kotlinpoet.tag +import java.util.Locale +import javax.lang.model.element.Element +import javax.lang.model.element.ElementKind +import javax.lang.model.element.PackageElement +import javax.lang.model.element.TypeElement +import kotlin.reflect.KClass +import kotlinx.metadata.Flags +import kotlinx.metadata.KmClass +import kotlinx.metadata.KmClassifier +import kotlinx.metadata.KmConstructor +import kotlinx.metadata.KmFunction +import kotlinx.metadata.KmPackage +import kotlinx.metadata.KmProperty +import kotlinx.metadata.KmType +import kotlinx.metadata.KmTypeAlias +import kotlinx.metadata.KmValueParameter +import kotlinx.metadata.jvm.JvmMethodSignature +import kotlinx.metadata.jvm.getterSignature +import kotlinx.metadata.jvm.jvmInternalName +import kotlinx.metadata.jvm.setterSignature +import kotlinx.metadata.jvm.signature + +/** @return a [TypeSpec] ABI representation of this [KClass]. */ +@KotlinPoetMetadataPreview +public fun KClass<*>.toTypeSpec( + classInspector: ClassInspector? = null, +): TypeSpec = java.toTypeSpec(classInspector) + +/** @return a [TypeSpec] ABI representation of this [KClass]. */ +@KotlinPoetMetadataPreview +public fun Class<*>.toTypeSpec( + classInspector: ClassInspector? = null, +): TypeSpec = toKmClass().toTypeSpec(classInspector, asClassName()) + +/** @return a [TypeSpec] ABI representation of this [TypeElement]. */ +@Suppress("DEPRECATION") +@KotlinPoetMetadataPreview +public fun TypeElement.toTypeSpec( + classInspector: ClassInspector? = null, +): TypeSpec = toKmClass().toTypeSpec(classInspector, asClassName()) + +/** @return a [FileSpec] ABI representation of this [KClass]. */ +@KotlinPoetMetadataPreview +public fun KClass<*>.toFileSpec( + classInspector: ClassInspector? = null, +): FileSpec = java.toFileSpec(classInspector) + +/** @return a [FileSpec] ABI representation of this [KClass]. */ +@KotlinPoetMetadataPreview +public fun Class<*>.toFileSpec( + classInspector: ClassInspector? = null, +): FileSpec = FileSpec.get(`package`.name, toTypeSpec(classInspector)) + +/** @return a [FileSpec] ABI representation of this [TypeElement]. */ +@KotlinPoetMetadataPreview +public fun TypeElement.toFileSpec( + classInspector: ClassInspector? = null, +): FileSpec = FileSpec.get( + packageName = packageName, + typeSpec = toTypeSpec(classInspector), +) + +/** @return a [TypeSpec] ABI representation of this [KmClass]. */ +@KotlinPoetMetadataPreview +public fun KmClass.toTypeSpec( + classInspector: ClassInspector?, + className: ClassName = createClassName(name), +): TypeSpec { + return toTypeSpec(classInspector, className, null) +} + +/** @return a [FileSpec] ABI representation of this [KmClass]. */ +@KotlinPoetMetadataPreview +public fun KmClass.toFileSpec( + classInspector: ClassInspector?, + className: ClassName = createClassName(name), +): FileSpec { + return FileSpec.get( + packageName = className.packageName, + typeSpec = toTypeSpec(classInspector, className), + ) +} + +/** @return a [FileSpec] ABI representation of this [KmPackage]. */ +@KotlinPoetMetadataPreview +public fun KmPackage.toFileSpec( + classInspector: ClassInspector?, + className: ClassName, +): FileSpec { + val fileData = classInspector?.containerData(className, null) + check(fileData is FileData?) { + "Unexpected container data type: ${fileData?.javaClass}" + } + val fileName = fileData?.fileName ?: className.simpleName + return FileSpec.builder(className.packageName, fileName) + .apply { + fileData?.let { data -> + data.jvmName?.let { name -> + addAnnotation( + AnnotationSpec.builder(ClassInspectorUtil.JVM_NAME) + .addMember("name = %S", name) + .build(), + ) + } + val fileAnnotations = createAnnotations(FILE) { + addAll(data.annotations.filterNot { it.typeName == METADATA }) + } + for (fileAnnotation in fileAnnotations) { + addAnnotation(fileAnnotation) + } + } + for (function in functions) { + val methodData = fileData?.methods?.get(function) + addFunction( + function.toFunSpec( + classInspector = classInspector, + containerData = fileData, + methodData = methodData, + isInInterface = false, + ), + ) + } + for (property in properties) { + val propertyData = fileData?.properties?.get(property) + addProperty( + property.toPropertySpec( + classInspector = classInspector, + containerData = fileData, + propertyData = propertyData, + isInInterface = false, + ), + ) + } + for (alias in typeAliases) { + addTypeAlias(alias.toTypeAliasSpec()) + } + } + .build() +} + +private const val NOT_IMPLEMENTED = "throw·NotImplementedError(\"Stub!\")" + +@KotlinPoetMetadataPreview +private fun KmClass.toTypeSpec( + classInspector: ClassInspector?, + className: ClassName, + parentClassName: ClassName?, +): TypeSpec { + val classTypeParamsResolver = typeParameters.toTypeParameterResolver() + val jvmInternalName = name.jvmInternalName + val simpleName = className.simpleName + val classData = classInspector?.containerData(className, parentClassName) + check(classData is ClassData?) { + "Unexpected container data type: ${classData?.javaClass}" + } + + val builder = when { + isAnnotation -> TypeSpec.annotationBuilder(simpleName) + isCompanionObject -> TypeSpec.companionObjectBuilder(companionObjectName(simpleName)) + isEnum -> TypeSpec.enumBuilder(simpleName) + isExpect -> TypeSpec.expectClassBuilder(simpleName) + isObject -> TypeSpec.objectBuilder(simpleName) + isInterface -> { + if (classData?.declarationContainer?.isFun == true) { + TypeSpec.funInterfaceBuilder(simpleName) + } else { + TypeSpec.interfaceBuilder(simpleName) + } + } + isEnumEntry -> TypeSpec.anonymousClassBuilder() + else -> TypeSpec.classBuilder(simpleName) + } + + classData?.annotations + ?.filterNot { + it.typeName == METADATA || it.typeName in JAVA_ANNOTATION_ANNOTATIONS + } + ?.let(builder::addAnnotations) + + if (isEnum) { + enumEntries.forEach { entryName -> + val typeSpec = if (classInspector != null) { + val entry = classInspector.enumEntry(className, entryName) + entry.declarationContainer + ?.let { enumEntryClass -> + val entryClassName = className.nestedClass(entryName) + enumEntryClass.toTypeSpec(classInspector, entryClassName, parentClassName = className) + } + ?: TypeSpec.anonymousClassBuilder() + .addAnnotations(entry.annotations) + .build() + } else { + TypeSpec.anonymousClassBuilder() + .addKdoc( + "No ClassInspector was available during metadata parsing, so this entry may not be reflected accurately if it has a class body.", + ) + .build() + } + builder.addEnumConstant(entryName, typeSpec) + } + } + + if (!isEnumEntry) { + visibilityFrom(flags) { builder.addModifiers(it) } + builder.addModifiers( + *flags.modalities + .filterNot { it == FINAL } // Default + .filterNot { isInterface && it == ABSTRACT } // Abstract is a default on interfaces + .toTypedArray(), + ) + if (isData) { + builder.addModifiers(DATA) + } + if (isExternal) { + builder.addModifiers(EXTERNAL) + } + if (isValue) { + builder.addModifiers(VALUE) + } + if (isInner) { + builder.addModifiers(INNER) + } + builder.addTypeVariables(typeParameters.map { it.toTypeVariableName(classTypeParamsResolver) }) + // If we have an inspector, we can check exactly which "supertype" is an interface vs + // class. Without a handler though, we have to best-effort guess. Usually, the flow is: + // - First element of a non-interface type is the superclass (can be `Any`) + // - First element of an interface type is the first superinterface + val superClassFilter = classInspector?.let { handler -> + { type: KmType -> + !handler.isInterface(createClassName((type.classifier as KmClassifier.Class).name)) + } + } ?: { true } + val superClass = supertypes.asSequence() + .filter { it.classifier is KmClassifier.Class } + .find(superClassFilter) + if (superClass != null && !isEnum && !isInterface && !isAnnotation) { + superClass.toTypeName(classTypeParamsResolver).takeIf { it != ANY } + ?.let(builder::superclass) + } + builder.addSuperinterfaces( + supertypes.asSequence() + .filterNot { it == superClass } + .map { it.toTypeName(classTypeParamsResolver) } + .filterNot { it == ANY } + .asIterable(), + ) + val primaryConstructorParams = mutableMapOf<String, ParameterSpec>() + if (isClass || isAnnotation || isEnum) { + primaryConstructor?.let { + it.toFunSpec(classTypeParamsResolver, classData?.constructors?.get(it) ?: return@let) + .also { spec -> + val finalSpec = if (isEnum && spec.annotations.isEmpty()) { + // Metadata specifies the constructor as private, but that's implicit so we can omit it + spec.toBuilder().apply { modifiers.remove(PRIVATE) }.build() + } else { + spec + } + builder.primaryConstructor(finalSpec) + primaryConstructorParams.putAll(spec.parameters.associateBy { it.name }) + } + } + constructors.filter { !it.isPrimary }.takeIf { it.isNotEmpty() }?.let { secondaryConstructors -> + builder.addFunctions( + secondaryConstructors + .mapNotNull { kmConstructor -> + classData?.constructors?.get(kmConstructor)?.let { kmConstructor to it } + } + .map { (kmConstructor, constructorData) -> + kmConstructor.toFunSpec(classTypeParamsResolver, constructorData) + }, + ) + } + } + builder.addProperties( + properties + .asSequence() + .filter { it.isDeclaration } + .filterNot { it.isSynthesized } + .map { it to classData?.properties?.get(it) } + .map { (property, propertyData) -> + property.toPropertySpec( + typeParamResolver = classTypeParamsResolver, + isConstructorParam = property.name in primaryConstructorParams, + classInspector = classInspector, + containerData = classData, + propertyData = propertyData, + ) + } + .asIterable(), + ) + companionObject?.let { objectName -> + val companionType = if (classInspector != null) { + val companionClassName = className.nestedClass(objectName) + classInspector.classFor(companionClassName) + .toTypeSpec(classInspector, companionClassName, parentClassName = className) + } else { + TypeSpec.companionObjectBuilder(companionObjectName(objectName)) + .addKdoc( + "No ClassInspector was available during metadata parsing, so this companion object's API/contents may not be reflected accurately.", + ) + .build() + } + builder.addType(companionType) + } + } + builder.addFunctions( + functions + .asSequence() + .filter { it.isDeclaration } + .filterNot { it.isDelegation } + .filterNot { it.isSynthesized } + .map { it to classData?.methods?.get(it) } + .map { (func, methodData) -> + func.toFunSpec(classTypeParamsResolver, classInspector, classData, methodData) + .toBuilder() + .apply { + // For interface methods, remove any body and mark the methods as abstract + fun isKotlinDefaultInterfaceMethod(): Boolean { + classInspector?.let { handler -> + func.signature?.let { signature -> + val suffix = signature.desc.removePrefix("(") + return handler.methodExists( + className.nestedClass("DefaultImpls"), + signature.copy( + desc = "(L$jvmInternalName;$suffix", + ), + ) + } + } + return false + } + // For interface methods, remove any body and mark the methods as abstract + // IFF it doesn't have a default interface body. + if (isInterface && + annotations.none { it.typeName == JVM_DEFAULT } && + (methodData?.jvmModifiers?.contains(DEFAULT) == false) && + !isKotlinDefaultInterfaceMethod() + ) { + addModifiers(ABSTRACT) + clearBody() + } else if (ABSTRACT in modifiers) { + // Remove bodies for abstract functions + clearBody() + } + if (methodData?.isSynthetic == true) { + addKdoc( + "Note: Since this is a synthetic function, some JVM information " + + "(annotations, modifiers) may be missing.", + ) + } + } + .build() + } + .asIterable(), + ) + + for (it in nestedClasses) { + val nestedClassName = className.nestedClass(it) + val nestedClass = classInspector?.classFor(nestedClassName) + val nestedType = if (nestedClass != null) { + if (nestedClass.isCompanionObject) { + // We handle these separately + continue + } else { + nestedClass.toTypeSpec(classInspector, nestedClassName, parentClassName = className) + } + } else { + TypeSpec.classBuilder(it) + .addKdoc( + "No ClassInspector was available during metadata parsing, so this nested class's API/contents may not be reflected accurately.", + ) + .build() + } + builder.addType(nestedType) + } + + return builder + .tag(this) + .build() +} + +private fun companionObjectName(name: String): String? { + return if (name == "Companion") null else name +} + +@KotlinPoetMetadataPreview +private fun KmConstructor.toFunSpec( + typeParamResolver: TypeParameterResolver, + constructorData: ConstructorData?, +): FunSpec { + return FunSpec.constructorBuilder() + .apply { + addAnnotations(constructorData?.allAnnotations.orEmpty()) + visibilityFrom(flags) { addModifiers(it) } + addParameters( + this@toFunSpec.valueParameters.mapIndexed { index, param -> + param.toParameterSpec( + typeParamResolver, + constructorData?.takeIf { it != ConstructorData.EMPTY } + ?.parameterAnnotations + ?.get(index) + .orEmpty(), + ) + }, + ) + if (!isPrimary) { + // TODO How do we know when to add callSuperConstructor()? + } + } + .tag(this) + .build() +} + +@KotlinPoetMetadataPreview +private val ContainerData.isInterface: Boolean get() { + return declarationContainer.let { container -> + container is KmClass && container.isInterface + } +} + +@KotlinPoetMetadataPreview +private fun KmFunction.toFunSpec( + classTypeParamsResolver: TypeParameterResolver = TypeParameterResolver.EMPTY, + classInspector: ClassInspector? = null, + containerData: ContainerData? = null, + methodData: MethodData? = null, + isInInterface: Boolean = containerData?.isInterface ?: false, +): FunSpec { + val typeParamsResolver = typeParameters.toTypeParameterResolver( + fallback = classTypeParamsResolver, + ) + val mutableAnnotations = mutableListOf<AnnotationSpec>() + if (classInspector != null && containerData != null) { + signature?.let { signature -> + if (!containerData.isInterface) { + // Infer if JvmName was used + // We skip interface types for this because they can't have @JvmName. + signature.jvmNameAnnotation(name)?.let { jvmNameAnnotation -> + mutableAnnotations += jvmNameAnnotation + } + } + } + } + val anyReified = typeParameters.any { it.isReified } + val isInFacade = containerData is FileData + val annotations = mutableAnnotations + .plus(methodData?.allAnnotations(containsReifiedTypeParameter = anyReified).orEmpty()) + .filterNot { isInFacade && it.typeName == JVM_STATIC } + .toTreeSet() + return FunSpec.builder(name) + .apply { + addAnnotations(annotations) + visibilityFrom(flags) { addModifiers(it) } + val isOverride = methodData?.isOverride == true + addModifiers( + flags.modalities + .filterNot { it == FINAL && !isOverride } // Final is the default + .filterNot { it == OPEN && isOverride } // Overrides are implicitly open + .filterNot { it == OPEN && isInInterface }, // interface methods are implicitly open + ) + if (valueParameters.isNotEmpty()) { + addParameters( + valueParameters.mapIndexed { index, param -> + param.toParameterSpec( + typeParamsResolver, + // This can be empty if the element is synthetic + methodData?.parameterAnnotations?.get(index).orEmpty(), + ) + }, + ) + } + if (typeParameters.isNotEmpty()) { + addTypeVariables(typeParameters.map { it.toTypeVariableName(typeParamsResolver) }) + } + if (methodData?.isOverride == true) { + addModifiers(KModifier.OVERRIDE) + } + if (isOperator) { + addModifiers(OPERATOR) + } + if (isInfix) { + addModifiers(INFIX) + } + if (isInline) { + addModifiers(INLINE) + } + if (isTailRec) { + addModifiers(TAILREC) + } + if (isExternal) { + addModifiers(EXTERNAL) + } + if (isExpect) { + addModifiers(EXPECT) + } + if (isSuspend) { + addModifiers(SUSPEND) + } + val returnTypeName = this@toFunSpec.returnType.toTypeName(typeParamsResolver) + if (returnTypeName != UNIT) { + returns(returnTypeName) + if (!flags.isAbstract) { + addStatement(NOT_IMPLEMENTED) + } + } + receiverParameterType?.toTypeName(typeParamsResolver)?.let { receiver(it) } + } + .tag(this) + .build() +} + +@KotlinPoetMetadataPreview +private fun KmValueParameter.toParameterSpec( + typeParamResolver: TypeParameterResolver, + annotations: Collection<AnnotationSpec>, +): ParameterSpec { + val paramType = varargElementType ?: type ?: throw IllegalStateException("No argument type!") + return ParameterSpec.builder(name, paramType.toTypeName(typeParamResolver)) + .apply { + addAnnotations(annotations) + if (varargElementType != null) { + addModifiers(VARARG) + } + if (isCrossInline) { + addModifiers(CROSSINLINE) + } + if (isNoInline) { + addModifiers(NOINLINE) + } + if (declaresDefaultValue) { + defaultValue(NOT_IMPLEMENTED) + } + } + .tag(this) + .build() +} + +@KotlinPoetMetadataPreview +private fun KmProperty.toPropertySpec( + typeParamResolver: TypeParameterResolver = TypeParameterResolver.EMPTY, + isConstructorParam: Boolean = false, + classInspector: ClassInspector? = null, + containerData: ContainerData? = null, + propertyData: PropertyData? = null, + isInInterface: Boolean = containerData?.isInterface ?: false, +): PropertySpec { + val isOverride = propertyData?.isOverride ?: false + val returnTypeName = returnType.toTypeName(typeParamResolver) + val mutableAnnotations = mutableListOf<AnnotationSpec>() + if (containerData != null && propertyData != null) { + if (hasGetter) { + getterSignature?.let { getterSignature -> + if (!containerData.isInterface && + !flags.isOpen && + !flags.isAbstract + ) { + // Infer if JvmName was used + // We skip interface types or open/abstract properties because they can't have @JvmName. + // For annotation properties, kotlinc puts JvmName annotations by default in + // bytecode but they're implicit in source, so we expect the simple name for + // annotation types. + val expectedMetadataName = if (containerData is ClassData && + containerData.declarationContainer.isAnnotation + ) { + name + } else { + "get${name.safeCapitalize(Locale.US)}" + } + getterSignature.jvmNameAnnotation( + metadataName = expectedMetadataName, + useSiteTarget = UseSiteTarget.GET, + )?.let { jvmNameAnnotation -> + mutableAnnotations += jvmNameAnnotation + } + } + } + } + if (hasSetter) { + setterSignature?.let { setterSignature -> + if (containerData is ClassData && + !containerData.declarationContainer.isAnnotation && + !containerData.declarationContainer.isInterface && + classInspector?.supportsNonRuntimeRetainedAnnotations == false && + !flags.isOpen && + !flags.isAbstract + ) { + // Infer if JvmName was used + // We skip annotation types for this because they can't have vars. + // We skip interface types or open/abstract properties because they can't have @JvmName. + setterSignature.jvmNameAnnotation( + metadataName = "set${name.safeCapitalize(Locale.US)}", + useSiteTarget = UseSiteTarget.SET, + )?.let { jvmNameAnnotation -> + mutableAnnotations += jvmNameAnnotation + } + } + } + } + } + return PropertySpec.builder(name, returnTypeName) + .apply { + // If a property annotation doesn't have a custom site target and is used in a constructor + // we have to add the property: site target to it. + + val isInFacade = containerData is FileData + val finalAnnotations = mutableAnnotations + .plus(propertyData?.allAnnotations.orEmpty()) + .filterNot { (isConst || isInFacade) && it.typeName == JVM_STATIC } + .map { + if (isConstructorParam && it.useSiteTarget == null) { + // TODO Ideally don't do this if the annotation use site is only field? + // e.g. JvmField. It's technically fine, but redundant on parameters as it's + // automatically applied to the property for these annotation types. + // This is another thing ClassInspector *could* tell us + it.toBuilder().useSiteTarget(UseSiteTarget.PROPERTY).build() + } else { + it + } + } + .toTreeSet() + addAnnotations(finalAnnotations) + visibilityFrom(flags) { addModifiers(it) } + addModifiers( + flags.modalities + .filterNot { it == FINAL && !isOverride } // Final is the default + .filterNot { it == OPEN && isOverride } // Overrides are implicitly open + .filterNot { it == OPEN && isInInterface } // Interface properties implicitly open + .filterNot { it == ABSTRACT && isInInterface }, // Interface properties implicitly abstract + ) + if (isOverride) { + addModifiers(KModifier.OVERRIDE) + } + if (isConst) { + addModifiers(CONST) + } + if (isVar) { + mutable(true) + } else if (isVal) { + mutable(false) + } + if (isDelegated) { + // Placeholders for these are tricky + addKdoc("Note: delegation is ABI stub only and not guaranteed to match source code.") + if (isVal) { + delegate("%M { %L }", MemberName("kotlin", "lazy"), NOT_IMPLEMENTED) // Placeholder + } else { + if (returnTypeName.isNullable) { + delegate( + "%T.observable(null) { _, _, _ -> }", + ClassName("kotlin.properties", "Delegates"), + ) + } else { + delegate("%T.notNull()", ClassName("kotlin.properties", "Delegates")) // Placeholder + } + } + } + if (isExpect) { + addModifiers(EXPECT) + } + if (isExternal) { + addModifiers(EXTERNAL) + } + if (isLateinit) { + addModifiers(LATEINIT) + } + if (isConstructorParam || (!isDelegated && !isLateinit)) { + val constant = propertyData?.fieldData?.constant + when { + constant != null -> initializer(constant) + isConstructorParam -> initializer(name) + returnTypeName.isNullable -> initializer("null") + flags.isAbstract || isInInterface -> { + // No-op, don't emit an initializer for abstract or interface properties + } + else -> initializer(NOT_IMPLEMENTED) + } + } + // Delegated properties have setters/getters defined for some reason, ignore here + // since the delegate handles it + // vals with initialized constants have a getter in bytecode but not a body in kotlin source + val modifierSet = modifiers.toSet() + if (hasGetter && !isDelegated && !flags.isAbstract) { + propertyAccessor( + modifierSet, + getterFlags, + FunSpec.getterBuilder().addStatement(NOT_IMPLEMENTED), + isOverride, + )?.let(::getter) + } + if (hasSetter && !isDelegated && !flags.isAbstract) { + propertyAccessor(modifierSet, setterFlags, FunSpec.setterBuilder(), isOverride)?.let(::setter) + } + } + .tag(this) + .build() +} + +@KotlinPoetMetadataPreview +private fun propertyAccessor( + propertyModifiers: Set<KModifier>, + flags: Flags, + functionBuilder: Builder, + isOverride: Boolean, +): FunSpec? { + val visibility = flags.visibility + if (visibility == PUBLIC || visibility !in propertyModifiers) { + // This is redundant and just a stub + // For annotations on this accessor, we declare them on the property with site target instead + return null + } + val modalities = flags.modalities + .filterNot { it == FINAL && !isOverride } + .filterNot { it == OPEN && isOverride } + val propertyAccessorFlags = flags.propertyAccessorFlags + return if (visibility != PUBLIC || modalities.isNotEmpty() || propertyAccessorFlags.isNotEmpty()) { + functionBuilder + .apply { + addModifiers(visibility) + addModifiers(modalities) + addModifiers(*propertyAccessorFlags.toKModifiersArray()) + } + .build() + } else { + null + } +} + +private fun Set<PropertyAccessorFlag>.toKModifiersArray(): Array<KModifier> { + return mapNotNull { + when (it) { + IS_EXTERNAL -> EXTERNAL + IS_INLINE -> INLINE + IS_NOT_DEFAULT -> null // Gracefully skip over these + } + }.toTypedArray() +} + +@KotlinPoetMetadataPreview +private fun KmTypeAlias.toTypeAliasSpec(): TypeAliasSpec { + val typeParamResolver = typeParameters.toTypeParameterResolver() + return TypeAliasSpec.builder(name, underlyingType.toTypeName(typeParamResolver)) + .apply { + visibilityFrom(flags) { + addModifiers(it) + } + if (flags.hasAnnotations) { + val annotationSpecs = this@toTypeAliasSpec.annotations + .map { it.toAnnotationSpec() } + addAnnotations(annotationSpecs) + } + } + .addTypeVariables(typeParamResolver.parametersMap.values) + .build() +} + +private fun JvmMethodSignature.jvmNameAnnotation( + metadataName: String, + useSiteTarget: UseSiteTarget? = null, +): AnnotationSpec? { + return if (name == metadataName) { + null + } else { + return AnnotationSpec.builder(JvmName::class) + .addMember("name = %S", name) + .useSiteTarget(useSiteTarget) + .build() + } +} + +private val JAVA_ANNOTATION_ANNOTATIONS = setOf( + java.lang.annotation.Retention::class.asClassName(), + java.lang.annotation.Target::class.asClassName(), +) + +@KotlinPoetMetadataPreview +private val Flags.visibility: KModifier + get() = when { + isInternal -> INTERNAL + isPrivate -> PRIVATE + isProtected -> PROTECTED + isPublic -> PUBLIC + else -> { + // IS_PRIVATE_TO_THIS or IS_LOCAL, so just default to public + PUBLIC + } + } + +@KotlinPoetMetadataPreview +private fun visibilityFrom(flags: Flags, body: (KModifier) -> Unit) { + val modifierVisibility = flags.visibility + if (modifierVisibility != PUBLIC) { + body(modifierVisibility) + } +} + +private fun String.safeCapitalize(locale: Locale): String { + return replaceFirstChar { if (it.isLowerCase()) it.titlecase(locale) else it.toString() } +} + +@KotlinPoetMetadataPreview +private val Flags.modalities: Set<KModifier> + get() = setOf { + if (isFinal) { + add(FINAL) + } + if (isOpen) { + add(OPEN) + } + if (isAbstract) { + add(ABSTRACT) + } + if (isSealed) { + add(SEALED) + } + } + +private inline fun <E> setOf(body: MutableSet<E>.() -> Unit): Set<E> { + return mutableSetOf<E>().apply(body).toSet() +} + +private val METADATA = Metadata::class.asClassName() + +@Suppress("DEPRECATION") +private val JVM_DEFAULT = JvmDefault::class.asClassName() +private val JVM_STATIC = JvmStatic::class.asClassName() + +@PublishedApi +internal val Element.packageName: String + get() { + var element = this + while (element.kind != ElementKind.PACKAGE) { + element = element.enclosingElement + } + return (element as PackageElement).toString() + } diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/MethodData.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/MethodData.kt new file mode 100644 index 00000000..7319e9d8 --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/MethodData.kt @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.metadata.specs + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil + +/** + * Represents relevant information on a method used for [ClassInspector]. Should only be + * associated with methods of a [ClassData] or [PropertyData]. + * + * @param annotations declared annotations on this method. + * @property parameterAnnotations a mapping of parameter indices to annotations on them. + * @property isSynthetic indicates if this method is synthetic or not. + * @property jvmModifiers set of [JvmMethodModifiers][JvmMethodModifier] on this method. + * @property isOverride indicates if this method overrides one in a supertype. + * @property exceptions list of exceptions thrown by this method. + */ +@KotlinPoetMetadataPreview +public data class MethodData( + private val annotations: List<AnnotationSpec>, + val parameterAnnotations: Map<Int, Collection<AnnotationSpec>>, + val isSynthetic: Boolean, + val jvmModifiers: Set<JvmMethodModifier>, + val isOverride: Boolean, + val exceptions: List<TypeName>, +) { + + /** + * A collection of all annotations on this method, including any derived from [jvmModifiers], + * [isSynthetic], and [exceptions]. + * + * @param useSiteTarget an optional [UseSiteTarget] that all annotations on this method should + * use. + * @param containsReifiedTypeParameter an optional boolean indicating if any type parameters on + * this function are `reified`, which are implicitly synthetic. + */ + public fun allAnnotations( + useSiteTarget: UseSiteTarget? = null, + containsReifiedTypeParameter: Boolean = false, + ): Collection<AnnotationSpec> { + return ClassInspectorUtil.createAnnotations( + useSiteTarget, + ) { + addAll(annotations) + if (isSynthetic && !containsReifiedTypeParameter) { + add(ClassInspectorUtil.JVM_SYNTHETIC_SPEC) + } + addAll(jvmModifiers.mapNotNull(JvmMethodModifier::annotationSpec)) + exceptions.takeIf { it.isNotEmpty() } + ?.let { + add(ClassInspectorUtil.createThrowsSpec(it, useSiteTarget)) + } + } + } + + public companion object { + public val SYNTHETIC: MethodData = MethodData( + annotations = emptyList(), + parameterAnnotations = emptyMap(), + isSynthetic = true, + jvmModifiers = emptySet(), + isOverride = false, + exceptions = emptyList(), + ) + public val EMPTY: MethodData = MethodData( + annotations = emptyList(), + parameterAnnotations = emptyMap(), + isSynthetic = false, + jvmModifiers = emptySet(), + isOverride = false, + exceptions = emptyList(), + ) + } +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/PropertyData.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/PropertyData.kt new file mode 100644 index 00000000..9fc6d3e4 --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/PropertyData.kt @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.metadata.specs + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget.GET +import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget.SET +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil + +/** + * Represents relevant information on a property used for [ClassInspector]. Should only be + * associated with properties of a [ClassData]. + * + * @param annotations declared annotations on this property. + * @property fieldData associated [FieldData] with this property, if any. + * @property getterData associated getter (as [MethodData]) with this property, if any. + * @property setterData associated setter (as [MethodData]) with this property, if any. + * @property isJvmField indicates if this property should be treated as a jvm field. + */ +@KotlinPoetMetadataPreview +public data class PropertyData( + private val annotations: List<AnnotationSpec>, + val fieldData: FieldData?, + val getterData: MethodData?, + val setterData: MethodData?, + val isJvmField: Boolean, +) { + /** Indicates if this property overrides another from a supertype. */ + val isOverride: Boolean = (getterData?.isOverride ?: false) || (setterData?.isOverride ?: false) + + /** + * A collection of all annotations on this property including declared ones and any derived from + * [fieldData], [getterData], [setterData], and [isJvmField]. + */ + val allAnnotations: Collection<AnnotationSpec> = ClassInspectorUtil.createAnnotations { + // Don't add annotations that are already defined on the parent + val higherScopedAnnotations = annotations.associateBy { it.typeName } + val fieldAnnotations = fieldData?.allAnnotations.orEmpty() + .filterNot { it.typeName in higherScopedAnnotations } + .associateByTo(LinkedHashMap()) { it.typeName } + val getterAnnotations = getterData?.allAnnotations(GET).orEmpty() + .filterNot { it.typeName in higherScopedAnnotations } + .associateByTo(LinkedHashMap()) { it.typeName } + + val finalTopAnnotations = annotations.toMutableList() + + // If this is a val, and annotation is on both getter and field, we can move it to just the + // regular annotations + if (setterData == null && !isJvmField) { + val sharedAnnotations = getterAnnotations.keys.intersect(fieldAnnotations.keys) + for (sharedAnnotation in sharedAnnotations) { + // Add it to the top-level annotations without a site-target + finalTopAnnotations += getterAnnotations.getValue(sharedAnnotation).toBuilder() + .useSiteTarget(null) + .build() + + // Remove from field and getter + fieldAnnotations.remove(sharedAnnotation) + getterAnnotations.remove(sharedAnnotation) + } + } + + addAll(finalTopAnnotations) + addAll(fieldAnnotations.values) + addAll(getterAnnotations.values) + addAll( + setterData?.allAnnotations(SET).orEmpty() + .filterNot { it.typeName in higherScopedAnnotations }, + ) + if (isJvmField) { + add(ClassInspectorUtil.JVM_FIELD_SPEC) + } + } +} diff --git a/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/kmAnnotations.kt b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/kmAnnotations.kt new file mode 100644 index 00000000..f6b44670 --- /dev/null +++ b/interop/kotlinx-metadata/src/main/kotlin/com/squareup/kotlinpoet/metadata/specs/kmAnnotations.kt @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.metadata.specs + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.joinToCode +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil.createClassName +import com.squareup.kotlinpoet.tag +import kotlinx.metadata.KmAnnotation +import kotlinx.metadata.KmAnnotationArgument +import kotlinx.metadata.KmAnnotationArgument.AnnotationValue +import kotlinx.metadata.KmAnnotationArgument.ArrayValue +import kotlinx.metadata.KmAnnotationArgument.BooleanValue +import kotlinx.metadata.KmAnnotationArgument.ByteValue +import kotlinx.metadata.KmAnnotationArgument.CharValue +import kotlinx.metadata.KmAnnotationArgument.DoubleValue +import kotlinx.metadata.KmAnnotationArgument.EnumValue +import kotlinx.metadata.KmAnnotationArgument.FloatValue +import kotlinx.metadata.KmAnnotationArgument.IntValue +import kotlinx.metadata.KmAnnotationArgument.KClassValue +import kotlinx.metadata.KmAnnotationArgument.LongValue +import kotlinx.metadata.KmAnnotationArgument.ShortValue +import kotlinx.metadata.KmAnnotationArgument.StringValue +import kotlinx.metadata.KmAnnotationArgument.UByteValue +import kotlinx.metadata.KmAnnotationArgument.UIntValue +import kotlinx.metadata.KmAnnotationArgument.ULongValue +import kotlinx.metadata.KmAnnotationArgument.UShortValue + +@KotlinPoetMetadataPreview +internal fun KmAnnotation.toAnnotationSpec(): AnnotationSpec { + val cn = createClassName(className) + return AnnotationSpec.builder(cn) + .apply { + arguments.forEach { (name, arg) -> + addMember("%L = %L", name, arg.toCodeBlock()) + } + } + .tag(this) + .build() +} + +@OptIn(ExperimentalUnsignedTypes::class) +@KotlinPoetMetadataPreview +internal fun KmAnnotationArgument.toCodeBlock(): CodeBlock { + return when (this) { + is ByteValue -> CodeBlock.of("%L", value) + is CharValue -> CodeBlock.of("'%L'", value) + is ShortValue -> CodeBlock.of("%L", value) + is IntValue -> CodeBlock.of("%L", value) + is LongValue -> CodeBlock.of("%LL", value) + is FloatValue -> CodeBlock.of("%LF", value) + is DoubleValue -> CodeBlock.of("%L", value) + is BooleanValue -> CodeBlock.of("%L", value) + is UByteValue -> CodeBlock.of("%Lu", value) + is UShortValue -> CodeBlock.of("%Lu", value) + is UIntValue -> CodeBlock.of("%Lu", value) + is ULongValue -> CodeBlock.of("%Lu", value) + is StringValue -> CodeBlock.of("%S", value) + is KClassValue -> CodeBlock.of("%T::class", createClassName(className)) + is EnumValue -> CodeBlock.of("%T.%L", createClassName(enumClassName), enumEntryName) + is AnnotationValue -> CodeBlock.of("%L", annotation.toAnnotationSpec()) + is ArrayValue -> elements.map { it.toCodeBlock() }.joinToCode(", ", "[", "]") + } +} diff --git a/interop/kotlinx-metadata/src/test/kotlin/com/squareup/kotlinpoet/metadata/specs/FacadeFile.kt b/interop/kotlinx-metadata/src/test/kotlin/com/squareup/kotlinpoet/metadata/specs/FacadeFile.kt new file mode 100644 index 00000000..6eb40a88 --- /dev/null +++ b/interop/kotlinx-metadata/src/test/kotlin/com/squareup/kotlinpoet/metadata/specs/FacadeFile.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:JvmName("FacadeFile") +@file:FileAnnotation("file annotations!") + +package com.squareup.kotlinpoet.metadata.specs + +import kotlin.annotation.AnnotationTarget.FILE + +@Target(FILE) +annotation class FileAnnotation(val value: String) + +@JvmName("jvmStaticFunction") +fun jvmNameFunction() { +} + +fun regularFun() { +} + +@Synchronized +fun synchronizedFun() { +} + +@JvmOverloads +fun jvmOverloads( + param1: String, + optionalParam2: String = "", + nullableParam3: String? = null, +) { +} + +val BOOL_PROP = false +val BINARY_PROP = 0b00001011 +val INT_PROP = 1 +val UNDERSCORES_PROP = 1_000_000 +val HEX_PROP = 0x0F +val UNDERSCORES_HEX_PROP = 0xFF_EC_DE_5E +val LONG_PROP = 1L +val FLOAT_PROP = 1.0f +val DOUBLE_PROP = 1.0 +val STRING_PROP = "prop" +var VAR_BOOL_PROP = false +var VAR_BINARY_PROP = 0b00001011 +var VAR_INT_PROP = 1 +var VAR_UNDERSCORES_PROP = 1_000_000 +var VAR_HEX_PROP = 0x0F +var VAR_UNDERSCORES_HEX_PROP = 0xFF_EC_DE_5E +var VAR_LONG_PROP = 1L +var VAR_FLOAT_PROP = 1.0f +var VAR_DOUBLE_PROP = 1.0 +var VAR_STRING_PROP = "prop" + +const val CONST_BOOL_PROP = false +const val CONST_BINARY_PROP = 0b00001011 +const val CONST_INT_PROP = 1 +const val CONST_UNDERSCORES_PROP = 1_000_000 +const val CONST_HEX_PROP = 0x0F +const val CONST_UNDERSCORES_HEX_PROP = 0xFF_EC_DE_5E +const val CONST_LONG_PROP = 1L +const val CONST_FLOAT_PROP = 1.0f +const val CONST_DOUBLE_PROP = 1.0 +const val CONST_STRING_PROP = "prop" + +@JvmField +@JvmSynthetic +val syntheticFieldProperty: kotlin.String? = null + +@field:JvmSynthetic +val syntheticProperty: kotlin.String? = null + +@get:JvmSynthetic +val syntheticPropertyGet: kotlin.String? = null + +@get:JvmSynthetic +@set:JvmSynthetic +var syntheticPropertyGetAndSet: kotlin.String? = null + +@set:JvmSynthetic +var syntheticPropertySet: kotlin.String? = null + +typealias FacadeTypeAliasName = String +typealias FacadeGenericTypeAlias = List<String> +typealias FacadeNestedTypeAlias = List<GenericTypeAlias> diff --git a/interop/kotlinx-metadata/src/test/kotlin/com/squareup/kotlinpoet/metadata/specs/FacadeFileTest.kt b/interop/kotlinx-metadata/src/test/kotlin/com/squareup/kotlinpoet/metadata/specs/FacadeFileTest.kt new file mode 100644 index 00000000..adea853f --- /dev/null +++ b/interop/kotlinx-metadata/src/test/kotlin/com/squareup/kotlinpoet/metadata/specs/FacadeFileTest.kt @@ -0,0 +1,394 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.metadata.specs + +import com.google.common.truth.Truth.assertThat +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import com.squareup.kotlinpoet.metadata.specs.MultiClassInspectorTest.ClassInspectorType.ELEMENTS +import com.squareup.kotlinpoet.metadata.specs.MultiClassInspectorTest.ClassInspectorType.REFLECTIVE +import org.junit.Test + +@KotlinPoetMetadataPreview +class FacadeFileTest : MultiClassInspectorTest() { + + @IgnoreForHandlerType( + handlerType = ELEMENTS, + reason = "Elements can detect JvmOverloads, JvmName not possible in reflection", + ) + @Test + fun facadeFile_reflective() { + val fileSpec = Class.forName( + "com.squareup.kotlinpoet.metadata.specs.FacadeFile", + ).kotlin.toFileSpecWithTestHandler() + assertThat(fileSpec.name).isEqualTo("FacadeFile") + //language=kotlin + assertThat(fileSpec.trimmedToString()).isEqualTo( + """ + @file:JvmName(name = "FacadeFile") + @file:FileAnnotation(value = "file annotations!") + + package com.squareup.kotlinpoet.metadata.specs + + import com.squareup.kotlinpoet.metadata.specs.FileAnnotation + import kotlin.Boolean + import kotlin.Double + import kotlin.Float + import kotlin.Int + import kotlin.Long + import kotlin.String + import kotlin.Unit + import kotlin.collections.List + import kotlin.jvm.JvmField + import kotlin.jvm.JvmName + import kotlin.jvm.JvmSynthetic + import kotlin.jvm.Synchronized + + @JvmName(name = "jvmStaticFunction") + public fun jvmNameFunction(): Unit { + } + + public fun jvmOverloads( + param1: String, + optionalParam2: String = throw NotImplementedError("Stub!"), + nullableParam3: String? = throw NotImplementedError("Stub!"), + ): Unit { + } + + public fun regularFun(): Unit { + } + + @Synchronized + public fun synchronizedFun(): Unit { + } + + public val BINARY_PROP: Int = 11 + + public val BOOL_PROP: Boolean = false + + public const val CONST_BINARY_PROP: Int = 11 + + public const val CONST_BOOL_PROP: Boolean = false + + public const val CONST_DOUBLE_PROP: Double = 1.0 + + public const val CONST_FLOAT_PROP: Float = 1.0F + + public const val CONST_HEX_PROP: Int = 15 + + public const val CONST_INT_PROP: Int = 1 + + public const val CONST_LONG_PROP: Long = 1L + + public const val CONST_STRING_PROP: String = "prop" + + public const val CONST_UNDERSCORES_HEX_PROP: Long = 4_293_713_502L + + public const val CONST_UNDERSCORES_PROP: Int = 1_000_000 + + public val DOUBLE_PROP: Double = 1.0 + + public val FLOAT_PROP: Float = 1.0F + + public val HEX_PROP: Int = 15 + + public val INT_PROP: Int = 1 + + public val LONG_PROP: Long = 1L + + public val STRING_PROP: String = "prop" + + public val UNDERSCORES_HEX_PROP: Long = 4_293_713_502L + + public val UNDERSCORES_PROP: Int = 1_000_000 + + public var VAR_BINARY_PROP: Int = throw NotImplementedError("Stub!") + + public var VAR_BOOL_PROP: Boolean = throw NotImplementedError("Stub!") + + public var VAR_DOUBLE_PROP: Double = throw NotImplementedError("Stub!") + + public var VAR_FLOAT_PROP: Float = throw NotImplementedError("Stub!") + + public var VAR_HEX_PROP: Int = throw NotImplementedError("Stub!") + + public var VAR_INT_PROP: Int = throw NotImplementedError("Stub!") + + public var VAR_LONG_PROP: Long = throw NotImplementedError("Stub!") + + public var VAR_STRING_PROP: String = throw NotImplementedError("Stub!") + + public var VAR_UNDERSCORES_HEX_PROP: Long = throw NotImplementedError("Stub!") + + public var VAR_UNDERSCORES_PROP: Int = throw NotImplementedError("Stub!") + + @field:JvmSynthetic + @JvmField + public val syntheticFieldProperty: String? = null + + @field:JvmSynthetic + public val syntheticProperty: String? = null + + @get:JvmSynthetic + public val syntheticPropertyGet: String? = null + + @get:JvmSynthetic + @set:JvmSynthetic + public var syntheticPropertyGetAndSet: String? = null + + @set:JvmSynthetic + public var syntheticPropertySet: String? = null + + public typealias FacadeGenericTypeAlias = List<String> + + public typealias FacadeNestedTypeAlias = List<GenericTypeAlias> + + public typealias FacadeTypeAliasName = String + """.trimIndent(), + ) + } + + @IgnoreForHandlerType( + handlerType = REFLECTIVE, + reason = "Elements can detect JvmOverloads, JvmName not possible in reflection", + ) + @Test + fun facadeFile_elements() { + val fileSpec = Class.forName( + "com.squareup.kotlinpoet.metadata.specs.FacadeFile", + ).kotlin.toFileSpecWithTestHandler() + assertThat(fileSpec.name).isEqualTo("FacadeFile") + //language=kotlin + assertThat(fileSpec.trimmedToString()).isEqualTo( + """ + @file:FileAnnotation(value = "file annotations!") + @file:JvmName(name = "FacadeFile") + + package com.squareup.kotlinpoet.metadata.specs + + import com.squareup.kotlinpoet.metadata.specs.FileAnnotation + import kotlin.Boolean + import kotlin.Double + import kotlin.Float + import kotlin.Int + import kotlin.Long + import kotlin.String + import kotlin.Unit + import kotlin.collections.List + import kotlin.jvm.JvmName + import kotlin.jvm.JvmOverloads + import kotlin.jvm.JvmSynthetic + import kotlin.jvm.Synchronized + + @JvmName(name = "jvmStaticFunction") + public fun jvmNameFunction(): Unit { + } + + @JvmOverloads + public fun jvmOverloads( + param1: String, + optionalParam2: String = throw NotImplementedError("Stub!"), + nullableParam3: String? = throw NotImplementedError("Stub!"), + ): Unit { + } + + public fun regularFun(): Unit { + } + + @Synchronized + public fun synchronizedFun(): Unit { + } + + public val BINARY_PROP: Int = throw NotImplementedError("Stub!") + + public val BOOL_PROP: Boolean = throw NotImplementedError("Stub!") + + public const val CONST_BINARY_PROP: Int = 11 + + public const val CONST_BOOL_PROP: Boolean = false + + public const val CONST_DOUBLE_PROP: Double = 1.0 + + public const val CONST_FLOAT_PROP: Float = 1.0F + + public const val CONST_HEX_PROP: Int = 15 + + public const val CONST_INT_PROP: Int = 1 + + public const val CONST_LONG_PROP: Long = 1L + + public const val CONST_STRING_PROP: String = "prop" + + public const val CONST_UNDERSCORES_HEX_PROP: Long = 4_293_713_502L + + public const val CONST_UNDERSCORES_PROP: Int = 1_000_000 + + public val DOUBLE_PROP: Double = throw NotImplementedError("Stub!") + + public val FLOAT_PROP: Float = throw NotImplementedError("Stub!") + + public val HEX_PROP: Int = throw NotImplementedError("Stub!") + + public val INT_PROP: Int = throw NotImplementedError("Stub!") + + public val LONG_PROP: Long = throw NotImplementedError("Stub!") + + public val STRING_PROP: String = throw NotImplementedError("Stub!") + + public val UNDERSCORES_HEX_PROP: Long = throw NotImplementedError("Stub!") + + public val UNDERSCORES_PROP: Int = throw NotImplementedError("Stub!") + + public var VAR_BINARY_PROP: Int = throw NotImplementedError("Stub!") + + public var VAR_BOOL_PROP: Boolean = throw NotImplementedError("Stub!") + + public var VAR_DOUBLE_PROP: Double = throw NotImplementedError("Stub!") + + public var VAR_FLOAT_PROP: Float = throw NotImplementedError("Stub!") + + public var VAR_HEX_PROP: Int = throw NotImplementedError("Stub!") + + public var VAR_INT_PROP: Int = throw NotImplementedError("Stub!") + + public var VAR_LONG_PROP: Long = throw NotImplementedError("Stub!") + + public var VAR_STRING_PROP: String = throw NotImplementedError("Stub!") + + public var VAR_UNDERSCORES_HEX_PROP: Long = throw NotImplementedError("Stub!") + + public var VAR_UNDERSCORES_PROP: Int = throw NotImplementedError("Stub!") + + @field:JvmSynthetic + public val syntheticFieldProperty: String? = null + + @field:JvmSynthetic + public val syntheticProperty: String? = null + + @get:JvmSynthetic + public val syntheticPropertyGet: String? = null + + @get:JvmSynthetic + @set:JvmSynthetic + public var syntheticPropertyGetAndSet: String? = null + + @set:JvmSynthetic + public var syntheticPropertySet: String? = null + + public typealias FacadeGenericTypeAlias = List<String> + + public typealias FacadeNestedTypeAlias = List<GenericTypeAlias> + + public typealias FacadeTypeAliasName = String + """.trimIndent(), + ) + } + + @IgnoreForHandlerType( + handlerType = ELEMENTS, + reason = "JvmName not possible in reflection", + ) + @Test + fun noJvmName_reflective() { + val fileSpec = Class.forName( + "com.squareup.kotlinpoet.metadata.specs.NoJvmNameFacadeFileKt", + ).kotlin.toFileSpecWithTestHandler() + assertThat(fileSpec.name).isEqualTo("NoJvmNameFacadeFile") + //language=kotlin + assertThat(fileSpec.trimmedToString()).isEqualTo( + """ + package com.squareup.kotlinpoet.metadata.specs + + import kotlin.String + + public val prop: String = "" + """.trimIndent(), + ) + } + + @IgnoreForHandlerType( + handlerType = REFLECTIVE, + reason = "JvmName not possible in reflection", + ) + @Test + fun noJvmName_elements() { + val fileSpec = Class.forName( + "com.squareup.kotlinpoet.metadata.specs.NoJvmNameFacadeFileKt", + ).kotlin.toFileSpecWithTestHandler() + assertThat(fileSpec.name).isEqualTo("NoJvmNameFacadeFile") + //language=kotlin + assertThat(fileSpec.trimmedToString()).isEqualTo( + """ + package com.squareup.kotlinpoet.metadata.specs + + import kotlin.String + + public val prop: String = throw NotImplementedError("Stub!") + """.trimIndent(), + ) + } + + @IgnoreForHandlerType( + handlerType = ELEMENTS, + reason = "JvmName not possible in reflection", + ) + @Test + fun jvmName_with_kt_reflective() { + val fileSpec = Class.forName( + "com.squareup.kotlinpoet.metadata.specs.JvmNameKt", + ).kotlin.toFileSpecWithTestHandler() + assertThat(fileSpec.name).isEqualTo("JvmName") + //language=kotlin + assertThat(fileSpec.trimmedToString()).isEqualTo( + """ + package com.squareup.kotlinpoet.metadata.specs + + import kotlin.String + + public val prop2: String = "" + """.trimIndent(), + ) + } + + @IgnoreForHandlerType( + handlerType = REFLECTIVE, + reason = "JvmName not possible in reflection", + ) + @Test + fun jvmName_with_kt_elements() { + val fileSpec = Class.forName( + "com.squareup.kotlinpoet.metadata.specs.JvmNameKt", + ).kotlin.toFileSpecWithTestHandler() + assertThat(fileSpec.name).isEqualTo("JvmName") + //language=kotlin + assertThat(fileSpec.trimmedToString()).isEqualTo( + """ + @file:JvmName(name = "JvmNameKt") + + package com.squareup.kotlinpoet.metadata.specs + + import kotlin.String + import kotlin.jvm.JvmName + + public val prop2: String = throw NotImplementedError("Stub!") + """.trimIndent(), + ) + } +} + +private fun FileSpec.trimmedToString(): String { + return buildString { writeTo(this) }.trim() +} diff --git a/interop/kotlinx-metadata/src/test/kotlin/com/squareup/kotlinpoet/metadata/specs/JvmNameWithKtFacadeFile.kt b/interop/kotlinx-metadata/src/test/kotlin/com/squareup/kotlinpoet/metadata/specs/JvmNameWithKtFacadeFile.kt new file mode 100644 index 00000000..008b36f1 --- /dev/null +++ b/interop/kotlinx-metadata/src/test/kotlin/com/squareup/kotlinpoet/metadata/specs/JvmNameWithKtFacadeFile.kt @@ -0,0 +1,20 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:JvmName("JvmNameKt") + +package com.squareup.kotlinpoet.metadata.specs + +val prop2: String = "" diff --git a/interop/kotlinx-metadata/src/test/kotlin/com/squareup/kotlinpoet/metadata/specs/KmAnnotationsTest.kt b/interop/kotlinx-metadata/src/test/kotlin/com/squareup/kotlinpoet/metadata/specs/KmAnnotationsTest.kt new file mode 100644 index 00000000..c7928b4e --- /dev/null +++ b/interop/kotlinx-metadata/src/test/kotlin/com/squareup/kotlinpoet/metadata/specs/KmAnnotationsTest.kt @@ -0,0 +1,260 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.metadata.specs + +import com.google.common.truth.Truth.assertThat +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import kotlin.test.Test +import kotlinx.metadata.KmAnnotation +import kotlinx.metadata.KmAnnotationArgument.AnnotationValue +import kotlinx.metadata.KmAnnotationArgument.ArrayValue +import kotlinx.metadata.KmAnnotationArgument.BooleanValue +import kotlinx.metadata.KmAnnotationArgument.ByteValue +import kotlinx.metadata.KmAnnotationArgument.CharValue +import kotlinx.metadata.KmAnnotationArgument.DoubleValue +import kotlinx.metadata.KmAnnotationArgument.EnumValue +import kotlinx.metadata.KmAnnotationArgument.FloatValue +import kotlinx.metadata.KmAnnotationArgument.IntValue +import kotlinx.metadata.KmAnnotationArgument.KClassValue +import kotlinx.metadata.KmAnnotationArgument.LongValue +import kotlinx.metadata.KmAnnotationArgument.ShortValue +import kotlinx.metadata.KmAnnotationArgument.StringValue +import kotlinx.metadata.KmAnnotationArgument.UByteValue +import kotlinx.metadata.KmAnnotationArgument.UIntValue +import kotlinx.metadata.KmAnnotationArgument.ULongValue +import kotlinx.metadata.KmAnnotationArgument.UShortValue + +@OptIn(ExperimentalUnsignedTypes::class) +@KotlinPoetMetadataPreview +class KmAnnotationsTest { + + @Test fun noMembers() { + val annotation = KmAnnotation("test/NoMembersAnnotation", emptyMap()) + assertThat(annotation.toAnnotationSpec().toString()).isEqualTo( + """ + @test.NoMembersAnnotation + """.trimIndent(), + ) + } + + @Test fun byteValue() { + val annotation = KmAnnotation( + "test/ByteValueAnnotation", + mapOf("value" to ByteValue(2)), + ) + assertThat(annotation.toAnnotationSpec().toString()).isEqualTo( + """ + @test.ByteValueAnnotation(value = 2) + """.trimIndent(), + ) + } + + @Test fun charValue() { + val annotation = KmAnnotation( + "test/CharValueAnnotation", + mapOf("value" to CharValue('2')), + ) + assertThat(annotation.toAnnotationSpec().toString()).isEqualTo( + """ + @test.CharValueAnnotation(value = '2') + """.trimIndent(), + ) + } + + @Test fun shortValue() { + val annotation = KmAnnotation( + "test/ShortValueAnnotation", + mapOf("value" to ShortValue(2)), + ) + assertThat(annotation.toAnnotationSpec().toString()).isEqualTo( + """ + @test.ShortValueAnnotation(value = 2) + """.trimIndent(), + ) + } + + @Test fun intValue() { + val annotation = KmAnnotation( + "test/IntValueAnnotation", + mapOf("value" to IntValue(2)), + ) + assertThat(annotation.toAnnotationSpec().toString()).isEqualTo( + """ + @test.IntValueAnnotation(value = 2) + """.trimIndent(), + ) + } + + @Test fun longValue() { + val annotation = KmAnnotation( + "test/LongValueAnnotation", + mapOf("value" to LongValue(2L)), + ) + assertThat(annotation.toAnnotationSpec().toString()).isEqualTo( + """ + @test.LongValueAnnotation(value = 2L) + """.trimIndent(), + ) + } + + @Test fun floatValue() { + val annotation = KmAnnotation( + "test/FloatValueAnnotation", + mapOf("value" to FloatValue(2.0F)), + ) + assertThat(annotation.toAnnotationSpec().toString()).isEqualTo( + """ + @test.FloatValueAnnotation(value = 2.0F) + """.trimIndent(), + ) + } + + @Test fun doubleValue() { + val annotation = KmAnnotation( + "test/DoubleValueAnnotation", + mapOf("value" to DoubleValue(2.0)), + ) + assertThat(annotation.toAnnotationSpec().toString()).isEqualTo( + """ + @test.DoubleValueAnnotation(value = 2.0) + """.trimIndent(), + ) + } + + @Test fun booleanValue() { + val annotation = KmAnnotation( + "test/BooleanValueAnnotation", + mapOf("value" to BooleanValue(true)), + ) + assertThat(annotation.toAnnotationSpec().toString()).isEqualTo( + """ + @test.BooleanValueAnnotation(value = true) + """.trimIndent(), + ) + } + + @Test fun uByteValue() { + val annotation = KmAnnotation( + "test/UByteValueAnnotation", + mapOf("value" to UByteValue(2u)), + ) + assertThat(annotation.toAnnotationSpec().toString()).isEqualTo( + """ + @test.UByteValueAnnotation(value = 2u) + """.trimIndent(), + ) + } + + @Test fun uShortValue() { + val annotation = KmAnnotation( + "test/UShortValueAnnotation", + mapOf("value" to UShortValue(2u)), + ) + assertThat(annotation.toAnnotationSpec().toString()).isEqualTo( + """ + @test.UShortValueAnnotation(value = 2u) + """.trimIndent(), + ) + } + + @Test fun uIntValue() { + val annotation = KmAnnotation( + "test/UIntValueAnnotation", + mapOf("value" to UIntValue(2u)), + ) + assertThat(annotation.toAnnotationSpec().toString()).isEqualTo( + """ + @test.UIntValueAnnotation(value = 2u) + """.trimIndent(), + ) + } + + @Test fun uLongValue() { + val annotation = KmAnnotation( + "test/ULongValueAnnotation", + mapOf("value" to ULongValue(2u)), + ) + assertThat(annotation.toAnnotationSpec().toString()).isEqualTo( + """ + @test.ULongValueAnnotation(value = 2u) + """.trimIndent(), + ) + } + + @Test fun stringValue() { + val annotation = KmAnnotation( + "test/StringValueAnnotation", + mapOf("value" to StringValue("taco")), + ) + assertThat(annotation.toAnnotationSpec().toString()).isEqualTo( + """ + @test.StringValueAnnotation(value = "taco") + """.trimIndent(), + ) + } + + @Test fun kClassValue() { + val annotation = KmAnnotation( + "test/KClassValueAnnotation", + mapOf("value" to KClassValue("test/OtherClass", 0)), + ) + assertThat(annotation.toAnnotationSpec().toString()).isEqualTo( + """ + @test.KClassValueAnnotation(value = test.OtherClass::class) + """.trimIndent(), + ) + } + + @Test fun enumValue() { + val annotation = KmAnnotation( + "test/EnumValueAnnotation", + mapOf("value" to EnumValue("test/OtherClass", "VALUE")), + ) + assertThat(annotation.toAnnotationSpec().toString()).isEqualTo( + """ + @test.EnumValueAnnotation(value = test.OtherClass.VALUE) + """.trimIndent(), + ) + } + + @Test fun annotationValue() { + val annotation = KmAnnotation( + "test/AnnotationValueAnnotation", + mapOf( + "value" to AnnotationValue( + KmAnnotation("test/OtherAnnotation", mapOf("value" to StringValue("Hello!"))), + ), + ), + ) + assertThat(annotation.toAnnotationSpec().toString()).isEqualTo( + """ + @test.AnnotationValueAnnotation(value = test.OtherAnnotation(value = "Hello!")) + """.trimIndent(), + ) + } + + @Test fun arrayValue() { + val annotation = KmAnnotation( + "test/ArrayValueAnnotation", + mapOf("value" to ArrayValue(listOf(IntValue(1), IntValue(2), IntValue(3)))), + ) + assertThat(annotation.toAnnotationSpec().toString()).isEqualTo( + """ + @test.ArrayValueAnnotation(value = [1, 2, 3]) + """.trimIndent(), + ) + } +} diff --git a/interop/kotlinx-metadata/src/test/kotlin/com/squareup/kotlinpoet/metadata/specs/KotlinPoetMetadataSpecsTest.kt b/interop/kotlinx-metadata/src/test/kotlin/com/squareup/kotlinpoet/metadata/specs/KotlinPoetMetadataSpecsTest.kt new file mode 100644 index 00000000..9198dbcb --- /dev/null +++ b/interop/kotlinx-metadata/src/test/kotlin/com/squareup/kotlinpoet/metadata/specs/KotlinPoetMetadataSpecsTest.kt @@ -0,0 +1,2250 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:OptIn(KotlinPoetMetadataPreview::class) +@file:Suppress( + "DEPRECATION", + "NOTHING_TO_INLINE", + "RedundantSuspendModifier", + "RedundantUnitReturnType", + "RedundantVisibilityModifier", + "RemoveEmptyPrimaryConstructor", + "RemoveRedundantQualifierName", + "UNUSED_PARAMETER", + "unused", +) + +package com.squareup.kotlinpoet.metadata.specs + +import com.google.common.truth.Truth.assertThat +import com.squareup.kotlinpoet.LIST +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.STRING +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import com.squareup.kotlinpoet.metadata.specs.MultiClassInspectorTest.ClassInspectorType.ELEMENTS +import com.squareup.kotlinpoet.metadata.specs.MultiClassInspectorTest.ClassInspectorType.REFLECTIVE +import com.squareup.kotlinpoet.tag +import com.squareup.kotlinpoet.tags.TypeAliasTag +import kotlin.annotation.AnnotationRetention.RUNTIME +import kotlin.annotation.AnnotationTarget.TYPE +import kotlin.annotation.AnnotationTarget.TYPE_PARAMETER +import kotlin.properties.Delegates +import kotlin.test.fail +import kotlinx.metadata.KmClass +import kotlinx.metadata.KmConstructor +import kotlinx.metadata.KmFunction +import kotlinx.metadata.KmProperty +import kotlinx.metadata.KmTypeParameter +import kotlinx.metadata.KmValueParameter +import org.junit.Ignore +import org.junit.Test + +class KotlinPoetMetadataSpecsTest : MultiClassInspectorTest() { + + @Test + fun constructorData() { + val typeSpec = ConstructorClass::class.toTypeSpecWithTestHandler() + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class ConstructorClass( + public val foo: kotlin.String, + vararg bar: kotlin.Int, + ) { + public constructor(bar: kotlin.Int) + } + """.trimIndent(), + ) + } + + class ConstructorClass(val foo: String, vararg bar: Int) { + // Secondary constructors are ignored, so we expect this constructor to not be the one picked + // up in the test. + constructor(bar: Int) : this("defaultFoo") + } + + @Test + fun supertype() { + val typeSpec = Supertype::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class Supertype() : com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.BaseType(), com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.BaseInterface + """.trimIndent(), + ) + } + + abstract class BaseType + interface BaseInterface + class Supertype : BaseType(), BaseInterface + + @IgnoreForHandlerType( + reason = "Elements properly resolves the string constant", + handlerType = ELEMENTS, + ) + @Test + fun propertiesReflective() { + val typeSpec = Properties::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class Properties() { + public var aList: kotlin.collections.List<kotlin.Int> = throw NotImplementedError("Stub!") + + public val bar: kotlin.String? = null + + public var baz: kotlin.Int = throw NotImplementedError("Stub!") + + public val foo: kotlin.String = throw NotImplementedError("Stub!") + } + """.trimIndent(), + ) + } + + @IgnoreForHandlerType( + reason = "Elements properly resolves the string constant", + handlerType = REFLECTIVE, + ) + @Test + fun propertiesElements() { + val typeSpec = Properties::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class Properties() { + public var aList: kotlin.collections.List<kotlin.Int> = throw NotImplementedError("Stub!") + + public val bar: kotlin.String? = null + + public var baz: kotlin.Int = throw NotImplementedError("Stub!") + + public val foo: kotlin.String = throw NotImplementedError("Stub!") + } + """.trimIndent(), + ) + } + + class Properties { + val foo: String = "" + val bar: String? = null + var baz: Int = 0 + var aList: List<Int> = emptyList() + } + + @Test + fun companionObject() { + val typeSpec = CompanionObject::class.toTypeSpecWithTestHandler() + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class CompanionObject() { + public companion object + } + """.trimIndent(), + ) + } + + class CompanionObject { + companion object + } + + @Test + fun namedCompanionObject() { + val typeSpec = NamedCompanionObject::class.toTypeSpecWithTestHandler() + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class NamedCompanionObject() { + public companion object Named + } + """.trimIndent(), + ) + } + + class NamedCompanionObject { + companion object Named + } + + @Test + fun generics() { + val typeSpec = Generics::class.toTypeSpecWithTestHandler() + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class Generics<out T, in R, V>( + public val genericInput: T, + ) + """.trimIndent(), + ) + } + + class Generics<out T, in R, V>(val genericInput: T) + + @Test + fun typeAliases() { + val typeSpec = TypeAliases::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class TypeAliases( + public val foo: com.squareup.kotlinpoet.metadata.specs.TypeAliasName, + public val bar: com.squareup.kotlinpoet.metadata.specs.GenericTypeAlias, + ) + """.trimIndent(), + ) + + val fooPropertyType = typeSpec.propertySpecs.first { it.name == "foo" }.type + val fooAliasData = fooPropertyType.tag<TypeAliasTag>() + checkNotNull(fooAliasData) + assertThat(fooAliasData.abbreviatedType).isEqualTo(STRING) + + val barPropertyType = typeSpec.propertySpecs.first { it.name == "bar" }.type + val barAliasData = barPropertyType.tag<TypeAliasTag>() + checkNotNull(barAliasData) + assertThat(barAliasData.abbreviatedType).isEqualTo(LIST.parameterizedBy(STRING)) + } + + class TypeAliases(val foo: TypeAliasName, val bar: GenericTypeAlias) + + @Test + fun propertyMutability() { + val typeSpec = PropertyMutability::class.toTypeSpecWithTestHandler() + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class PropertyMutability( + public val foo: kotlin.String, + public var mutableFoo: kotlin.String, + ) + """.trimIndent(), + ) + } + + class PropertyMutability(val foo: String, var mutableFoo: String) + + @Test + fun collectionMutability() { + val typeSpec = CollectionMutability::class.toTypeSpecWithTestHandler() + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class CollectionMutability( + public val immutableList: kotlin.collections.List<kotlin.String>, + public val mutableList: kotlin.collections.MutableList<kotlin.String>, + ) + """.trimIndent(), + ) + } + + class CollectionMutability(val immutableList: List<String>, val mutableList: MutableList<String>) + + @Test + fun suspendTypes() { + val typeSpec = SuspendTypes::class.toTypeSpecWithTestHandler() + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class SuspendTypes() { + public val testProp: suspend (kotlin.Int, kotlin.Long) -> kotlin.String = throw NotImplementedError("Stub!") + + public suspend fun testComplexSuspendFun(body: suspend (kotlin.Int, suspend (kotlin.Long) -> kotlin.String) -> kotlin.String): kotlin.Unit { + } + + public fun testFun(body: suspend (kotlin.Int, kotlin.Long) -> kotlin.String): kotlin.Unit { + } + + public suspend fun testSuspendFun(param1: kotlin.String): kotlin.Unit { + } + } + """.trimIndent(), + ) + } + + class SuspendTypes { + val testProp: suspend (Int, Long) -> String = { _, _ -> "" } + + fun testFun(body: suspend (Int, Long) -> String) { + } + + suspend fun testSuspendFun(param1: String) { + } + + suspend fun testComplexSuspendFun(body: suspend (Int, suspend (Long) -> String) -> String) { + } + } + + @Test + fun parameters() { + val typeSpec = Parameters::class.toTypeSpecWithTestHandler() + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class Parameters() { + public inline fun hasDefault(param1: kotlin.String = throw NotImplementedError("Stub!")): kotlin.Unit { + } + + public inline fun `inline`(crossinline param1: () -> kotlin.String): kotlin.Unit { + } + + public inline fun `noinline`(noinline param1: () -> kotlin.String): kotlin.String = throw NotImplementedError("Stub!") + } + """.trimIndent(), + ) + } + + class Parameters { + inline fun inline(crossinline param1: () -> String) { + } + + inline fun noinline(noinline param1: () -> String): String { + return "" + } + + inline fun hasDefault(param1: String = "Nope") { + } + } + + @Test + fun lambdaReceiver() { + val typeSpec = LambdaReceiver::class.toTypeSpecWithTestHandler() + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class LambdaReceiver() { + public fun lambdaReceiver(block: kotlin.String.() -> kotlin.Unit): kotlin.Unit { + } + + public fun lambdaReceiver2(block: kotlin.String.(kotlin.Int) -> kotlin.Unit): kotlin.Unit { + } + + public fun lambdaReceiver3(block: kotlin.String.(kotlin.Int, kotlin.String) -> kotlin.Unit): kotlin.Unit { + } + } + """.trimIndent(), + ) + } + + class LambdaReceiver { + fun lambdaReceiver(block: String.() -> Unit) { + } + fun lambdaReceiver2(block: String.(Int) -> Unit) { + } + fun lambdaReceiver3(block: String.(Int, String) -> Unit) { + } + } + + @Test + fun nestedTypeAlias() { + val typeSpec = NestedTypeAliasTest::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class NestedTypeAliasTest() { + public val prop: com.squareup.kotlinpoet.metadata.specs.NestedTypeAlias = throw NotImplementedError("Stub!") + } + """.trimIndent(), + ) + } + + class NestedTypeAliasTest { + val prop: NestedTypeAlias = listOf(listOf("")) + } + + @Test + fun inlineClass() { + val typeSpec = InlineClass::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + @kotlin.jvm.JvmInline + public value class InlineClass( + public val `value`: kotlin.String, + ) + """.trimIndent(), + ) + } + + @Test + fun valueClass() { + val typeSpec = ValueClass::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + @kotlin.jvm.JvmInline + public value class ValueClass( + public val `value`: kotlin.String, + ) + """.trimIndent(), + ) + } + + @Test + fun functionReferencingTypeParam() { + val typeSpec = FunctionsReferencingTypeParameters::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class FunctionsReferencingTypeParameters<T>() { + public fun test(`param`: T): kotlin.Unit { + } + } + """.trimIndent(), + ) + } + + class FunctionsReferencingTypeParameters<T> { + fun test(param: T) { + } + } + + @Test + fun overriddenThings() { + val typeSpec = OverriddenThings::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public abstract class OverriddenThings() : com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.OverriddenThingsBase(), com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.OverriddenThingsInterface { + public override var openProp: kotlin.String = throw NotImplementedError("Stub!") + + public override var openPropInterface: kotlin.String = throw NotImplementedError("Stub!") + + public override fun openFunction(): kotlin.Unit { + } + + public override fun openFunctionInterface(): kotlin.Unit { + } + } + """.trimIndent(), + ) + } + + abstract class OverriddenThingsBase { + abstract var openProp: String + + abstract fun openFunction() + } + + interface OverriddenThingsInterface { + var openPropInterface: String + + fun openFunctionInterface() + } + + abstract class OverriddenThings : OverriddenThingsBase(), OverriddenThingsInterface { + override var openProp: String = "" + override var openPropInterface: String = "" + + override fun openFunction() { + } + + override fun openFunctionInterface() { + } + } + + @Test + fun delegatedProperties() { + val typeSpec = DelegatedProperties::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class DelegatedProperties() { + /** + * Note: delegation is ABI stub only and not guaranteed to match source code. + */ + public val immutable: kotlin.String by kotlin.lazy { throw NotImplementedError("Stub!") } + + /** + * Note: delegation is ABI stub only and not guaranteed to match source code. + */ + public val immutableNullable: kotlin.String? by kotlin.lazy { throw NotImplementedError("Stub!") } + + /** + * Note: delegation is ABI stub only and not guaranteed to match source code. + */ + public var mutable: kotlin.String by kotlin.properties.Delegates.notNull() + + /** + * Note: delegation is ABI stub only and not guaranteed to match source code. + */ + public var mutableNullable: kotlin.String? by kotlin.properties.Delegates.observable(null) { _, _, _ -> } + } + """.trimIndent(), + ) + } + + class DelegatedProperties { + val immutable: String by lazy { "" } + val immutableNullable: String? by lazy { "" } + var mutable: String by Delegates.notNull() + var mutableNullable: String? by Delegates.observable(null) { _, _, _ -> } + } + + @Ignore("Need to be able to know about class delegation in metadata") + @Test + fun classDelegation() { + val typeSpec = ClassDelegation::class.toTypeSpecWithTestHandler() + + // TODO Assert this also excludes functions handled by the delegate + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class ClassDelegation<T>( + delegate: List<T> + ): List<T> by delegate + """.trimIndent(), + ) + } + + class ClassDelegation<T>(delegate: List<T>) : List<T> by delegate + + @Test + fun simpleEnum() { + val typeSpec = SimpleEnum::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public enum class SimpleEnum() { + FOO, + BAR, + BAZ, + } + """.trimIndent(), + ) + } + + enum class SimpleEnum { + FOO, BAR, BAZ + } + + @Test + fun complexEnum() { + val typeSpec = ComplexEnum::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public enum class ComplexEnum( + public val `value`: kotlin.String, + ) { + FOO { + public override fun toString(): kotlin.String = throw NotImplementedError("Stub!") + }, + BAR { + public override fun toString(): kotlin.String = throw NotImplementedError("Stub!") + }, + BAZ { + public override fun toString(): kotlin.String = throw NotImplementedError("Stub!") + }, + ; + } + """.trimIndent(), + ) + } + + enum class ComplexEnum(val value: String) { + FOO("foo") { + override fun toString(): String { + return "foo1" + } + }, + BAR("bar") { + override fun toString(): String { + return "bar1" + } + }, + BAZ("baz") { + override fun toString(): String { + return "baz1" + } + }, + } + + @Test + fun enumWithAnnotation() { + val typeSpec = EnumWithAnnotation::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public enum class EnumWithAnnotation() { + FOO, + @com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.FieldAnnotation + BAR, + BAZ, + } + """.trimIndent(), + ) + } + + enum class EnumWithAnnotation { + FOO, @FieldAnnotation + BAR, BAZ + } + + @Test + fun complexEnumWithAnnotation() { + val typeSpec = ComplexEnumWithAnnotation::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public enum class ComplexEnumWithAnnotation( + public val `value`: kotlin.String, + ) { + FOO { + public override fun toString(): kotlin.String = throw NotImplementedError("Stub!") + }, + @com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.FieldAnnotation + BAR { + public override fun toString(): kotlin.String = throw NotImplementedError("Stub!") + }, + BAZ { + public override fun toString(): kotlin.String = throw NotImplementedError("Stub!") + }, + ; + } + """.trimIndent(), + ) + } + + enum class ComplexEnumWithAnnotation(val value: String) { + FOO("foo") { + override fun toString(): String { + return "foo1" + } + }, + + @FieldAnnotation + BAR("bar") { + override fun toString(): String { + return "bar1" + } + }, + BAZ("baz") { + override fun toString(): String { + return "baz1" + } + }, + } + + @Test + fun interfaces() { + val testInterfaceSpec = TestInterface::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(testInterfaceSpec.trimmedToString()).isEqualTo( + """ + public interface TestInterface { + public fun complex(input: kotlin.String, input2: kotlin.String = throw NotImplementedError("Stub!")): kotlin.String = throw NotImplementedError("Stub!") + + public fun hasDefault(): kotlin.Unit { + } + + public fun hasDefaultMultiParam(input: kotlin.String, input2: kotlin.String): kotlin.String = throw NotImplementedError("Stub!") + + public fun hasDefaultSingleParam(input: kotlin.String): kotlin.String = throw NotImplementedError("Stub!") + + @kotlin.jvm.JvmDefault + public fun hasJvmDefault(): kotlin.Unit { + } + + public fun noDefault(): kotlin.Unit + + public fun noDefaultWithInput(input: kotlin.String): kotlin.Unit + + public fun noDefaultWithInputDefault(input: kotlin.String = throw NotImplementedError("Stub!")): kotlin.Unit + } + """.trimIndent(), + ) + + val subInterfaceSpec = SubInterface::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(subInterfaceSpec.trimmedToString()).isEqualTo( + """ + public interface SubInterface : com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.TestInterface { + public override fun hasDefault(): kotlin.Unit { + } + + @kotlin.jvm.JvmDefault + public override fun hasJvmDefault(): kotlin.Unit { + } + + public fun subInterfaceFunction(): kotlin.Unit { + } + } + """.trimIndent(), + ) + + val implSpec = TestSubInterfaceImpl::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(implSpec.trimmedToString()).isEqualTo( + """ + public class TestSubInterfaceImpl() : com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.SubInterface { + public override fun noDefault(): kotlin.Unit { + } + + public override fun noDefaultWithInput(input: kotlin.String): kotlin.Unit { + } + + public override fun noDefaultWithInputDefault(input: kotlin.String): kotlin.Unit { + } + } + """.trimIndent(), + ) + } + + interface TestInterface { + + fun noDefault() + + fun noDefaultWithInput(input: String) + + fun noDefaultWithInputDefault(input: String = "") + + @JvmDefault + fun hasJvmDefault() { + } + + fun hasDefault() { + } + + fun hasDefaultSingleParam(input: String): String { + return "1234" + } + + fun hasDefaultMultiParam(input: String, input2: String): String { + return "1234" + } + + fun complex(input: String, input2: String = ""): String { + return "5678" + } + } + + interface SubInterface : TestInterface { + fun subInterfaceFunction() { + } + + @JvmDefault + override fun hasJvmDefault() { + super.hasJvmDefault() + } + + override fun hasDefault() { + super.hasDefault() + } + } + + class TestSubInterfaceImpl : SubInterface { + override fun noDefault() { + } + + override fun noDefaultWithInput(input: String) { + } + + override fun noDefaultWithInputDefault(input: String) { + } + } + + @Test + fun backwardReferencingTypeVars() { + val typeSpec = BackwardReferencingTypeVars::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public interface BackwardReferencingTypeVars<T> : kotlin.collections.List<kotlin.collections.Set<T>> + """.trimIndent(), + ) + } + + interface BackwardReferencingTypeVars<T> : List<Set<T>> + + @Test + fun taggedTypes() { + val typeSpec = TaggedTypes::class.toTypeSpecWithTestHandler() + assertThat(typeSpec.tag<KmClass>()).isNotNull() + + val constructorSpec = typeSpec.primaryConstructor ?: fail("No constructor found!") + assertThat(constructorSpec.tag<KmConstructor>()).isNotNull() + + val parameterSpec = constructorSpec.parameters[0] + assertThat(parameterSpec.tag<KmValueParameter>()).isNotNull() + + val typeVar = typeSpec.typeVariables[0] + assertThat(typeVar.tag<KmTypeParameter>()).isNotNull() + + val funSpec = typeSpec.funSpecs[0] + assertThat(funSpec.tag<KmFunction>()).isNotNull() + + val propertySpec = typeSpec.propertySpecs[0] + assertThat(propertySpec.tag<KmProperty>()).isNotNull() + } + + class TaggedTypes<T>(val param: T) { + val property: String = "" + + fun function() { + } + } + + @Test + fun annotations() { + val typeSpec = MyAnnotation::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public annotation class MyAnnotation( + public val `value`: kotlin.String, + ) + """.trimIndent(), + ) + } + + annotation class MyAnnotation(val value: String) + + @Test + fun functionTypeArgsSupersedeClass() { + val typeSpec = GenericClass::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class GenericClass<T>() { + public fun <T> functionAlsoWithT(`param`: T): kotlin.Unit { + } + + public fun <R> functionWithADifferentType(`param`: R): kotlin.Unit { + } + + public fun functionWithT(`param`: T): kotlin.Unit { + } + + /** + * Note: Since this is a synthetic function, some JVM information (annotations, modifiers) may be missing. + */ + public inline fun <reified T> `reified`(`param`: T): kotlin.Unit { + } + } + """.trimIndent(), + ) + + val func1TypeVar = typeSpec.funSpecs.find { it.name == "functionAlsoWithT" }!!.typeVariables.first() + val classTypeVar = typeSpec.typeVariables.first() + + assertThat(func1TypeVar).isNotSameInstanceAs(classTypeVar) + } + + class GenericClass<T> { + fun functionWithT(param: T) { + } + fun <T> functionAlsoWithT(param: T) { + } + fun <R> functionWithADifferentType(param: R) { + } + + // Regression for https://github.com/square/kotlinpoet/issues/829 + inline fun <reified T> reified(param: T) { + } + } + + @Test + fun complexCompanionObject() { + val typeSpec = ComplexCompanionObject::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class ComplexCompanionObject() { + public companion object ComplexObject : com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.CompanionBase(), com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.CompanionInterface + } + """.trimIndent(), + ) + } + + interface CompanionInterface + open class CompanionBase + + class ComplexCompanionObject { + companion object ComplexObject : CompanionBase(), CompanionInterface + } + + @IgnoreForHandlerType( + reason = "TODO Synthetic methods that hold annotations aren't visible in these tests", + handlerType = ELEMENTS, + ) + @Test + fun annotationsAreCopied() { + val typeSpec = AnnotationHolders::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class AnnotationHolders @com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.ConstructorAnnotation constructor() { + @field:com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.FieldAnnotation + public var `field`: kotlin.String? = null + + @get:com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.GetterAnnotation + public var getter: kotlin.String? = null + + @com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.HolderAnnotation + @kotlin.jvm.JvmField + public var holder: kotlin.String? = null + + @set:com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.SetterAnnotation + public var setter: kotlin.String? = null + + @com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.ConstructorAnnotation + public constructor(`value`: kotlin.String) + + @com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.FunctionAnnotation + public fun function(): kotlin.Unit { + } + } + """.trimIndent(), + ) + } + + class AnnotationHolders @ConstructorAnnotation constructor() { + + @ConstructorAnnotation + constructor(value: String) : this() + + @field:FieldAnnotation + var field: String? = null + + @get:GetterAnnotation + var getter: String? = null + + @set:SetterAnnotation + var setter: String? = null + + @HolderAnnotation + @JvmField + var holder: String? = null + + @FunctionAnnotation + fun function() { + } + } + + @Retention(RUNTIME) + annotation class ConstructorAnnotation + + @Retention(RUNTIME) + annotation class FieldAnnotation + + @Retention(RUNTIME) + annotation class GetterAnnotation + + @Retention(RUNTIME) + annotation class SetterAnnotation + + @Retention(RUNTIME) + annotation class HolderAnnotation + + @Retention(RUNTIME) + annotation class FunctionAnnotation + + @IgnoreForHandlerType( + reason = "Elements properly resolves the regular properties + JvmStatic, but reflection will not", + handlerType = REFLECTIVE, + ) + @Test + fun constantValuesElements() { + val typeSpec = Constants::class.toTypeSpecWithTestHandler() + + // Note: formats like hex/binary/underscore are not available as formatted at runtime + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class Constants( + public val `param`: kotlin.String = throw NotImplementedError("Stub!"), + ) { + public val binaryProp: kotlin.Int = throw NotImplementedError("Stub!") + + public val boolProp: kotlin.Boolean = throw NotImplementedError("Stub!") + + public val doubleProp: kotlin.Double = throw NotImplementedError("Stub!") + + public val floatProp: kotlin.Float = throw NotImplementedError("Stub!") + + public val hexProp: kotlin.Int = throw NotImplementedError("Stub!") + + public val intProp: kotlin.Int = throw NotImplementedError("Stub!") + + public val longProp: kotlin.Long = throw NotImplementedError("Stub!") + + public val stringProp: kotlin.String = throw NotImplementedError("Stub!") + + public val underscoresHexProp: kotlin.Long = throw NotImplementedError("Stub!") + + public val underscoresProp: kotlin.Int = throw NotImplementedError("Stub!") + + public companion object { + public const val CONST_BINARY_PROP: kotlin.Int = 11 + + public const val CONST_BOOL_PROP: kotlin.Boolean = false + + public const val CONST_DOUBLE_PROP: kotlin.Double = 1.0 + + public const val CONST_FLOAT_PROP: kotlin.Float = 1.0F + + public const val CONST_HEX_PROP: kotlin.Int = 15 + + public const val CONST_INT_PROP: kotlin.Int = 1 + + public const val CONST_LONG_PROP: kotlin.Long = 1L + + public const val CONST_STRING_PROP: kotlin.String = "prop" + + public const val CONST_UNDERSCORES_HEX_PROP: kotlin.Long = 4_293_713_502L + + public const val CONST_UNDERSCORES_PROP: kotlin.Int = 1_000_000 + + @kotlin.jvm.JvmStatic + public val STATIC_CONST_BINARY_PROP: kotlin.Int = throw NotImplementedError("Stub!") + + @kotlin.jvm.JvmStatic + public val STATIC_CONST_BOOL_PROP: kotlin.Boolean = throw NotImplementedError("Stub!") + + @kotlin.jvm.JvmStatic + public val STATIC_CONST_DOUBLE_PROP: kotlin.Double = throw NotImplementedError("Stub!") + + @kotlin.jvm.JvmStatic + public val STATIC_CONST_FLOAT_PROP: kotlin.Float = throw NotImplementedError("Stub!") + + @kotlin.jvm.JvmStatic + public val STATIC_CONST_HEX_PROP: kotlin.Int = throw NotImplementedError("Stub!") + + @kotlin.jvm.JvmStatic + public val STATIC_CONST_INT_PROP: kotlin.Int = throw NotImplementedError("Stub!") + + @kotlin.jvm.JvmStatic + public val STATIC_CONST_LONG_PROP: kotlin.Long = throw NotImplementedError("Stub!") + + @kotlin.jvm.JvmStatic + public val STATIC_CONST_STRING_PROP: kotlin.String = throw NotImplementedError("Stub!") + + @kotlin.jvm.JvmStatic + public val STATIC_CONST_UNDERSCORES_HEX_PROP: kotlin.Long = throw NotImplementedError("Stub!") + + @kotlin.jvm.JvmStatic + public val STATIC_CONST_UNDERSCORES_PROP: kotlin.Int = throw NotImplementedError("Stub!") + } + } + """.trimIndent(), + ) + + // TODO check with objects + } + + @IgnoreForHandlerType( + reason = "Elements properly resolves the regular properties + JvmStatic, but reflection will not", + handlerType = ELEMENTS, + ) + @Test + fun constantValuesReflective() { + val typeSpec = Constants::class.toTypeSpecWithTestHandler() + + // Note: formats like hex/binary/underscore are not available as formatted in elements + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class Constants( + public val `param`: kotlin.String = throw NotImplementedError("Stub!"), + ) { + public val binaryProp: kotlin.Int = throw NotImplementedError("Stub!") + + public val boolProp: kotlin.Boolean = throw NotImplementedError("Stub!") + + public val doubleProp: kotlin.Double = throw NotImplementedError("Stub!") + + public val floatProp: kotlin.Float = throw NotImplementedError("Stub!") + + public val hexProp: kotlin.Int = throw NotImplementedError("Stub!") + + public val intProp: kotlin.Int = throw NotImplementedError("Stub!") + + public val longProp: kotlin.Long = throw NotImplementedError("Stub!") + + public val stringProp: kotlin.String = throw NotImplementedError("Stub!") + + public val underscoresHexProp: kotlin.Long = throw NotImplementedError("Stub!") + + public val underscoresProp: kotlin.Int = throw NotImplementedError("Stub!") + + public companion object { + public const val CONST_BINARY_PROP: kotlin.Int = 11 + + public const val CONST_BOOL_PROP: kotlin.Boolean = false + + public const val CONST_DOUBLE_PROP: kotlin.Double = 1.0 + + public const val CONST_FLOAT_PROP: kotlin.Float = 1.0F + + public const val CONST_HEX_PROP: kotlin.Int = 15 + + public const val CONST_INT_PROP: kotlin.Int = 1 + + public const val CONST_LONG_PROP: kotlin.Long = 1L + + public const val CONST_STRING_PROP: kotlin.String = "prop" + + public const val CONST_UNDERSCORES_HEX_PROP: kotlin.Long = 4_293_713_502L + + public const val CONST_UNDERSCORES_PROP: kotlin.Int = 1_000_000 + + @kotlin.jvm.JvmStatic + public val STATIC_CONST_BINARY_PROP: kotlin.Int = 11 + + @kotlin.jvm.JvmStatic + public val STATIC_CONST_BOOL_PROP: kotlin.Boolean = false + + @kotlin.jvm.JvmStatic + public val STATIC_CONST_DOUBLE_PROP: kotlin.Double = 1.0 + + @kotlin.jvm.JvmStatic + public val STATIC_CONST_FLOAT_PROP: kotlin.Float = 1.0F + + @kotlin.jvm.JvmStatic + public val STATIC_CONST_HEX_PROP: kotlin.Int = 15 + + @kotlin.jvm.JvmStatic + public val STATIC_CONST_INT_PROP: kotlin.Int = 1 + + @kotlin.jvm.JvmStatic + public val STATIC_CONST_LONG_PROP: kotlin.Long = 1L + + @kotlin.jvm.JvmStatic + public val STATIC_CONST_STRING_PROP: kotlin.String = "prop" + + @kotlin.jvm.JvmStatic + public val STATIC_CONST_UNDERSCORES_HEX_PROP: kotlin.Long = 4_293_713_502L + + @kotlin.jvm.JvmStatic + public val STATIC_CONST_UNDERSCORES_PROP: kotlin.Int = 1_000_000 + } + } + """.trimIndent(), + ) + } + + class Constants( + val param: String = "param", + ) { + val boolProp = false + val binaryProp = 0b00001011 + val intProp = 1 + val underscoresProp = 1_000_000 + val hexProp = 0x0F + val underscoresHexProp = 0xFF_EC_DE_5E + val longProp = 1L + val floatProp = 1.0F + val doubleProp = 1.0 + val stringProp = "prop" + + companion object { + @JvmStatic val STATIC_CONST_BOOL_PROP = false + + @JvmStatic val STATIC_CONST_BINARY_PROP = 0b00001011 + + @JvmStatic val STATIC_CONST_INT_PROP = 1 + + @JvmStatic val STATIC_CONST_UNDERSCORES_PROP = 1_000_000 + + @JvmStatic val STATIC_CONST_HEX_PROP = 0x0F + + @JvmStatic val STATIC_CONST_UNDERSCORES_HEX_PROP = 0xFF_EC_DE_5E + + @JvmStatic val STATIC_CONST_LONG_PROP = 1L + + @JvmStatic val STATIC_CONST_FLOAT_PROP = 1.0f + + @JvmStatic val STATIC_CONST_DOUBLE_PROP = 1.0 + + @JvmStatic val STATIC_CONST_STRING_PROP = "prop" + + const val CONST_BOOL_PROP = false + const val CONST_BINARY_PROP = 0b00001011 + const val CONST_INT_PROP = 1 + const val CONST_UNDERSCORES_PROP = 1_000_000 + const val CONST_HEX_PROP = 0x0F + const val CONST_UNDERSCORES_HEX_PROP = 0xFF_EC_DE_5E + const val CONST_LONG_PROP = 1L + const val CONST_FLOAT_PROP = 1.0f + const val CONST_DOUBLE_PROP = 1.0 + const val CONST_STRING_PROP = "prop" + } + } + + @Test + fun jvmAnnotations() { + val typeSpec = JvmAnnotations::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class JvmAnnotations() { + @get:kotlin.jvm.Synchronized + public val synchronizedGetProp: kotlin.String? = null + + @set:kotlin.jvm.Synchronized + public var synchronizedSetProp: kotlin.String? = null + + @kotlin.jvm.Transient + public val transientProp: kotlin.String? = null + + @kotlin.jvm.Volatile + public var volatileProp: kotlin.String? = null + + @kotlin.jvm.Synchronized + public fun synchronizedFun(): kotlin.Unit { + } + } + """.trimIndent(), + ) + + val interfaceSpec = JvmAnnotationsInterface::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(interfaceSpec.trimmedToString()).isEqualTo( + """ + public interface JvmAnnotationsInterface { + @kotlin.jvm.JvmDefault + public fun defaultMethod(): kotlin.Unit { + } + + public fun notDefaultMethod(): kotlin.Unit + } + """.trimIndent(), + ) + } + + class JvmAnnotations { + @Transient val transientProp: String? = null + + @Volatile var volatileProp: String? = null + + @get:Synchronized val synchronizedGetProp: String? = null + + @set:Synchronized var synchronizedSetProp: String? = null + + @Synchronized + fun synchronizedFun() { + } + } + + interface JvmAnnotationsInterface { + @JvmDefault + fun defaultMethod() { + } + fun notDefaultMethod() + } + + @Test + fun nestedClasses() { + val typeSpec = NestedClasses::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class NestedClasses() { + public abstract class NestedClass<T>() : kotlin.collections.List<T> + + public inner class NestedInnerClass() + } + """.trimIndent(), + ) + } + + class NestedClasses { + abstract class NestedClass<T> : List<T> + inner class NestedInnerClass + } + + @IgnoreForHandlerType( + reason = "Reflection properly resolves companion properties + JvmStatic + JvmName, but " + + "elements will not", + handlerType = ELEMENTS, + ) + @Test + fun jvmNamesReflective() { + val typeSpec = JvmNameData::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class JvmNameData( + @get:kotlin.jvm.JvmName(name = "jvmParam") + public val `param`: kotlin.String, + ) { + @get:kotlin.jvm.JvmName(name = "jvmPropertyGet") + public val propertyGet: kotlin.String? = null + + @get:kotlin.jvm.JvmName(name = "jvmPropertyGetAndSet") + @set:kotlin.jvm.JvmName(name = "jvmPropertyGetAndSet") + public var propertyGetAndSet: kotlin.String? = null + + @set:kotlin.jvm.JvmName(name = "jvmPropertySet") + public var propertySet: kotlin.String? = null + + @kotlin.jvm.JvmName(name = "jvmFunction") + public fun function(): kotlin.Unit { + } + + public interface InterfaceWithJvmName { + public companion object { + @get:kotlin.jvm.JvmName(name = "fooBoolJvm") + @kotlin.jvm.JvmStatic + public val FOO_BOOL: kotlin.Boolean = false + + @kotlin.jvm.JvmName(name = "jvmStaticFunction") + @kotlin.jvm.JvmStatic + public fun staticFunction(): kotlin.Unit { + } + } + } + } + """.trimIndent(), + ) + } + + @IgnoreForHandlerType( + reason = "Reflection properly resolves companion properties + JvmStatic + JvmName, but " + + "elements will not", + handlerType = REFLECTIVE, + ) + @Test + fun jvmNamesElements() { + val typeSpec = JvmNameData::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class JvmNameData( + @get:kotlin.jvm.JvmName(name = "jvmParam") + public val `param`: kotlin.String, + ) { + @get:kotlin.jvm.JvmName(name = "jvmPropertyGet") + public val propertyGet: kotlin.String? = null + + @get:kotlin.jvm.JvmName(name = "jvmPropertyGetAndSet") + @set:kotlin.jvm.JvmName(name = "jvmPropertyGetAndSet") + public var propertyGetAndSet: kotlin.String? = null + + @set:kotlin.jvm.JvmName(name = "jvmPropertySet") + public var propertySet: kotlin.String? = null + + @kotlin.jvm.JvmName(name = "jvmFunction") + public fun function(): kotlin.Unit { + } + + public interface InterfaceWithJvmName { + public companion object { + @get:kotlin.jvm.JvmName(name = "fooBoolJvm") + @kotlin.jvm.JvmStatic + public val FOO_BOOL: kotlin.Boolean = throw NotImplementedError("Stub!") + + @kotlin.jvm.JvmName(name = "jvmStaticFunction") + @kotlin.jvm.JvmStatic + public fun staticFunction(): kotlin.Unit { + } + } + } + } + """.trimIndent(), + ) + } + + class JvmNameData( + @get:JvmName("jvmParam") val param: String, + ) { + + @get:JvmName("jvmPropertyGet") + val propertyGet: String? = null + + @set:JvmName("jvmPropertySet") + var propertySet: String? = null + + @set:JvmName("jvmPropertyGetAndSet") + @get:JvmName("jvmPropertyGetAndSet") + var propertyGetAndSet: String? = null + + @JvmName("jvmFunction") + fun function() { + } + + // Interfaces can't have JvmName, but covering a potential edge case of having a companion + // object with JvmName elements. Also covers an edge case where constants have getters + interface InterfaceWithJvmName { + companion object { + @JvmStatic + @get:JvmName("fooBoolJvm") + val FOO_BOOL = false + + @JvmName("jvmStaticFunction") + @JvmStatic + fun staticFunction() { + } + } + } + } + + @IgnoreForHandlerType( + reason = "JvmOverloads is not runtime retained and thus not visible to reflection.", + handlerType = REFLECTIVE, + ) + @Test + fun overloads() { + val typeSpec = Overloads::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class Overloads @kotlin.jvm.JvmOverloads constructor( + public val param1: kotlin.String, + public val optionalParam2: kotlin.String = throw NotImplementedError("Stub!"), + public val nullableParam3: kotlin.String? = throw NotImplementedError("Stub!"), + ) { + @kotlin.jvm.JvmOverloads + public fun testFunction( + param1: kotlin.String, + optionalParam2: kotlin.String = throw NotImplementedError("Stub!"), + nullableParam3: kotlin.String? = throw NotImplementedError("Stub!"), + ): kotlin.Unit { + } + } + """.trimIndent(), + ) + } + + class Overloads @JvmOverloads constructor( + val param1: String, + val optionalParam2: String = "", + val nullableParam3: String? = null, + ) { + @JvmOverloads + fun testFunction( + param1: String, + optionalParam2: String = "", + nullableParam3: String? = null, + ) { + } + } + + @IgnoreForHandlerType( + reason = "Elements generates initializer values.", + handlerType = ELEMENTS, + ) + @Test + fun jvmFields_reflective() { + val typeSpec = Fields::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class Fields( + @property:kotlin.jvm.JvmField + public val param1: kotlin.String, + ) { + @kotlin.jvm.JvmField + public val fieldProp: kotlin.String = throw NotImplementedError("Stub!") + + public companion object { + @kotlin.jvm.JvmField + public val companionProp: kotlin.String = "" + + public const val constCompanionProp: kotlin.String = "" + + @kotlin.jvm.JvmStatic + public val staticCompanionProp: kotlin.String = "" + } + } + """.trimIndent(), + ) + } + + @IgnoreForHandlerType( + reason = "Elements generates initializer values.", + handlerType = REFLECTIVE, + ) + @Test + fun jvmFields_elements() { + val typeSpec = Fields::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class Fields( + @property:kotlin.jvm.JvmField + public val param1: kotlin.String, + ) { + @kotlin.jvm.JvmField + public val fieldProp: kotlin.String = throw NotImplementedError("Stub!") + + public companion object { + @kotlin.jvm.JvmField + public val companionProp: kotlin.String = throw NotImplementedError("Stub!") + + public const val constCompanionProp: kotlin.String = "" + + @kotlin.jvm.JvmStatic + public val staticCompanionProp: kotlin.String = throw NotImplementedError("Stub!") + } + } + """.trimIndent(), + ) + } + + class Fields( + @JvmField val param1: String, + ) { + @JvmField val fieldProp: String = "" + + companion object { + @JvmField val companionProp: String = "" + + @JvmStatic val staticCompanionProp: String = "" + const val constCompanionProp: String = "" + } + } + + @IgnoreForHandlerType( + reason = "Synthetic constructs aren't available in elements, so some information like " + + "JvmStatic can't be deduced.", + handlerType = ELEMENTS, + ) + @Test + fun synthetics_reflective() { + val typeSpec = Synthetics::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class Synthetics( + @get:kotlin.jvm.JvmSynthetic + public val `param`: kotlin.String, + ) { + @field:kotlin.jvm.JvmSynthetic + public val fieldProperty: kotlin.String? = null + + @field:kotlin.jvm.JvmSynthetic + public val `property`: kotlin.String? = null + + @get:kotlin.jvm.JvmSynthetic + public val propertyGet: kotlin.String? = null + + @get:kotlin.jvm.JvmSynthetic + @set:kotlin.jvm.JvmSynthetic + public var propertyGetAndSet: kotlin.String? = null + + @set:kotlin.jvm.JvmSynthetic + public var propertySet: kotlin.String? = null + + /** + * Note: Since this is a synthetic function, some JVM information (annotations, modifiers) may be missing. + */ + @kotlin.jvm.JvmSynthetic + public fun function(): kotlin.Unit { + } + + public interface InterfaceWithJvmName { + /** + * Note: Since this is a synthetic function, some JVM information (annotations, modifiers) may be missing. + */ + @kotlin.jvm.JvmSynthetic + public fun interfaceFunction(): kotlin.Unit + + public companion object { + @get:kotlin.jvm.JvmSynthetic + @kotlin.jvm.JvmStatic + public val FOO_BOOL: kotlin.Boolean = false + + /** + * Note: Since this is a synthetic function, some JVM information (annotations, modifiers) may be missing. + */ + @kotlin.jvm.JvmStatic + @kotlin.jvm.JvmSynthetic + public fun staticFunction(): kotlin.Unit { + } + } + } + } + """.trimIndent(), + ) + } + + @IgnoreForHandlerType( + reason = "Synthetic constructs aren't available in elements, so some information like " + + "JvmStatic can't be deduced.", + handlerType = REFLECTIVE, + ) + @Test + fun synthetics_elements() { + val typeSpec = Synthetics::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class Synthetics( + @get:kotlin.jvm.JvmSynthetic + public val `param`: kotlin.String, + ) { + @field:kotlin.jvm.JvmSynthetic + public val fieldProperty: kotlin.String? = null + + @field:kotlin.jvm.JvmSynthetic + public val `property`: kotlin.String? = null + + @get:kotlin.jvm.JvmSynthetic + public val propertyGet: kotlin.String? = null + + @get:kotlin.jvm.JvmSynthetic + @set:kotlin.jvm.JvmSynthetic + public var propertyGetAndSet: kotlin.String? = null + + @set:kotlin.jvm.JvmSynthetic + public var propertySet: kotlin.String? = null + + /** + * Note: Since this is a synthetic function, some JVM information (annotations, modifiers) may be missing. + */ + @kotlin.jvm.JvmSynthetic + public fun function(): kotlin.Unit { + } + + public interface InterfaceWithJvmName { + /** + * Note: Since this is a synthetic function, some JVM information (annotations, modifiers) may be missing. + */ + @kotlin.jvm.JvmSynthetic + public fun interfaceFunction(): kotlin.Unit + + public companion object { + @get:kotlin.jvm.JvmSynthetic + @kotlin.jvm.JvmStatic + public val FOO_BOOL: kotlin.Boolean = throw NotImplementedError("Stub!") + + /** + * Note: Since this is a synthetic function, some JVM information (annotations, modifiers) may be missing. + */ + @kotlin.jvm.JvmSynthetic + public fun staticFunction(): kotlin.Unit { + } + } + } + } + """.trimIndent(), + ) + } + + class Synthetics( + @get:JvmSynthetic val param: String, + ) { + + @JvmSynthetic + val property: String? = null + + @field:JvmSynthetic + val fieldProperty: String? = null + + @get:JvmSynthetic + val propertyGet: String? = null + + @set:JvmSynthetic + var propertySet: String? = null + + @set:JvmSynthetic + @get:JvmSynthetic + var propertyGetAndSet: String? = null + + @JvmSynthetic + fun function() { + } + + // Interfaces can have JvmSynthetic, so covering a potential edge case of having a companion + // object with JvmSynthetic elements. Also covers an edge case where constants have getters + interface InterfaceWithJvmName { + @JvmSynthetic + fun interfaceFunction() + + companion object { + @JvmStatic + @get:JvmSynthetic + val FOO_BOOL = false + + @JvmSynthetic + @JvmStatic + fun staticFunction() { + } + } + } + } + + @Test + fun throws() { + val typeSpec = Throwing::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class Throwing @kotlin.jvm.Throws(exceptionClasses = [java.lang.IllegalStateException::class]) constructor() { + @get:kotlin.jvm.Throws(exceptionClasses = [java.lang.IllegalStateException::class]) + @set:kotlin.jvm.Throws(exceptionClasses = [java.lang.IllegalStateException::class]) + public var getterAndSetterThrows: kotlin.String? = null + + @get:kotlin.jvm.Throws(exceptionClasses = [java.lang.IllegalStateException::class]) + public val getterThrows: kotlin.String? = null + + @set:kotlin.jvm.Throws(exceptionClasses = [java.lang.IllegalStateException::class]) + public var setterThrows: kotlin.String? = null + + @kotlin.jvm.Throws(exceptionClasses = [java.lang.IllegalStateException::class]) + public fun testFunction(): kotlin.Unit { + } + } + """.trimIndent(), + ) + } + + class Throwing + @Throws(IllegalStateException::class) + constructor() { + + @get:Throws(IllegalStateException::class) + val getterThrows: String? = null + + @set:Throws(IllegalStateException::class) + var setterThrows: String? = null + + @get:Throws(IllegalStateException::class) + @set:Throws(IllegalStateException::class) + var getterAndSetterThrows: String? = null + + @Throws(IllegalStateException::class) + fun testFunction() { + } + } + + // The meta-ist of metadata meta-tests. + @IgnoreForHandlerType( + reason = "Reflection can't parse non-runtime retained annotations", + handlerType = REFLECTIVE, + ) + @Test + fun metaTest_elements() { + val typeSpec = Metadata::class.toTypeSpecWithTestHandler() + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + @kotlin.SinceKotlin(version = "1.3") + @kotlin.`annotation`.Retention(value = kotlin.`annotation`.AnnotationRetention.RUNTIME) + @kotlin.`annotation`.Target(allowedTargets = arrayOf(kotlin.`annotation`.AnnotationTarget.CLASS)) + public annotation class Metadata( + @get:kotlin.jvm.JvmName(name = "k") + public val kind: kotlin.Int = throw NotImplementedError("Stub!"), + @get:kotlin.jvm.JvmName(name = "mv") + public val metadataVersion: kotlin.IntArray = throw NotImplementedError("Stub!"), + @get:kotlin.jvm.JvmName(name = "bv") + public val bytecodeVersion: kotlin.IntArray = throw NotImplementedError("Stub!"), + @get:kotlin.jvm.JvmName(name = "d1") + public val data1: kotlin.Array<kotlin.String> = throw NotImplementedError("Stub!"), + @get:kotlin.jvm.JvmName(name = "d2") + public val data2: kotlin.Array<kotlin.String> = throw NotImplementedError("Stub!"), + @get:kotlin.jvm.JvmName(name = "xs") + public val extraString: kotlin.String = throw NotImplementedError("Stub!"), + @get:kotlin.jvm.JvmName(name = "pn") + public val packageName: kotlin.String = throw NotImplementedError("Stub!"), + @get:kotlin.jvm.JvmName(name = "xi") + public val extraInt: kotlin.Int = throw NotImplementedError("Stub!"), + ) + """.trimIndent(), + ) + } + + // The meta-ist of metadata meta-tests. + @IgnoreForHandlerType( + reason = "Reflection can't parse non-runtime retained annotations", + handlerType = ELEMENTS, + ) + @Test + fun metaTest_reflection() { + val typeSpec = Metadata::class.toTypeSpecWithTestHandler() + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + @kotlin.`annotation`.Retention(value = kotlin.`annotation`.AnnotationRetention.RUNTIME) + @kotlin.`annotation`.Target(allowedTargets = arrayOf(kotlin.`annotation`.AnnotationTarget.CLASS)) + public annotation class Metadata( + @get:kotlin.jvm.JvmName(name = "k") + public val kind: kotlin.Int = throw NotImplementedError("Stub!"), + @get:kotlin.jvm.JvmName(name = "mv") + public val metadataVersion: kotlin.IntArray = throw NotImplementedError("Stub!"), + @get:kotlin.jvm.JvmName(name = "bv") + public val bytecodeVersion: kotlin.IntArray = throw NotImplementedError("Stub!"), + @get:kotlin.jvm.JvmName(name = "d1") + public val data1: kotlin.Array<kotlin.String> = throw NotImplementedError("Stub!"), + @get:kotlin.jvm.JvmName(name = "d2") + public val data2: kotlin.Array<kotlin.String> = throw NotImplementedError("Stub!"), + @get:kotlin.jvm.JvmName(name = "xs") + public val extraString: kotlin.String = throw NotImplementedError("Stub!"), + @get:kotlin.jvm.JvmName(name = "pn") + public val packageName: kotlin.String = throw NotImplementedError("Stub!"), + @get:kotlin.jvm.JvmName(name = "xi") + public val extraInt: kotlin.Int = throw NotImplementedError("Stub!"), + ) + """.trimIndent(), + ) + } + + @Test + fun classNamesAndNesting() { + // Make sure we parse class names correctly at all levels + val typeSpec = ClassNesting::class.toTypeSpecWithTestHandler() + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class ClassNesting() { + public class NestedClass() { + public class SuperNestedClass() { + public inner class SuperDuperInnerClass() + } + } + } + """.trimIndent(), + ) + } + + @IgnoreForHandlerType( + reason = "compile-testing can't handle class names with dashes, will throw " + + "\"class file for com.squareup.kotlinpoet.metadata.specs.Fuzzy\$ClassNesting\$-Nested not found\"", + handlerType = ELEMENTS, + ) + @Test + fun classNamesAndNesting_pathological() { + // Make sure we parse class names correctly at all levels + val typeSpec = `Fuzzy$ClassNesting`::class.toTypeSpecWithTestHandler() + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class `Fuzzy${'$'}ClassNesting`() { + public class `-Nested`() { + public class SuperNestedClass() { + public inner class `-${'$'}Fuzzy${'$'}Super${'$'}Weird-Nested${'$'}Name`() + } + } + } + """.trimIndent(), + ) + } + + @IgnoreForHandlerType( + reason = "Property site-target annotations are always stored on the synthetic annotations " + + "method, which is not accessible in the elements API", + handlerType = ELEMENTS, + ) + @Test + fun parameterAnnotations_reflective() { + val typeSpec = ParameterAnnotations::class.toTypeSpecWithTestHandler() + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class ParameterAnnotations( + @property:com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.CustomAnnotation(name = "${'$'}{'${'$'}'}a") + @com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.CustomAnnotation(name = "b") + public val param1: kotlin.String, + @com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.CustomAnnotation(name = "2") + param2: kotlin.String, + ) { + public fun function(@com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.CustomAnnotation(name = "woo") param1: kotlin.String): kotlin.Unit { + } + } + """.trimIndent(), + ) + } + + @IgnoreForHandlerType( + reason = "Property site-target annotations are always stored on the synthetic annotations " + + "method, which is not accessible in the elements API", + handlerType = REFLECTIVE, + ) + @Test + fun parameterAnnotations_elements() { + val typeSpec = ParameterAnnotations::class.toTypeSpecWithTestHandler() + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class ParameterAnnotations( + @com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.CustomAnnotation(name = "b") + public val param1: kotlin.String, + @com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.CustomAnnotation(name = "2") + param2: kotlin.String, + ) { + public fun function(@com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.CustomAnnotation(name = "woo") param1: kotlin.String): kotlin.Unit { + } + } + """.trimIndent(), + ) + } + + annotation class CustomAnnotation(val name: String) + + class ParameterAnnotations( + @property:CustomAnnotation("\$a") + @param:CustomAnnotation("b") + val param1: String, + @CustomAnnotation("2") param2: String, + ) { + fun function(@CustomAnnotation("woo") param1: String) { + } + } + + @IgnoreForHandlerType( + reason = "Non-runtime annotations are not present for reflection.", + handlerType = ELEMENTS, + ) + @Test + fun classAnnotations_reflective() { + val typeSpec = ClassAnnotations::class.toTypeSpecWithTestHandler() + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + @com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.RuntimeCustomClassAnnotation(name = "Runtime") + public class ClassAnnotations() + """.trimIndent(), + ) + } + + @IgnoreForHandlerType( + reason = "Non-runtime annotations are not present for reflection.", + handlerType = REFLECTIVE, + ) + @Test + fun classAnnotations_elements() { + val typeSpec = ClassAnnotations::class.toTypeSpecWithTestHandler() + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + @com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.BinaryCustomClassAnnotation(name = "Binary") + @com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.RuntimeCustomClassAnnotation(name = "Runtime") + public class ClassAnnotations() + """.trimIndent(), + ) + } + + @Retention(AnnotationRetention.SOURCE) + annotation class SourceCustomClassAnnotation(val name: String) + + @Retention(AnnotationRetention.BINARY) + annotation class BinaryCustomClassAnnotation(val name: String) + + @Retention(AnnotationRetention.RUNTIME) + annotation class RuntimeCustomClassAnnotation(val name: String) + + @SourceCustomClassAnnotation("Source") + @BinaryCustomClassAnnotation("Binary") + @RuntimeCustomClassAnnotation("Runtime") + class ClassAnnotations + + @Test + fun typeAnnotations() { + val typeSpec = TypeAnnotations::class.toTypeSpecWithTestHandler() + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class TypeAnnotations() { + public val foo: kotlin.collections.List<@com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.TypeAnnotation kotlin.String> = throw NotImplementedError("Stub!") + + public fun <T> bar(input: @com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.TypeAnnotation kotlin.String, input2: @com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.TypeAnnotation (@com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.TypeAnnotation kotlin.Int) -> @com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.TypeAnnotation kotlin.String): kotlin.Unit { + } + } + """.trimIndent(), + ) + } + + @Target(TYPE, TYPE_PARAMETER) + annotation class TypeAnnotation + + class TypeAnnotations { + val foo: List<@TypeAnnotation String> = emptyList() + + fun <@TypeAnnotation T> bar( + input: @TypeAnnotation String, + input2: @TypeAnnotation (@TypeAnnotation Int) -> @TypeAnnotation String, + ) { + } + } + + // Regression test for https://github.com/square/kotlinpoet/issues/812 + @Test + fun backwardTypeVarReferences() { + val typeSpec = Asset::class.toTypeSpecWithTestHandler() + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public class Asset<A : com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.Asset<A>>() { + public fun <D : com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.Asset<D>, C : com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.Asset<A>> function(): kotlin.Unit { + } + + public class AssetIn<in C : com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.Asset.AssetIn<C>>() + + public class AssetOut<out B : com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.Asset.AssetOut<B>>() + } + """.trimIndent(), + ) + } + + class Asset<A : Asset<A>> { + fun <D : Asset<D>, C : Asset<A>> function() { + } + + class AssetOut<out B : AssetOut<B>> + class AssetIn<in C : AssetIn<C>> + } + + // Regression test for https://github.com/square/kotlinpoet/issues/821 + @Test + fun abstractClass() { + val typeSpec = AbstractClass::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public abstract class AbstractClass() { + public val baz: kotlin.String? = null + + public abstract val foo: kotlin.String + + public abstract fun bar(): kotlin.Unit + + public abstract fun barWithReturn(): kotlin.String + + public fun fuz(): kotlin.Unit { + } + + public fun fuzWithReturn(): kotlin.String = throw NotImplementedError("Stub!") + } + """.trimIndent(), + ) + } + + abstract class AbstractClass { + abstract val foo: String + abstract fun bar() + abstract fun barWithReturn(): String + + val baz: String? = null + fun fuz() {} + fun fuzWithReturn(): String { + return "" + } + } + + // Regression test for https://github.com/square/kotlinpoet/issues/820 + @Test + fun internalAbstractProperty() { + val typeSpec = InternalAbstractPropertyHolder::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public abstract class InternalAbstractPropertyHolder() { + internal abstract val valProp: kotlin.String + + internal abstract var varProp: kotlin.String + } + """.trimIndent(), + ) + } + + abstract class InternalAbstractPropertyHolder { + internal abstract val valProp: String + internal abstract var varProp: String + } + + @Test + fun modalities() { + val abstractModalities = AbstractModalities::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(abstractModalities.trimmedToString()).isEqualTo( + """ + public abstract class AbstractModalities() : com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.ModalitiesInterface { + public val implicitFinalProp: kotlin.String? = null + + public override val interfaceProp: kotlin.String? = null + + public open val openProp: kotlin.String? = null + + public fun implicitFinalFun(): kotlin.Unit { + } + + public override fun interfaceFun(): kotlin.Unit { + } + + public open fun openFun(): kotlin.Unit { + } + } + """.trimIndent(), + ) + + val finalAbstractModalities = FinalAbstractModalities::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(finalAbstractModalities.trimmedToString()).isEqualTo( + """ + public abstract class FinalAbstractModalities() : com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.ModalitiesInterface { + public final override val interfaceProp: kotlin.String? = null + + public final override fun interfaceFun(): kotlin.Unit { + } + } + """.trimIndent(), + ) + + val modalities = Modalities::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(modalities.trimmedToString()).isEqualTo( + """ + public class Modalities() : com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.AbstractModalities() { + public override val interfaceProp: kotlin.String? = null + + public override val openProp: kotlin.String? = null + + public override fun interfaceFun(): kotlin.Unit { + } + + public override fun openFun(): kotlin.Unit { + } + } + """.trimIndent(), + ) + } + + interface ModalitiesInterface { + val interfaceProp: String? + fun interfaceFun() + } + abstract class AbstractModalities : ModalitiesInterface { + override val interfaceProp: String? = null + override fun interfaceFun() { + } + val implicitFinalProp: String? = null + fun implicitFinalFun() { + } + open val openProp: String? = null + open fun openFun() { + } + } + abstract class FinalAbstractModalities : ModalitiesInterface { + final override val interfaceProp: String? = null + final override fun interfaceFun() { + } + } + class Modalities : AbstractModalities() { + override val interfaceProp: String? = null + override fun interfaceFun() { + } + + override val openProp: String? = null + override fun openFun() { + } + } + + @Test + fun funClass() { + val funInterface = FunInterface::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(funInterface.trimmedToString()).isEqualTo( + """ + public fun interface FunInterface { + public fun example(): kotlin.Unit + } + """.trimIndent(), + ) + } + + fun interface FunInterface { + fun example() + } + + @Test + fun selfReferencingTypeParams() { + val typeSpec = Node::class.toTypeSpecWithTestHandler() + + //language=kotlin + assertThat(typeSpec.trimmedToString()).isEqualTo( + """ + public open class Node<T : com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.Node<T, R>, R : com.squareup.kotlinpoet.metadata.specs.KotlinPoetMetadataSpecsTest.Node<R, T>>() { + public var r: R? = null + + public var t: T? = null + } + """.trimIndent(), + ) + } + + open class Node<T : Node<T, R>, R : Node<R, T>> { + var t: T? = null + var r: R? = null + } +} + +class ClassNesting { + class NestedClass { + class SuperNestedClass { + inner class SuperDuperInnerClass + } + } +} + +class `Fuzzy$ClassNesting` { + class `-Nested` { + class SuperNestedClass { + inner class `-$Fuzzy$Super$Weird-Nested$Name` + } + } +} + +private fun TypeSpec.trimmedToString(): String { + return toString().trim() +} + +inline class InlineClass(val value: String) + +@JvmInline +value class ValueClass(val value: String) + +typealias TypeAliasName = String +typealias GenericTypeAlias = List<String> +typealias NestedTypeAlias = List<GenericTypeAlias> diff --git a/interop/kotlinx-metadata/src/test/kotlin/com/squareup/kotlinpoet/metadata/specs/MultiClassInspectorTest.kt b/interop/kotlinx-metadata/src/test/kotlin/com/squareup/kotlinpoet/metadata/specs/MultiClassInspectorTest.kt new file mode 100644 index 00000000..cb7c21d6 --- /dev/null +++ b/interop/kotlinx-metadata/src/test/kotlin/com/squareup/kotlinpoet/metadata/specs/MultiClassInspectorTest.kt @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.metadata.specs + +import com.google.testing.compile.CompilationRule +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.asClassName +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import com.squareup.kotlinpoet.metadata.classinspectors.ElementsClassInspector +import com.squareup.kotlinpoet.metadata.classinspectors.ReflectiveClassInspector +import com.squareup.kotlinpoet.metadata.specs.MultiClassInspectorTest.ClassInspectorType +import com.squareup.kotlinpoet.metadata.toKotlinClassMetadata +import java.lang.annotation.Inherited +import kotlin.annotation.AnnotationRetention.RUNTIME +import kotlin.reflect.KClass +import kotlinx.metadata.jvm.KotlinClassMetadata.FileFacade +import org.junit.Assume +import org.junit.Rule +import org.junit.rules.TestRule +import org.junit.runner.RunWith +import org.junit.runners.Parameterized +import org.junit.runners.Parameterized.Parameter +import org.junit.runners.model.Statement + +/** Base test class that runs all tests with multiple [ClassInspectorTypes][ClassInspectorType]. */ +@RunWith(Parameterized::class) +@KotlinPoetMetadataPreview +abstract class MultiClassInspectorTest { + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun data(): Collection<Array<ClassInspectorType>> { + return listOf( + arrayOf(ClassInspectorType.REFLECTIVE), + arrayOf(ClassInspectorType.ELEMENTS), + ) + } + } + + enum class ClassInspectorType { + NONE { + override fun create(testInstance: MultiClassInspectorTest): ClassInspector { + throw IllegalStateException("Should not be called, just here to default the jvmfield to something.") + } + }, + REFLECTIVE { + override fun create(testInstance: MultiClassInspectorTest): ClassInspector { + return ReflectiveClassInspector.create() + } + }, + ELEMENTS { + override fun create(testInstance: MultiClassInspectorTest): ClassInspector { + return ElementsClassInspector.create(testInstance.compilation.elements, testInstance.compilation.types) + } + }, + ; + + abstract fun create(testInstance: MultiClassInspectorTest): ClassInspector + } + + @Retention(RUNTIME) + @Target(AnnotationTarget.FUNCTION) + @Inherited + annotation class IgnoreForHandlerType( + val reason: String, + val handlerType: ClassInspectorType, + ) + + @JvmField + @Parameter + var classInspectorType: ClassInspectorType = ClassInspectorType.NONE + + @Rule + @JvmField + val compilation = CompilationRule() + + @Rule + @JvmField + val ignoreForElementsRule = TestRule { base, description -> + object : Statement() { + override fun evaluate() { + val annotation = description.getAnnotation( + IgnoreForHandlerType::class.java, + ) + val shouldIgnore = annotation?.handlerType == classInspectorType + Assume.assumeTrue( + "Ignoring ${description.methodName}: ${annotation?.reason}", + !shouldIgnore, + ) + base.evaluate() + } + } + } + + protected fun KClass<*>.toTypeSpecWithTestHandler(): TypeSpec { + return toTypeSpec(classInspectorType.create(this@MultiClassInspectorTest)) + } + + protected fun KClass<*>.toFileSpecWithTestHandler(): FileSpec { + val classInspector = classInspectorType.create(this@MultiClassInspectorTest) + return java.annotations.filterIsInstance<Metadata>().first().toKotlinClassMetadata<FileFacade>() + .toKmPackage() + .toFileSpec(classInspector, asClassName()) + } +} diff --git a/interop/kotlinx-metadata/src/test/kotlin/com/squareup/kotlinpoet/metadata/specs/NoJvmNameFacadeFile.kt b/interop/kotlinx-metadata/src/test/kotlin/com/squareup/kotlinpoet/metadata/specs/NoJvmNameFacadeFile.kt new file mode 100644 index 00000000..6347108d --- /dev/null +++ b/interop/kotlinx-metadata/src/test/kotlin/com/squareup/kotlinpoet/metadata/specs/NoJvmNameFacadeFile.kt @@ -0,0 +1,18 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.metadata.specs + +val prop: String = "" diff --git a/interop/kotlinx-metadata/src/test/kotlin/com/squareup/kotlinpoet/metadata/specs/ReflectiveClassInspectorTest.kt b/interop/kotlinx-metadata/src/test/kotlin/com/squareup/kotlinpoet/metadata/specs/ReflectiveClassInspectorTest.kt new file mode 100644 index 00000000..a4ecc3de --- /dev/null +++ b/interop/kotlinx-metadata/src/test/kotlin/com/squareup/kotlinpoet/metadata/specs/ReflectiveClassInspectorTest.kt @@ -0,0 +1,90 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.metadata.specs + +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.asClassName +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import com.squareup.kotlinpoet.metadata.classinspectors.ReflectiveClassInspector +import com.tschuchort.compiletesting.KotlinCompilation +import com.tschuchort.compiletesting.SourceFile +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import org.junit.Test + +/** + * Class to test the new functionality of Issue#1036. + * @see <a href="https://github.com/square/kotlinpoet/issues/1036">issue</a> + * @author oberstrike + */ +@KotlinPoetMetadataPreview +class ReflectiveClassInspectorTest { + + data class Person(val name: String) + + /** + * Tests if the [ReflectiveClassInspector] can be created without a + * custom ClassLoader and still works. + */ + @Test + fun standardClassLoaderTest() { + val classInspector = ReflectiveClassInspector.create() + val className = Person::class.asClassName() + val declarationContainer = classInspector.declarationContainerFor(className) + assertNotNull(declarationContainer) + } + + /** + * Tests if the [ReflectiveClassInspector] can be created with a + * custom ClassLoader. + */ + @Test + fun useACustomClassLoaderTest() { + val testClass = "Person" + val testPropertyName = "name" + val testPropertyType = "String" + val testPackageName = "com.test" + val testClassName = ClassName(testPackageName, testClass) + val testKtFileName = "KClass.kt" + + val kotlinSource = SourceFile.kotlin( + testKtFileName, + """ + package $testPackageName + data class $testClass(val $testPropertyName: $testPropertyType) + """.trimIndent(), + ) + + val result = KotlinCompilation().apply { + sources = listOf(kotlinSource) + }.compile() + + assertEquals(KotlinCompilation.ExitCode.OK, result.exitCode) + val classLoader = result.classLoader + val classInspector = ReflectiveClassInspector.create(classLoader) + + val declarationContainer = classInspector.declarationContainerFor(testClassName) + + val properties = declarationContainer.properties + assertEquals(1, properties.size) + + val testProperty = properties.findLast { it.name == testPropertyName } + assertNotNull(testProperty) + + val returnType = testProperty.returnType + assertNotNull(returnType) + } +} diff --git a/interop/kotlinx-metadata/src/test/kotlin/com/squareup/kotlinpoet/metadata/specs/classinspectors/ClassInspectorUtilTest.kt b/interop/kotlinx-metadata/src/test/kotlin/com/squareup/kotlinpoet/metadata/specs/classinspectors/ClassInspectorUtilTest.kt new file mode 100644 index 00000000..4e1c268b --- /dev/null +++ b/interop/kotlinx-metadata/src/test/kotlin/com/squareup/kotlinpoet/metadata/specs/classinspectors/ClassInspectorUtilTest.kt @@ -0,0 +1,96 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.metadata.specs.classinspectors + +import com.google.common.truth.Truth.assertThat +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.asClassName +import com.squareup.kotlinpoet.metadata.KotlinPoetMetadataPreview +import com.squareup.kotlinpoet.metadata.classinspectors.ClassInspectorUtil +import kotlin.test.Test + +@KotlinPoetMetadataPreview +class ClassInspectorUtilTest { + + @Test fun createClassName_simple() { + assertThat(ClassInspectorUtil.createClassName("some/path/Foo")) + .isEqualTo(ClassName("some.path", "Foo")) + } + + @Test fun createClassName_nested() { + assertThat(ClassInspectorUtil.createClassName("some/path/Foo.Nested")) + .isEqualTo(ClassName("some.path", "Foo", "Nested")) + } + + @Test fun createClassName_simple_dollarNameStart() { + assertThat(ClassInspectorUtil.createClassName("some/path/Foo$")) + .isEqualTo(ClassName("some.path", "Foo$")) + } + + @Test fun createClassName_simple_dollarNameMiddle() { + assertThat(ClassInspectorUtil.createClassName("some/path/Fo${'$'}o")) + .isEqualTo(ClassName("some.path", "Fo${'$'}o")) + } + + @Test fun createClassName_simple_dollarNameEnd() { + assertThat(ClassInspectorUtil.createClassName("some/path/Nested.${'$'}Foo")) + .isEqualTo(ClassName("some.path", "Nested", "${'$'}Foo")) + } + + @Test fun createClassName_nested_dollarNameStart() { + assertThat(ClassInspectorUtil.createClassName("some/path/Nested.Foo$")) + .isEqualTo(ClassName("some.path", "Nested", "Foo$")) + } + + @Test fun createClassName_nested_dollarNameMiddle() { + assertThat(ClassInspectorUtil.createClassName("some/path/Nested.Fo${'$'}o")) + .isEqualTo(ClassName("some.path", "Nested", "Fo${'$'}o")) + } + + @Test fun createClassName_nested_dollarNameEnd() { + assertThat(ClassInspectorUtil.createClassName("some/path/Nested.${'$'}Foo")) + .isEqualTo(ClassName("some.path", "Nested", "${'$'}Foo")) + } + + @Test fun createClassName_noPackageName() { + assertThat(ClassInspectorUtil.createClassName("ClassWithNoPackage")) + .isEqualTo(ClassName("", "ClassWithNoPackage")) + } + + // Regression test for avoiding https://github.com/square/kotlinpoet/issues/795 + @Test fun createClassName_noEmptyNames() { + val noPackage = ClassInspectorUtil.createClassName("ClassWithNoPackage") + assertThat(noPackage.simpleNames.any { it.isEmpty() }).isFalse() + + val withPackage = ClassInspectorUtil.createClassName("path/to/ClassWithNoPackage") + assertThat(withPackage.simpleNames.any { it.isEmpty() }).isFalse() + } + + @Test fun createClassName_packageWithCaps() { + assertThat(ClassInspectorUtil.createClassName("some/Path/Foo.Nested")) + .isEqualTo(ClassName("some.Path", "Foo", "Nested")) + } + + @Test fun throwsSpec_normal() { + assertThat(ClassInspectorUtil.createThrowsSpec(listOf(Exception::class.asClassName()))) + .isEqualTo( + AnnotationSpec.builder(Throws::class.asClassName()) + .addMember("exceptionClasses = [%T::class]", Exception::class.asClassName()) + .build(), + ) + } +} diff --git a/interop/ksp/api/ksp.api b/interop/ksp/api/ksp.api new file mode 100644 index 00000000..cc8cb2b8 --- /dev/null +++ b/interop/ksp/api/ksp.api @@ -0,0 +1,64 @@ +public final class com/squareup/kotlinpoet/ksp/AnnotationsKt { + public static final fun toAnnotationSpec (Lcom/google/devtools/ksp/symbol/KSAnnotation;)Lcom/squareup/kotlinpoet/AnnotationSpec; +} + +public final class com/squareup/kotlinpoet/ksp/KsClassDeclarationsKt { + public static final fun toClassName (Lcom/google/devtools/ksp/symbol/KSClassDeclaration;)Lcom/squareup/kotlinpoet/ClassName; +} + +public final class com/squareup/kotlinpoet/ksp/KsTypesKt { + public static final fun toClassName (Lcom/google/devtools/ksp/symbol/KSType;)Lcom/squareup/kotlinpoet/ClassName; + public static final fun toTypeName (Lcom/google/devtools/ksp/symbol/KSType;Lcom/squareup/kotlinpoet/ksp/TypeParameterResolver;)Lcom/squareup/kotlinpoet/TypeName; + public static final fun toTypeName (Lcom/google/devtools/ksp/symbol/KSTypeArgument;Lcom/squareup/kotlinpoet/ksp/TypeParameterResolver;)Lcom/squareup/kotlinpoet/TypeName; + public static final fun toTypeName (Lcom/google/devtools/ksp/symbol/KSTypeReference;Lcom/squareup/kotlinpoet/ksp/TypeParameterResolver;)Lcom/squareup/kotlinpoet/TypeName; + public static synthetic fun toTypeName$default (Lcom/google/devtools/ksp/symbol/KSType;Lcom/squareup/kotlinpoet/ksp/TypeParameterResolver;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/TypeName; + public static synthetic fun toTypeName$default (Lcom/google/devtools/ksp/symbol/KSTypeArgument;Lcom/squareup/kotlinpoet/ksp/TypeParameterResolver;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/TypeName; + public static synthetic fun toTypeName$default (Lcom/google/devtools/ksp/symbol/KSTypeReference;Lcom/squareup/kotlinpoet/ksp/TypeParameterResolver;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/TypeName; + public static final fun toTypeVariableName (Lcom/google/devtools/ksp/symbol/KSTypeParameter;Lcom/squareup/kotlinpoet/ksp/TypeParameterResolver;)Lcom/squareup/kotlinpoet/TypeVariableName; + public static synthetic fun toTypeVariableName$default (Lcom/google/devtools/ksp/symbol/KSTypeParameter;Lcom/squareup/kotlinpoet/ksp/TypeParameterResolver;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/TypeVariableName; +} + +public final class com/squareup/kotlinpoet/ksp/ModifiersKt { + public static final fun toKModifier (Lcom/google/devtools/ksp/symbol/Modifier;)Lcom/squareup/kotlinpoet/KModifier; +} + +public abstract interface class com/squareup/kotlinpoet/ksp/OriginatingKSFiles { + public abstract fun getFiles ()Ljava/util/List; +} + +public final class com/squareup/kotlinpoet/ksp/OriginatingKSFilesKt { + public static final fun addOriginatingKSFile (Lcom/squareup/kotlinpoet/FunSpec$Builder;Lcom/google/devtools/ksp/symbol/KSFile;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public static final fun addOriginatingKSFile (Lcom/squareup/kotlinpoet/PropertySpec$Builder;Lcom/google/devtools/ksp/symbol/KSFile;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public static final fun addOriginatingKSFile (Lcom/squareup/kotlinpoet/TypeAliasSpec$Builder;Lcom/google/devtools/ksp/symbol/KSFile;)Lcom/squareup/kotlinpoet/TypeAliasSpec$Builder; + public static final fun addOriginatingKSFile (Lcom/squareup/kotlinpoet/TypeSpec$Builder;Lcom/google/devtools/ksp/symbol/KSFile;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public static final fun kspDependencies (Lcom/squareup/kotlinpoet/FileSpec;ZLjava/lang/Iterable;)Lcom/google/devtools/ksp/processing/Dependencies; + public static synthetic fun kspDependencies$default (Lcom/squareup/kotlinpoet/FileSpec;ZLjava/lang/Iterable;ILjava/lang/Object;)Lcom/google/devtools/ksp/processing/Dependencies; + public static final fun originatingKSFiles (Lcom/squareup/kotlinpoet/FileSpec;)Ljava/util/List; + public static final fun originatingKSFiles (Lcom/squareup/kotlinpoet/FunSpec;)Ljava/util/List; + public static final fun originatingKSFiles (Lcom/squareup/kotlinpoet/PropertySpec;)Ljava/util/List; + public static final fun originatingKSFiles (Lcom/squareup/kotlinpoet/TypeAliasSpec;)Ljava/util/List; + public static final fun originatingKSFiles (Lcom/squareup/kotlinpoet/TypeSpec;)Ljava/util/List; + public static final fun writeTo (Lcom/squareup/kotlinpoet/FileSpec;Lcom/google/devtools/ksp/processing/CodeGenerator;Lcom/google/devtools/ksp/processing/Dependencies;)V + public static final fun writeTo (Lcom/squareup/kotlinpoet/FileSpec;Lcom/google/devtools/ksp/processing/CodeGenerator;ZLjava/lang/Iterable;)V + public static synthetic fun writeTo$default (Lcom/squareup/kotlinpoet/FileSpec;Lcom/google/devtools/ksp/processing/CodeGenerator;ZLjava/lang/Iterable;ILjava/lang/Object;)V +} + +public abstract interface class com/squareup/kotlinpoet/ksp/TypeParameterResolver { + public static final field Companion Lcom/squareup/kotlinpoet/ksp/TypeParameterResolver$Companion; + public abstract fun get (Ljava/lang/String;)Lcom/squareup/kotlinpoet/TypeVariableName; + public abstract fun getParametersMap ()Ljava/util/Map; +} + +public final class com/squareup/kotlinpoet/ksp/TypeParameterResolver$Companion { + public final fun getEMPTY ()Lcom/squareup/kotlinpoet/ksp/TypeParameterResolver; +} + +public final class com/squareup/kotlinpoet/ksp/TypeParameterResolverKt { + public static final fun toTypeParameterResolver (Ljava/util/List;Lcom/squareup/kotlinpoet/ksp/TypeParameterResolver;Ljava/lang/String;)Lcom/squareup/kotlinpoet/ksp/TypeParameterResolver; + public static synthetic fun toTypeParameterResolver$default (Ljava/util/List;Lcom/squareup/kotlinpoet/ksp/TypeParameterResolver;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/ksp/TypeParameterResolver; +} + +public final class com/squareup/kotlinpoet/ksp/VisibilitiesKt { + public static final fun toKModifier (Lcom/google/devtools/ksp/symbol/Visibility;)Lcom/squareup/kotlinpoet/KModifier; +} + diff --git a/interop/ksp/build.gradle.kts b/interop/ksp/build.gradle.kts new file mode 100644 index 00000000..c18fa7c9 --- /dev/null +++ b/interop/ksp/build.gradle.kts @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +tasks.jar { + manifest { + attributes("Automatic-Module-Name" to "com.squareup.kotlinpoet.ksp") + } +} + +dependencies { + api(project(":kotlinpoet")) + compileOnly(libs.ksp.api) + testImplementation(libs.kotlin.junit) + testImplementation(libs.truth) +} diff --git a/interop/ksp/gradle.properties b/interop/ksp/gradle.properties new file mode 100644 index 00000000..dda5a7df --- /dev/null +++ b/interop/ksp/gradle.properties @@ -0,0 +1,4 @@ +POM_ARTIFACT_ID=kotlinpoet-ksp +POM_NAME=KotlinPoet (KSP Interop) +POM_DESCRIPTION=Extensions for interop with KSP (Kotlin Symbol Processing). +POM_PACKAGING=jar diff --git a/interop/ksp/src/main/kotlin/com/squareup/kotlinpoet/ksp/Annotations.kt b/interop/ksp/src/main/kotlin/com/squareup/kotlinpoet/ksp/Annotations.kt new file mode 100644 index 00000000..7964de2e --- /dev/null +++ b/interop/ksp/src/main/kotlin/com/squareup/kotlinpoet/ksp/Annotations.kt @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.ksp + +import com.google.devtools.ksp.symbol.AnnotationUseSiteTarget +import com.google.devtools.ksp.symbol.ClassKind +import com.google.devtools.ksp.symbol.KSAnnotation +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSName +import com.google.devtools.ksp.symbol.KSType +import com.google.devtools.ksp.symbol.KSTypeAlias +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.CodeBlock +import com.squareup.kotlinpoet.ParameterizedTypeName + +/** Returns an [AnnotationSpec] representation of this [KSAnnotation] instance. */ +public fun KSAnnotation.toAnnotationSpec(): AnnotationSpec { + val builder = when (val type = annotationType.resolve().unwrapTypeAlias().toTypeName()) { + is ClassName -> AnnotationSpec.builder(type) + is ParameterizedTypeName -> AnnotationSpec.builder(type) + else -> error("This is never possible.") + } + useSiteTarget?.let { builder.useSiteTarget(it.kpAnalog) } + // TODO support type params once they're exposed https://github.com/google/ksp/issues/753 + for (argument in arguments) { + val member = CodeBlock.builder() + val name = argument.name!!.getShortName() + member.add("%N = ", name) + addValueToBlock(argument.value!!, member) + builder.addMember(member.build()) + } + return builder.build() +} + +private val AnnotationUseSiteTarget.kpAnalog: UseSiteTarget get() = when (this) { + AnnotationUseSiteTarget.FILE -> UseSiteTarget.FILE + AnnotationUseSiteTarget.PROPERTY -> UseSiteTarget.PROPERTY + AnnotationUseSiteTarget.FIELD -> UseSiteTarget.FIELD + AnnotationUseSiteTarget.GET -> UseSiteTarget.GET + AnnotationUseSiteTarget.SET -> UseSiteTarget.SET + AnnotationUseSiteTarget.RECEIVER -> UseSiteTarget.RECEIVER + AnnotationUseSiteTarget.PARAM -> UseSiteTarget.PARAM + AnnotationUseSiteTarget.SETPARAM -> UseSiteTarget.SETPARAM + AnnotationUseSiteTarget.DELEGATE -> UseSiteTarget.DELEGATE +} + +internal fun KSType.unwrapTypeAlias(): KSType { + return if (this.declaration is KSTypeAlias) { + (this.declaration as KSTypeAlias).type.resolve() + } else { + this + } +} + +private fun addValueToBlock(value: Any, member: CodeBlock.Builder) { + when (value) { + is List<*> -> { + // Array type + val arrayType = when (value.firstOrNull()) { + is Boolean -> "booleanArrayOf" + is Byte -> "byteArrayOf" + is Char -> "charArrayOf" + is Short -> "shortArrayOf" + is Int -> "intArrayOf" + is Long -> "longArrayOf" + is Float -> "floatArrayOf" + is Double -> "doubleArrayOf" + else -> "arrayOf" + } + member.add("$arrayType(⇥⇥") + value.forEachIndexed { index, innerValue -> + if (index > 0) member.add(", ") + addValueToBlock(innerValue!!, member) + } + member.add("⇤⇤)") + } + is KSType -> { + val unwrapped = value.unwrapTypeAlias() + val isEnum = (unwrapped.declaration as KSClassDeclaration).classKind == ClassKind.ENUM_ENTRY + if (isEnum) { + val parent = unwrapped.declaration.parentDeclaration as KSClassDeclaration + val entry = unwrapped.declaration.simpleName.getShortName() + member.add("%T.%L", parent.toClassName(), entry) + } else { + member.add("%T::class", unwrapped.toClassName()) + } + } + is KSName -> + member.add( + "%T.%L", + ClassName.bestGuess(value.getQualifier()), + value.getShortName(), + ) + is KSAnnotation -> member.add("%L", value.toAnnotationSpec()) + else -> member.add(memberForValue(value)) + } +} + +/** + * Creates a [CodeBlock] with parameter `format` depending on the given `value` object. + * Handles a number of special cases, such as appending "f" to `Float` values, and uses + * `%L` for other types. + */ +internal fun memberForValue(value: Any) = when (value) { + is Class<*> -> CodeBlock.of("%T::class", value) + is Enum<*> -> CodeBlock.of("%T.%L", value.javaClass, value.name) + is String -> CodeBlock.of("%S", value) + is Float -> CodeBlock.of("%Lf", value) + is Double -> CodeBlock.of("%L", value) + is Char -> CodeBlock.of("'%L'", value) + is Byte -> CodeBlock.of("$value.toByte()") + is Short -> CodeBlock.of("$value.toShort()") + // Int or Boolean + else -> CodeBlock.of("%L", value) +} diff --git a/interop/ksp/src/main/kotlin/com/squareup/kotlinpoet/ksp/KsClassDeclarations.kt b/interop/ksp/src/main/kotlin/com/squareup/kotlinpoet/ksp/KsClassDeclarations.kt new file mode 100644 index 00000000..443be1b3 --- /dev/null +++ b/interop/ksp/src/main/kotlin/com/squareup/kotlinpoet/ksp/KsClassDeclarations.kt @@ -0,0 +1,24 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.ksp + +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.squareup.kotlinpoet.ClassName + +/** Returns the [ClassName] representation of this [KSClassDeclaration]. */ +public fun KSClassDeclaration.toClassName(): ClassName { + return toClassNameInternal() +} diff --git a/interop/ksp/src/main/kotlin/com/squareup/kotlinpoet/ksp/KsTypes.kt b/interop/ksp/src/main/kotlin/com/squareup/kotlinpoet/ksp/KsTypes.kt new file mode 100644 index 00000000..0572b739 --- /dev/null +++ b/interop/ksp/src/main/kotlin/com/squareup/kotlinpoet/ksp/KsTypes.kt @@ -0,0 +1,190 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.ksp + +import com.google.devtools.ksp.symbol.ClassKind +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.google.devtools.ksp.symbol.KSType +import com.google.devtools.ksp.symbol.KSTypeAlias +import com.google.devtools.ksp.symbol.KSTypeArgument +import com.google.devtools.ksp.symbol.KSTypeParameter +import com.google.devtools.ksp.symbol.KSTypeReference +import com.google.devtools.ksp.symbol.Variance +import com.google.devtools.ksp.symbol.Variance.CONTRAVARIANT +import com.google.devtools.ksp.symbol.Variance.COVARIANT +import com.google.devtools.ksp.symbol.Variance.INVARIANT +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.KModifier +import com.squareup.kotlinpoet.STAR +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.TypeVariableName +import com.squareup.kotlinpoet.WildcardTypeName +import com.squareup.kotlinpoet.tags.TypeAliasTag + +/** Returns the [ClassName] representation of this [KSType] IFF it's a [KSClassDeclaration]. */ +public fun KSType.toClassName(): ClassName { + val decl = declaration + check(decl is KSClassDeclaration) { + "Declaration was not a KSClassDeclaration: $this" + } + return decl.toClassName() +} + +/** + * Returns the [TypeName] representation of this [KSType]. + * + * @see toTypeParameterResolver + * @param typeParamResolver an optional resolver for enclosing declarations' type parameters. Parent + * declarations can be anything with generics that child nodes declare as + * defined by [KSType.arguments]. + */ +public fun KSType.toTypeName( + typeParamResolver: TypeParameterResolver = TypeParameterResolver.EMPTY, +): TypeName = toTypeName(typeParamResolver, emptyList()) + +internal fun KSType.toTypeName( + typeParamResolver: TypeParameterResolver, + typeArguments: List<KSTypeArgument>, +): TypeName { + require(!isError) { + "Error type '$this' is not resolvable in the current round of processing." + } + val type = when (val decl = declaration) { + is KSClassDeclaration -> { + val arguments = if (decl.classKind == ClassKind.ANNOTATION_CLASS) { + arguments + } else { + typeArguments + } + + decl.toClassName().withTypeArguments(arguments.map { it.toTypeName(typeParamResolver) }) + } + is KSTypeParameter -> typeParamResolver[decl.name.getShortName()] + is KSTypeAlias -> { + var typeAlias: KSTypeAlias = decl + var arguments = arguments + + var resolvedType: KSType + var mappedArgs: List<KSTypeArgument> + var extraResolver: TypeParameterResolver = typeParamResolver + while (true) { + resolvedType = typeAlias.type.resolve() + mappedArgs = mapTypeArgumentsFromTypeAliasToAbbreviatedType( + typeAlias = typeAlias, + typeAliasTypeArguments = arguments, + abbreviatedType = resolvedType, + ) + extraResolver = if (typeAlias.typeParameters.isEmpty()) { + extraResolver + } else { + typeAlias.typeParameters.toTypeParameterResolver(extraResolver) + } + + typeAlias = resolvedType.declaration as? KSTypeAlias ?: break + arguments = mappedArgs + } + + val abbreviatedType = resolvedType + .toTypeName(extraResolver) + .copy(nullable = isMarkedNullable) + .rawType() + .withTypeArguments(mappedArgs.map { it.toTypeName(extraResolver) }) + + val aliasArgs = typeArguments.map { it.toTypeName(typeParamResolver) } + + decl.toClassNameInternal() + .withTypeArguments(aliasArgs) + .copy(tags = mapOf(TypeAliasTag::class to TypeAliasTag(abbreviatedType))) + } + else -> error("Unsupported type: $declaration") + } + + return type.copy(nullable = isMarkedNullable) +} + +private fun mapTypeArgumentsFromTypeAliasToAbbreviatedType( + typeAlias: KSTypeAlias, + typeAliasTypeArguments: List<KSTypeArgument>, + abbreviatedType: KSType, +): List<KSTypeArgument> { + return abbreviatedType.arguments + .map { typeArgument -> + // Check if type argument is a reference to a typealias type parameter, and not an actual type. + val typeAliasTypeParameterIndex = typeAlias.typeParameters.indexOfFirst { typeAliasTypeParameter -> + typeAliasTypeParameter.name.asString() == typeArgument.type.toString() + } + if (typeAliasTypeParameterIndex >= 0) { + typeAliasTypeArguments[typeAliasTypeParameterIndex] + } else { + typeArgument + } + } +} + +/** + * Returns a [TypeVariableName] representation of this [KSTypeParameter]. + * + * @see toTypeParameterResolver + * @param typeParamResolver an optional resolver for enclosing declarations' type parameters. Parent + * declarations can be anything with generics that child nodes declare as + * defined by [KSType.arguments]. + */ +public fun KSTypeParameter.toTypeVariableName( + typeParamResolver: TypeParameterResolver = TypeParameterResolver.EMPTY, +): TypeVariableName { + val typeVarName = name.getShortName() + val typeVarBounds = bounds.map { it.toTypeName(typeParamResolver) }.toList() + val typeVarVariance = when (variance) { + COVARIANT -> KModifier.OUT + CONTRAVARIANT -> KModifier.IN + else -> null + } + return TypeVariableName(typeVarName, bounds = typeVarBounds, variance = typeVarVariance) +} + +/** + * Returns a [TypeName] representation of this [KSTypeArgument]. + * + * @see toTypeParameterResolver + * @param typeParamResolver an optional resolver for enclosing declarations' type parameters. Parent + * declarations can be anything with generics that child nodes declare as + * defined by [KSType.arguments]. + */ +public fun KSTypeArgument.toTypeName( + typeParamResolver: TypeParameterResolver = TypeParameterResolver.EMPTY, +): TypeName { + val type = this.type ?: return STAR + return when (variance) { + COVARIANT -> WildcardTypeName.producerOf(type.toTypeName(typeParamResolver)) + CONTRAVARIANT -> WildcardTypeName.consumerOf(type.toTypeName(typeParamResolver)) + Variance.STAR -> STAR + INVARIANT -> type.toTypeName(typeParamResolver) + } +} + +/** + * Returns a [TypeName] representation of this [KSTypeReference]. + * + * @see toTypeParameterResolver + * @param typeParamResolver an optional resolver for enclosing declarations' type parameters. Parent + * declarations can be anything with generics that child nodes declare as + * defined by [KSType.arguments]. + */ +public fun KSTypeReference.toTypeName( + typeParamResolver: TypeParameterResolver = TypeParameterResolver.EMPTY, +): TypeName { + return resolve().toTypeName(typeParamResolver, element?.typeArguments.orEmpty()) +} diff --git a/interop/ksp/src/main/kotlin/com/squareup/kotlinpoet/ksp/Modifiers.kt b/interop/ksp/src/main/kotlin/com/squareup/kotlinpoet/ksp/Modifiers.kt new file mode 100644 index 00000000..d4d8aa56 --- /dev/null +++ b/interop/ksp/src/main/kotlin/com/squareup/kotlinpoet/ksp/Modifiers.kt @@ -0,0 +1,89 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.ksp + +import com.google.devtools.ksp.symbol.Modifier +import com.google.devtools.ksp.symbol.Modifier.ABSTRACT +import com.google.devtools.ksp.symbol.Modifier.ACTUAL +import com.google.devtools.ksp.symbol.Modifier.ANNOTATION +import com.google.devtools.ksp.symbol.Modifier.CROSSINLINE +import com.google.devtools.ksp.symbol.Modifier.DATA +import com.google.devtools.ksp.symbol.Modifier.ENUM +import com.google.devtools.ksp.symbol.Modifier.EXPECT +import com.google.devtools.ksp.symbol.Modifier.EXTERNAL +import com.google.devtools.ksp.symbol.Modifier.FINAL +import com.google.devtools.ksp.symbol.Modifier.FUN +import com.google.devtools.ksp.symbol.Modifier.IN +import com.google.devtools.ksp.symbol.Modifier.INFIX +import com.google.devtools.ksp.symbol.Modifier.INLINE +import com.google.devtools.ksp.symbol.Modifier.INNER +import com.google.devtools.ksp.symbol.Modifier.INTERNAL +import com.google.devtools.ksp.symbol.Modifier.LATEINIT +import com.google.devtools.ksp.symbol.Modifier.NOINLINE +import com.google.devtools.ksp.symbol.Modifier.OPEN +import com.google.devtools.ksp.symbol.Modifier.OPERATOR +import com.google.devtools.ksp.symbol.Modifier.OUT +import com.google.devtools.ksp.symbol.Modifier.OVERRIDE +import com.google.devtools.ksp.symbol.Modifier.PRIVATE +import com.google.devtools.ksp.symbol.Modifier.PROTECTED +import com.google.devtools.ksp.symbol.Modifier.PUBLIC +import com.google.devtools.ksp.symbol.Modifier.REIFIED +import com.google.devtools.ksp.symbol.Modifier.SEALED +import com.google.devtools.ksp.symbol.Modifier.SUSPEND +import com.google.devtools.ksp.symbol.Modifier.TAILREC +import com.google.devtools.ksp.symbol.Modifier.VALUE +import com.google.devtools.ksp.symbol.Modifier.VARARG +import com.squareup.kotlinpoet.KModifier + +/** + * Returns the [KModifier] representation of this [Modifier] or null if this is a Java-only + * modifier (i.e. prefixed with `JAVA_`), which do not have obvious [KModifier] analogues. + */ +public fun Modifier.toKModifier(): KModifier? { + return when (this) { + PUBLIC -> KModifier.PUBLIC + PRIVATE -> KModifier.PRIVATE + INTERNAL -> KModifier.INTERNAL + PROTECTED -> KModifier.PROTECTED + IN -> KModifier.IN + OUT -> KModifier.OUT + OVERRIDE -> KModifier.OVERRIDE + LATEINIT -> KModifier.LATEINIT + ENUM -> KModifier.ENUM + SEALED -> KModifier.SEALED + ANNOTATION -> KModifier.ANNOTATION + DATA -> KModifier.DATA + INNER -> KModifier.INNER + FUN -> KModifier.FUN + VALUE -> KModifier.VALUE + SUSPEND -> KModifier.SUSPEND + TAILREC -> KModifier.TAILREC + OPERATOR -> KModifier.OPERATOR + INFIX -> KModifier.INFIX + INLINE -> KModifier.INLINE + EXTERNAL -> KModifier.EXTERNAL + ABSTRACT -> KModifier.ABSTRACT + FINAL -> KModifier.FINAL + OPEN -> KModifier.OPEN + VARARG -> KModifier.VARARG + NOINLINE -> KModifier.NOINLINE + CROSSINLINE -> KModifier.CROSSINLINE + REIFIED -> KModifier.REIFIED + EXPECT -> KModifier.EXPECT + ACTUAL -> KModifier.ACTUAL + else -> null + } +} diff --git a/interop/ksp/src/main/kotlin/com/squareup/kotlinpoet/ksp/OriginatingKSFiles.kt b/interop/ksp/src/main/kotlin/com/squareup/kotlinpoet/ksp/OriginatingKSFiles.kt new file mode 100644 index 00000000..f7c40b3d --- /dev/null +++ b/interop/ksp/src/main/kotlin/com/squareup/kotlinpoet/ksp/OriginatingKSFiles.kt @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.ksp + +import com.google.devtools.ksp.processing.CodeGenerator +import com.google.devtools.ksp.processing.Dependencies +import com.google.devtools.ksp.symbol.KSFile +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.Taggable +import com.squareup.kotlinpoet.TypeAliasSpec +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.tag +import java.io.OutputStreamWriter +import java.nio.charset.StandardCharsets + +/** + * A simple holder class for containing originating [KSFiles][KSFile], which are used by KSP to + * inform its incremental processing. + * + * See [the docs](https://github.com/google/ksp/blob/main/docs/incremental.md) for more information. + */ +public interface OriginatingKSFiles { + public val files: List<KSFile> +} + +/** Returns this spec's originating [KSFiles][KSFile] for use with incremental processing. */ +public fun TypeSpec.originatingKSFiles(): List<KSFile> = getKSFilesTag() + +/** Returns this spec's originating [KSFiles][KSFile] for use with incremental processing. */ +public fun FunSpec.originatingKSFiles(): List<KSFile> = getKSFilesTag() + +/** Returns this spec's originating [KSFiles][KSFile] for use with incremental processing. */ +public fun PropertySpec.originatingKSFiles(): List<KSFile> = getKSFilesTag() + +/** Returns this spec's originating [KSFiles][KSFile] for use with incremental processing. */ +public fun TypeAliasSpec.originatingKSFiles(): List<KSFile> = getKSFilesTag() + +/** + * Returns the list of all files added to the contained + * [TypeSpecs][TypeSpec], [PropertySpecs][PropertySpec], [FunSpecs][FunSpec], or + * [TypeAliasSpecs][TypeAliasSpec] contained in this spec. + */ +public fun FileSpec.originatingKSFiles(): List<KSFile> { + return members + .flatMap { + when (it) { + is FunSpec -> it.originatingKSFiles() + is PropertySpec -> it.originatingKSFiles() + is TypeSpec -> it.originatingKSFiles() + is TypeAliasSpec -> it.originatingKSFiles() + else -> emptyList() + } + } + .distinct() +} + +/** Adds the given [ksFile] to this builder's tags for use with [originatingKSFiles]. */ +public fun TypeAliasSpec.Builder.addOriginatingKSFile(ksFile: KSFile): TypeAliasSpec.Builder = apply { + getOrCreateKSFilesTag().add(ksFile) +} + +/** Adds the given [ksFile] to this builder's tags for use with [originatingKSFiles]. */ +public fun PropertySpec.Builder.addOriginatingKSFile(ksFile: KSFile): PropertySpec.Builder = apply { + getOrCreateKSFilesTag().add(ksFile) +} + +/** Adds the given [ksFile] to this builder's tags for use with [originatingKSFiles]. */ +public fun FunSpec.Builder.addOriginatingKSFile(ksFile: KSFile): FunSpec.Builder = apply { + getOrCreateKSFilesTag().add(ksFile) +} + +/** Adds the given [ksFile] to this builder's tags for use with [originatingKSFiles]. */ +public fun TypeSpec.Builder.addOriginatingKSFile(ksFile: KSFile): TypeSpec.Builder = apply { + getOrCreateKSFilesTag().add(ksFile) +} + +/** + * Writes this [FileSpec] to a given [codeGenerator] with the given [originatingKSFiles]. + * + * Note that if none are specified, the [originatingKSFiles] argument defaults to using + * [FileSpec.originatingKSFiles], which will automatically resolve any files added to the + * contained declarations. + * + * See [the docs](https://github.com/google/ksp/blob/main/docs/incremental.md) for more information. + * + * @see FileSpec.originatingKSFiles + * @param codeGenerator the [CodeGenerator] to write to. + * @param aggregating flag indicating if this is an aggregating symbol processor. + */ +public fun FileSpec.writeTo( + codeGenerator: CodeGenerator, + aggregating: Boolean, + originatingKSFiles: Iterable<KSFile> = originatingKSFiles(), +) { + val dependencies = kspDependencies(aggregating, originatingKSFiles) + writeTo(codeGenerator, dependencies) +} + +/** + * Writes this [FileSpec] to a given [codeGenerator] with the given [dependencies]. + * + * See [the docs](https://github.com/google/ksp/blob/main/docs/incremental.md) for more information. + * + * @see FileSpec.originatingKSFiles + * @see kspDependencies + * @param codeGenerator the [CodeGenerator] to write to. + * @param dependencies the [Dependencies] to create a new file with. + */ +public fun FileSpec.writeTo( + codeGenerator: CodeGenerator, + dependencies: Dependencies, +) { + val file = codeGenerator.createNewFile(dependencies, packageName, name) + // Don't use writeTo(file) because that tries to handle directories under the hood + OutputStreamWriter(file, StandardCharsets.UTF_8) + .use(::writeTo) +} + +/** + * Returns a KSP [Dependencies] component of this [FileSpec] with the given [originatingKSFiles], + * intended to be used in tandem with [writeTo]. + * + * Note that if no [originatingKSFiles] are specified, the [originatingKSFiles] argument defaults + * to using [FileSpec.originatingKSFiles], which will automatically resolve any files added to the + * contained declarations. + * + * See [the docs](https://github.com/google/ksp/blob/main/docs/incremental.md) for more information. + * + * @see FileSpec.originatingKSFiles + * @see FileSpec.writeTo + * @param aggregating flag indicating if this is an aggregating symbol processor. + */ +public fun FileSpec.kspDependencies( + aggregating: Boolean, + originatingKSFiles: Iterable<KSFile> = originatingKSFiles(), +): Dependencies = Dependencies(aggregating, *originatingKSFiles.toList().toTypedArray()) + +/** + * A mutable [OriginatingKSFiles] instance for use with KotlinPoet Builders via [Taggable.Builder]. + */ +private interface MutableOriginatingKSFiles : OriginatingKSFiles { + override val files: MutableList<KSFile> +} + +private data class MutableOriginatingKSFilesImpl(override val files: MutableList<KSFile> = mutableListOf()) : MutableOriginatingKSFiles + +private fun Taggable.getKSFilesTag(): List<KSFile> { + return tag<OriginatingKSFiles>()?.files.orEmpty() +} + +private fun Taggable.Builder<*>.getOrCreateKSFilesTag(): MutableList<KSFile> { + val holder = tags.getOrPut( + OriginatingKSFiles::class, + ::MutableOriginatingKSFilesImpl, + ) as MutableOriginatingKSFiles + return holder.files +} diff --git a/interop/ksp/src/main/kotlin/com/squareup/kotlinpoet/ksp/TypeParameterResolver.kt b/interop/ksp/src/main/kotlin/com/squareup/kotlinpoet/ksp/TypeParameterResolver.kt new file mode 100644 index 00000000..1304b2a4 --- /dev/null +++ b/interop/ksp/src/main/kotlin/com/squareup/kotlinpoet/ksp/TypeParameterResolver.kt @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.ksp + +import com.google.devtools.ksp.symbol.KSType +import com.google.devtools.ksp.symbol.KSTypeParameter +import com.squareup.kotlinpoet.TypeVariableName + +/** + * A resolver for enclosing declarations' type parameters. Parent declarations can be anything with + * generics that child nodes declare as defined by [KSType.arguments]. + * + * This is important for resolving inherited generics on child declarations, as KSP interop + * otherwise can't resolve them. + * + * In general, you want to retrieve an instance of this via [toTypeParameterResolver]. + * + * @see toTypeParameterResolver + */ +public interface TypeParameterResolver { + public val parametersMap: Map<String, TypeVariableName> + public operator fun get(index: String): TypeVariableName + + public companion object { + /** + * An empty instance of [TypeParameterResolver], only should be used if enclosing declarations + * are known to not have arguments, such as top-level classes. + */ + public val EMPTY: TypeParameterResolver = object : TypeParameterResolver { + override val parametersMap: Map<String, TypeVariableName> = emptyMap() + + override fun get(index: String): TypeVariableName = throw NoSuchElementException("No TypeParameter found for index $index") + } + } +} + +/** + * Returns a [TypeParameterResolver] for this list of [KSTypeParameters][KSTypeParameter] for use + * with enclosed declarations. + * + * @param parent the optional parent resolver, if any. An example of this is cases where you might + * create a resolver for a [KSFunction] and supply a parent resolved from the + * enclosing [KSClassDeclaration]. + * @param sourceTypeHint an optional hint for error messages. Unresolvable parameter IDs will + * include this hint in the thrown error's message. + */ +public fun List<KSTypeParameter>.toTypeParameterResolver( + parent: TypeParameterResolver? = null, + sourceTypeHint: String = "<unknown>", +): TypeParameterResolver { + val parametersMap = LinkedHashMap<String, TypeVariableName>() + val typeParamResolver = { id: String -> + parametersMap[id] + ?: parent?.get(id) + ?: throw IllegalStateException( + "No type argument found for $id! Analyzed $sourceTypeHint with known parameters " + + "${parametersMap.keys}", + ) + } + + val resolver = object : TypeParameterResolver { + override val parametersMap: Map<String, TypeVariableName> = parametersMap + + override operator fun get(index: String): TypeVariableName = typeParamResolver(index) + } + + // Fill the parametersMap. Need to do sequentially and allow for referencing previously defined params + for (typeVar in this) { + // Put the simple typevar in first, then it can be referenced in the full toTypeVariable() + // replacement later that may add bounds referencing this. + val id = typeVar.name.getShortName() + parametersMap[id] = TypeVariableName(id) + } + + for (typeVar in this) { + val id = typeVar.name.getShortName() + // Now replace it with the full version. + parametersMap[id] = typeVar.toTypeVariableName(resolver) + } + + return resolver +} diff --git a/interop/ksp/src/main/kotlin/com/squareup/kotlinpoet/ksp/Visibilities.kt b/interop/ksp/src/main/kotlin/com/squareup/kotlinpoet/ksp/Visibilities.kt new file mode 100644 index 00000000..dcca6d2e --- /dev/null +++ b/interop/ksp/src/main/kotlin/com/squareup/kotlinpoet/ksp/Visibilities.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.ksp + +import com.google.devtools.ksp.symbol.Visibility +import com.google.devtools.ksp.symbol.Visibility.JAVA_PACKAGE +import com.google.devtools.ksp.symbol.Visibility.LOCAL +import com.squareup.kotlinpoet.KModifier + +/** + * Returns the [KModifier] representation of this visibility or null if this is [JAVA_PACKAGE] + * or [LOCAL] (which do not have obvious [KModifier] alternatives). + */ +public fun Visibility.toKModifier(): KModifier? { + return when (this) { + Visibility.PUBLIC -> KModifier.PUBLIC + Visibility.PRIVATE -> KModifier.PRIVATE + Visibility.PROTECTED -> KModifier.PROTECTED + Visibility.INTERNAL -> KModifier.INTERNAL + else -> null + } +} diff --git a/interop/ksp/src/main/kotlin/com/squareup/kotlinpoet/ksp/utils.kt b/interop/ksp/src/main/kotlin/com/squareup/kotlinpoet/ksp/utils.kt new file mode 100644 index 00000000..7bc8feda --- /dev/null +++ b/interop/ksp/src/main/kotlin/com/squareup/kotlinpoet/ksp/utils.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.ksp + +import com.google.devtools.ksp.isLocal +import com.google.devtools.ksp.symbol.KSDeclaration +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.LambdaTypeName +import com.squareup.kotlinpoet.ParameterizedTypeName +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.TypeName + +internal fun TypeName.rawType(): ClassName { + return findRawType() ?: throw IllegalArgumentException("Cannot get raw type from $this") +} + +internal fun TypeName.findRawType(): ClassName? { + return when (this) { + is ClassName -> this + is ParameterizedTypeName -> rawType + is LambdaTypeName -> { + var count = parameters.size + if (receiver != null) { + count++ + } + val functionSimpleName = if (count >= 23) { + "FunctionN" + } else { + "Function$count" + } + ClassName("kotlin.jvm.functions", functionSimpleName) + } + else -> null + } +} + +internal fun ClassName.withTypeArguments(arguments: List<TypeName>): TypeName { + return if (arguments.isEmpty()) { + this + } else { + this.parameterizedBy(arguments) + } +} + +internal fun KSDeclaration.toClassNameInternal(): ClassName { + require(!isLocal()) { + "Local/anonymous classes are not supported!" + } + val pkgName = packageName.asString() + val typesString = checkNotNull(qualifiedName).asString().removePrefix("$pkgName.") + + val simpleNames = typesString + .split(".") + return ClassName(pkgName, simpleNames) +} diff --git a/interop/ksp/test-processor/build.gradle.kts b/interop/ksp/test-processor/build.gradle.kts new file mode 100644 index 00000000..9fc212e7 --- /dev/null +++ b/interop/ksp/test-processor/build.gradle.kts @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +plugins { + id("com.google.devtools.ksp") +} + +dependencies { + implementation(project(":kotlinpoet")) + implementation(project(":interop:ksp")) + implementation(libs.autoService) + compileOnly(libs.ksp.api) + ksp(libs.autoService.ksp) + // Always force the latest version of the KSP/kotlin impl in tests to match what we're building against + testImplementation(libs.ksp.api) + testImplementation(libs.kotlin.compilerEmbeddable) + testImplementation(libs.kotlin.annotationProcessingEmbeddable) + testImplementation(libs.ksp) + testImplementation(libs.kotlinCompileTesting) + testImplementation(libs.kotlinCompileTesting.ksp) + testImplementation(libs.kotlin.junit) + testImplementation(libs.truth) +} diff --git a/interop/ksp/test-processor/src/main/kotlin/com/squareup/kotlinpoet/ksp/test/processor/TestProcessor.kt b/interop/ksp/test-processor/src/main/kotlin/com/squareup/kotlinpoet/ksp/test/processor/TestProcessor.kt new file mode 100644 index 00000000..b1d41d40 --- /dev/null +++ b/interop/ksp/test-processor/src/main/kotlin/com/squareup/kotlinpoet/ksp/test/processor/TestProcessor.kt @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.ksp.test.processor + +import com.google.devtools.ksp.getDeclaredFunctions +import com.google.devtools.ksp.getDeclaredProperties +import com.google.devtools.ksp.getVisibility +import com.google.devtools.ksp.isConstructor +import com.google.devtools.ksp.processing.Resolver +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.symbol.KSAnnotated +import com.google.devtools.ksp.symbol.KSClassDeclaration +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.ParameterSpec +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.ksp.addOriginatingKSFile +import com.squareup.kotlinpoet.ksp.kspDependencies +import com.squareup.kotlinpoet.ksp.originatingKSFiles +import com.squareup.kotlinpoet.ksp.toAnnotationSpec +import com.squareup.kotlinpoet.ksp.toKModifier +import com.squareup.kotlinpoet.ksp.toTypeName +import com.squareup.kotlinpoet.ksp.toTypeParameterResolver +import com.squareup.kotlinpoet.ksp.toTypeVariableName +import com.squareup.kotlinpoet.ksp.writeTo + +/** + * A simple processor that generates a skeleton API of classes annotated with [ExampleAnnotation] + * for test and verification purposes. + */ +class TestProcessor(private val env: SymbolProcessorEnvironment) : SymbolProcessor { + + private val unwrapTypeAliases = env.options["unwrapTypeAliases"]?.toBooleanStrictOrNull() ?: false + + override fun process(resolver: Resolver): List<KSAnnotated> { + resolver.getSymbolsWithAnnotation(ExampleAnnotation::class.java.canonicalName) + .forEach(::process) + return emptyList() + } + + private fun process(decl: KSAnnotated) { + check(decl is KSClassDeclaration) + + val classBuilder = TypeSpec.classBuilder(decl.simpleName.getShortName()) + .addOriginatingKSFile(decl.containingFile!!) + .apply { + decl.getVisibility().toKModifier()?.let { addModifiers(it) } + addModifiers(decl.modifiers.mapNotNull { it.toKModifier() }) + addAnnotations( + decl.annotations + .filterNot { it.shortName.getShortName() == "ExampleAnnotation" } + .map { it.toAnnotationSpec() }.asIterable(), + ) + } + val classTypeParams = decl.typeParameters.toTypeParameterResolver() + classBuilder.addTypeVariables( + decl.typeParameters.map { typeParam -> + typeParam.toTypeVariableName(classTypeParams).let { + if (unwrapTypeAliases) { + it.unwrapTypeAlias() + } else { + it + } + } + }, + ) + + // Add properties + for (property in decl.getDeclaredProperties()) { + classBuilder.addProperty( + PropertySpec.builder( + property.simpleName.getShortName(), + property.type.toTypeName(classTypeParams).let { + if (unwrapTypeAliases) { + it.unwrapTypeAlias() + } else { + it + } + }, + ) + .addOriginatingKSFile(decl.containingFile!!) + .mutable(property.isMutable) + .apply { + property.getVisibility().toKModifier()?.let { addModifiers(it) } + addModifiers(property.modifiers.mapNotNull { it.toKModifier() }) + addAnnotations( + property.annotations + .map { it.toAnnotationSpec() }.asIterable(), + ) + } + .build(), + ) + } + + // Add functions + for (function in decl.getDeclaredFunctions().filterNot { it.isConstructor() }) { + val functionTypeParams = function.typeParameters.toTypeParameterResolver(classTypeParams) + classBuilder.addFunction( + FunSpec.builder(function.simpleName.getShortName()) + .addOriginatingKSFile(decl.containingFile!!) + .apply { + function.getVisibility().toKModifier()?.let { addModifiers(it) } + addModifiers(function.modifiers.mapNotNull { it.toKModifier() }) + } + .addTypeVariables( + function.typeParameters.map { typeParam -> + typeParam.toTypeVariableName(functionTypeParams).let { + if (unwrapTypeAliases) { + it.unwrapTypeAlias() + } else { + it + } + } + }, + ) + .addParameters( + function.parameters.map { parameter -> + val parameterType = parameter.type.toTypeName(functionTypeParams).let { + if (unwrapTypeAliases) { + it.unwrapTypeAlias() + } else { + it + } + } + parameter.name?.let { + ParameterSpec.builder(it.getShortName(), parameterType).build() + } ?: ParameterSpec.unnamed(parameterType) + }, + ) + .returns( + function.returnType!!.toTypeName(functionTypeParams).let { + if (unwrapTypeAliases) { + it.unwrapTypeAlias() + } else { + it + } + }, + ) + .build(), + ) + } + + val typeSpec = classBuilder.build() + val fileSpec = FileSpec.builder(decl.packageName.asString(), "Test${typeSpec.name}") + .addType(typeSpec) + .build() + + // Ensure that we're properly de-duping these under the hood. + check(fileSpec.originatingKSFiles().size == 1) + + val dependencies = fileSpec.kspDependencies(aggregating = true) + check(dependencies.originatingFiles.size == 1) + check(dependencies.originatingFiles[0] == decl.containingFile) + + fileSpec.writeTo(env.codeGenerator, dependencies) + } +} diff --git a/interop/ksp/test-processor/src/main/kotlin/com/squareup/kotlinpoet/ksp/test/processor/TestProcessorProvider.kt b/interop/ksp/test-processor/src/main/kotlin/com/squareup/kotlinpoet/ksp/test/processor/TestProcessorProvider.kt new file mode 100644 index 00000000..35bf5ce9 --- /dev/null +++ b/interop/ksp/test-processor/src/main/kotlin/com/squareup/kotlinpoet/ksp/test/processor/TestProcessorProvider.kt @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.ksp.test.processor + +import com.google.auto.service.AutoService +import com.google.devtools.ksp.processing.SymbolProcessor +import com.google.devtools.ksp.processing.SymbolProcessorEnvironment +import com.google.devtools.ksp.processing.SymbolProcessorProvider + +@AutoService(SymbolProcessorProvider::class) +class TestProcessorProvider : SymbolProcessorProvider { + override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor { + return TestProcessor(environment) + } +} diff --git a/interop/ksp/test-processor/src/main/kotlin/com/squareup/kotlinpoet/ksp/test/processor/TypeAliasUnwrapping.kt b/interop/ksp/test-processor/src/main/kotlin/com/squareup/kotlinpoet/ksp/test/processor/TypeAliasUnwrapping.kt new file mode 100644 index 00000000..599f8d70 --- /dev/null +++ b/interop/ksp/test-processor/src/main/kotlin/com/squareup/kotlinpoet/ksp/test/processor/TypeAliasUnwrapping.kt @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.ksp.test.processor + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.LambdaTypeName +import com.squareup.kotlinpoet.ParameterizedTypeName +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.TypeVariableName +import com.squareup.kotlinpoet.WildcardTypeName +import com.squareup.kotlinpoet.tag +import com.squareup.kotlinpoet.tags.TypeAliasTag +import java.util.TreeSet + +/* + * Example implementation of how to unwrap a typealias from TypeNameAliasTag + */ + +internal fun TypeName.unwrapTypeAliasReal(): TypeName { + return tag<TypeAliasTag>()?.abbreviatedType?.let { unwrappedType -> + // If any type is nullable, then the whole thing is nullable + var isAnyNullable = isNullable + // Keep track of all annotations across type levels. Sort them too for consistency. + val runningAnnotations = TreeSet<AnnotationSpec>(compareBy { it.toString() }).apply { + addAll(annotations) + } + val nestedUnwrappedType = unwrappedType.unwrapTypeAlias() + runningAnnotations.addAll(nestedUnwrappedType.annotations) + isAnyNullable = isAnyNullable || nestedUnwrappedType.isNullable + nestedUnwrappedType.copy(nullable = isAnyNullable, annotations = runningAnnotations.toList()) + } ?: this +} + +// TypeVariableName gets a special overload because these usually need to be kept in a type-safe +// manner. +internal fun TypeVariableName.unwrapTypeAlias(): TypeVariableName { + return TypeVariableName( + name = name, + bounds = bounds.map { it.unwrapTypeAlias() }, + variance = variance, + ) + .copy(nullable = isNullable, annotations = annotations, tags = tags) +} + +internal fun TypeName.unwrapTypeAlias(): TypeName { + return when (this) { + is ClassName -> unwrapTypeAliasReal() + is ParameterizedTypeName -> unwrapTypeAliasReal() + is TypeVariableName -> unwrapTypeAlias() + is WildcardTypeName -> unwrapTypeAliasReal() + is LambdaTypeName -> unwrapTypeAliasReal() + else -> throw UnsupportedOperationException("Type '${javaClass.simpleName}' is illegal. Only classes, parameterized types, wildcard types, or type variables are allowed.") + } +} diff --git a/interop/ksp/test-processor/src/main/kotlin/com/squareup/kotlinpoet/ksp/test/processor/exampleAnnotations.kt b/interop/ksp/test-processor/src/main/kotlin/com/squareup/kotlinpoet/ksp/test/processor/exampleAnnotations.kt new file mode 100644 index 00000000..993222e2 --- /dev/null +++ b/interop/ksp/test-processor/src/main/kotlin/com/squareup/kotlinpoet/ksp/test/processor/exampleAnnotations.kt @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.ksp.test.processor + +import kotlin.reflect.KClass + +annotation class ExampleAnnotation + +annotation class ComprehensiveAnnotation<T : CharSequence>( + val boolean: Boolean, + val booleanArray: BooleanArray, + val byte: Byte, + val byteArray: ByteArray, + val char: Char, + val charArray: CharArray, + val short: Short, + val shortArray: ShortArray, + val int: Int, + val intArray: IntArray, + val long: Long, + val longArray: LongArray, + val float: Float, + val floatArray: FloatArray, + val double: Double, + val doubleArray: DoubleArray, + val string: String, + val stringArray: Array<String>, + val someClass: KClass<*>, + val someClasses: Array<KClass<*>>, + val enumValue: AnnotationEnumValue, + val enumValueArray: Array<AnnotationEnumValue>, + val anotherAnnotation: AnotherAnnotation, + val anotherAnnotationArray: Array<AnotherAnnotation>, + // This is still included even when the argument is omitted until https://github.com/google/ksp/issues/674 + val defaultingString: String = "defaultValue", +) + +annotation class AnotherAnnotation(val input: String) + +enum class AnnotationEnumValue { + ONE, TWO, THREE +} diff --git a/interop/ksp/test-processor/src/test/kotlin/com/squareup/kotlinpoet/ksp/test/processor/KsTypesTest.kt b/interop/ksp/test-processor/src/test/kotlin/com/squareup/kotlinpoet/ksp/test/processor/KsTypesTest.kt new file mode 100644 index 00000000..b392f374 --- /dev/null +++ b/interop/ksp/test-processor/src/test/kotlin/com/squareup/kotlinpoet/ksp/test/processor/KsTypesTest.kt @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.ksp.test.processor + +import com.google.common.truth.Truth.assertThat +import com.google.devtools.ksp.symbol.KSAnnotation +import com.google.devtools.ksp.symbol.KSDeclaration +import com.google.devtools.ksp.symbol.KSType +import com.google.devtools.ksp.symbol.KSTypeArgument +import com.google.devtools.ksp.symbol.Nullability +import com.squareup.kotlinpoet.ksp.toTypeName +import kotlin.test.assertFailsWith +import org.junit.Test + +class KsTypesTest { + // Regression test for https://github.com/square/kotlinpoet/issues/1178 + @Test + fun errorTypesShouldFail() { + val type = object : KSType { + override val isError: Boolean = true + + // Boilerplate + override val annotations: Sequence<KSAnnotation> + get() = throw NotImplementedError() + override val arguments: List<KSTypeArgument> + get() = throw NotImplementedError() + override val declaration: KSDeclaration + get() = throw NotImplementedError() + override val isFunctionType: Boolean + get() = throw NotImplementedError() + override val isMarkedNullable: Boolean + get() = throw NotImplementedError() + override val isSuspendFunctionType: Boolean + get() = throw NotImplementedError() + override val nullability: Nullability + get() = throw NotImplementedError() + + override fun isAssignableFrom(that: KSType): Boolean { + throw NotImplementedError() + } + + override fun isCovarianceFlexible(): Boolean { + throw NotImplementedError() + } + + override fun isMutabilityFlexible(): Boolean { + throw NotImplementedError() + } + + override fun makeNotNullable(): KSType { + throw NotImplementedError() + } + + override fun makeNullable(): KSType { + throw NotImplementedError() + } + + override fun replace(arguments: List<KSTypeArgument>): KSType { + throw NotImplementedError() + } + + override fun starProjection(): KSType { + throw NotImplementedError() + } + } + + val exception = assertFailsWith<IllegalArgumentException> { + type.toTypeName() + } + assertThat(exception).hasMessageThat() + .contains("is not resolvable in the current round of processing") + } +} diff --git a/interop/ksp/test-processor/src/test/kotlin/com/squareup/kotlinpoet/ksp/test/processor/TestProcessorTest.kt b/interop/ksp/test-processor/src/test/kotlin/com/squareup/kotlinpoet/ksp/test/processor/TestProcessorTest.kt new file mode 100644 index 00000000..06518eff --- /dev/null +++ b/interop/ksp/test-processor/src/test/kotlin/com/squareup/kotlinpoet/ksp/test/processor/TestProcessorTest.kt @@ -0,0 +1,497 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.ksp.test.processor + +import com.google.common.truth.Truth.assertThat +import com.tschuchort.compiletesting.KotlinCompilation +import com.tschuchort.compiletesting.SourceFile +import com.tschuchort.compiletesting.SourceFile.Companion.kotlin +import com.tschuchort.compiletesting.kspArgs +import com.tschuchort.compiletesting.kspIncremental +import com.tschuchort.compiletesting.kspSourcesDir +import com.tschuchort.compiletesting.symbolProcessorProviders +import java.io.File +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder + +class TestProcessorTest { + + @Rule + @JvmField + val temporaryFolder: TemporaryFolder = TemporaryFolder() + + @Test + fun smokeTest() { + val compilation = prepareCompilation( + kotlin( + "Example.kt", + """ + package test + + import com.squareup.kotlinpoet.ksp.test.processor.AnnotationEnumValue + import com.squareup.kotlinpoet.ksp.test.processor.AnotherAnnotation + import com.squareup.kotlinpoet.ksp.test.processor.ComprehensiveAnnotation + import com.squareup.kotlinpoet.ksp.test.processor.ExampleAnnotation + + typealias TypeAliasName = String + typealias GenericTypeAlias = List<String> + typealias ParameterizedTypeAlias<T> = List<T> + + @ComprehensiveAnnotation<String>( + true, // Omit the name intentionally here to test names are still picked up + booleanArray = [true], + byte = 0.toByte(), + byteArray = [0.toByte()], + char = 'a', + charArray = ['a', 'b', 'c'], + short = 0.toShort(), + shortArray = [0.toShort()], + int = 0, + intArray = [0], + long = 0L, + longArray = [0L], + float = 0f, + floatArray = [0f], + double = 0.0, + doubleArray = [0.0], + string = "Hello", + stringArray = ["Hello"], + someClass = String::class, + someClasses = [String::class, Int::class], + enumValue = AnnotationEnumValue.ONE, + enumValueArray = [AnnotationEnumValue.ONE, AnnotationEnumValue.TWO], + anotherAnnotation = AnotherAnnotation("Hello"), + anotherAnnotationArray = [AnotherAnnotation("Hello")] + ) + @ExampleAnnotation + class SmokeTestClass<T, R : Any, E : Enum<E>> { + @field:AnotherAnnotation("siteTargeting") + private val propA: String = "" + internal val propB: String = "" + val propC: Int = 0 + val propD: Int? = null + lateinit var propE: String + var propF: T? = null + + fun functionA(): String { + error() + } + + fun functionB(): R { + error() + } + + fun <F> functionC(param1: String, param2: T, param3: F, param4: F?): R { + error() + } + + suspend fun functionD( + param1: () -> String, + param2: (String) -> String, + param3: String.() -> String + ) { + } + + // A whole bunch of wild types from Moshi's codegen smoke tests + fun wildTypes( + age: Int, + nationalities: List<String>, + weight: Float, + tattoos: Boolean = false, + race: String?, + hasChildren: Boolean = false, + favoriteFood: String? = null, + favoriteDrink: String? = "Water", + wildcardOut: MutableList<out String>, + nullableWildcardOut: MutableList<out String?>, + wildcardIn: Array<in String>, + any: List<*>, + anyTwo: List<Any>, + anyOut: MutableList<out Any>, + nullableAnyOut: MutableList<out Any?>, + favoriteThreeNumbers: IntArray, + favoriteArrayValues: Array<String>, + favoriteNullableArrayValues: Array<String?>, + nullableSetListMapArrayNullableIntWithDefault: Set<List<Map<String, Array<IntArray?>>>>?, + // These are actually currently rendered incorrectly and always unwrapped + aliasedName: TypeAliasName, + genericAlias: GenericTypeAlias, + parameterizedTypeAlias: ParameterizedTypeAlias<String>, + nestedArray: Array<Map<String, Any>>? + ) { + + } + } + """, + ), + ) + val result = compilation.compile() + assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + val generatedFileText = File(compilation.kspSourcesDir, "kotlin/test/TestSmokeTestClass.kt") + .readText() + assertThat(generatedFileText).isEqualTo( + """ + package test + + import com.squareup.kotlinpoet.ksp.test.processor.AnnotationEnumValue + import com.squareup.kotlinpoet.ksp.test.processor.AnotherAnnotation + import com.squareup.kotlinpoet.ksp.test.processor.ComprehensiveAnnotation + import kotlin.Any + import kotlin.Array + import kotlin.Boolean + import kotlin.Enum + import kotlin.Float + import kotlin.Function0 + import kotlin.Function1 + import kotlin.Int + import kotlin.IntArray + import kotlin.String + import kotlin.Unit + import kotlin.collections.List + import kotlin.collections.Map + import kotlin.collections.MutableList + import kotlin.collections.Set + + @ComprehensiveAnnotation<String>( + boolean = true, + booleanArray = booleanArrayOf(true), + byte = 0.toByte(), + byteArray = byteArrayOf(0.toByte()), + char = 'a', + charArray = charArrayOf('a', 'b', 'c'), + short = 0.toShort(), + shortArray = shortArrayOf(0.toShort()), + int = 0, + intArray = intArrayOf(0), + long = 0, + longArray = longArrayOf(0), + float = 0.0f, + floatArray = floatArrayOf(0.0f), + double = 0.0, + doubleArray = doubleArrayOf(0.0), + string = "Hello", + stringArray = arrayOf("Hello"), + someClass = String::class, + someClasses = arrayOf(String::class, Int::class), + enumValue = AnnotationEnumValue.ONE, + enumValueArray = arrayOf(AnnotationEnumValue.ONE, AnnotationEnumValue.TWO), + anotherAnnotation = AnotherAnnotation(input = "Hello"), + anotherAnnotationArray = arrayOf(AnotherAnnotation(input = "Hello")), + defaultingString = "defaultValue", + ) + public class SmokeTestClass<T, R : Any, E : Enum<E>> { + @field:AnotherAnnotation(input = "siteTargeting") + private val propA: String + + internal val propB: String + + public val propC: Int + + public val propD: Int? + + public lateinit var propE: String + + public var propF: T? + + public fun functionA(): String { + } + + public fun functionB(): R { + } + + public fun <F> functionC( + param1: String, + param2: T, + param3: F, + param4: F?, + ): R { + } + + public suspend fun functionD( + param1: Function0<String>, + param2: Function1<String, String>, + param3: Function1<String, String>, + ): Unit { + } + + public fun wildTypes( + age: Int, + nationalities: List<String>, + weight: Float, + tattoos: Boolean, + race: String?, + hasChildren: Boolean, + favoriteFood: String?, + favoriteDrink: String?, + wildcardOut: MutableList<out String>, + nullableWildcardOut: MutableList<out String?>, + wildcardIn: Array<in String>, + any: List<*>, + anyTwo: List<Any>, + anyOut: MutableList<out Any>, + nullableAnyOut: MutableList<*>, + favoriteThreeNumbers: IntArray, + favoriteArrayValues: Array<String>, + favoriteNullableArrayValues: Array<String?>, + nullableSetListMapArrayNullableIntWithDefault: Set<List<Map<String, Array<IntArray?>>>>?, + aliasedName: TypeAliasName, + genericAlias: GenericTypeAlias, + parameterizedTypeAlias: ParameterizedTypeAlias<String>, + nestedArray: Array<Map<String, Any>>?, + ): Unit { + } + } + + """.trimIndent(), + ) + } + + @Test + fun unwrapTypeAliases() { + val compilation = prepareCompilation( + kotlin( + "Example.kt", + """ + package test + + import com.squareup.kotlinpoet.ksp.test.processor.ExampleAnnotation + + typealias TypeAliasName = String + typealias GenericTypeAlias = List<String> + typealias GenericMapTypeAlias<V, K> = Map<K, V> + typealias T1Unused<T1, T2> = Map<T2, String> + typealias A1<T1, T2> = A2<T2, T1> + typealias A2<T2, T3> = Map<T3, T2> + + @ExampleAnnotation + class Example { + fun aliases( + aliasedName: TypeAliasName, + genericAlias: GenericTypeAlias, + genericMapAlias: GenericMapTypeAlias<String, Int>, + t1Unused: T1Unused<String, Int>, + a1: A1<String, Int>, + ) { + } + } + """, + ), + ) + compilation.kspArgs["unwrapTypeAliases"] = "true" + val result = compilation.compile() + assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + val generatedFileText = File(compilation.kspSourcesDir, "kotlin/test/TestExample.kt") + .readText() + assertThat(generatedFileText).isEqualTo( + """ + package test + + import kotlin.Int + import kotlin.String + import kotlin.Unit + import kotlin.collections.List + import kotlin.collections.Map + + public class Example { + public fun aliases( + aliasedName: String, + genericAlias: List<String>, + genericMapAlias: Map<Int, String>, + t1Unused: Map<Int, String>, + a1: Map<String, Int>, + ): Unit { + } + } + + """.trimIndent(), + ) + } + + @Test + fun complexSelfReferencingTypeArgs() { + val compilation = prepareCompilation( + kotlin( + "Example.kt", + """ + package test + + import com.squareup.kotlinpoet.ksp.test.processor.ExampleAnnotation + + @ExampleAnnotation + open class Node<T : Node<T, R>, R : Node<R, T>> { + var t: T? = null + var r: R? = null + } + """, + ), + ) + + val result = compilation.compile() + assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + val generatedFileText = File(compilation.kspSourcesDir, "kotlin/test/TestNode.kt") + .readText() + assertThat(generatedFileText).isEqualTo( + """ + package test + + public open class Node<T : Node<T, R>, R : Node<R, T>> { + public var t: T? + + public var r: R? + } + + """.trimIndent(), + ) + } + + @Test + fun wildcardParameterForRecursiveTypeBound() { + // Enum is an example of a recursive type bound - Enum<E: Enum<E>> + val compilation = prepareCompilation( + kotlin( + "Example.kt", + """ + package test + + import com.squareup.kotlinpoet.ksp.test.processor.ExampleAnnotation + + @ExampleAnnotation + class EnumWrapper { + val enumValue: Enum<*> + } + """, + ), + ) + + val result = compilation.compile() + assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + val generatedFileText = File(compilation.kspSourcesDir, "kotlin/test/TestEnumWrapper.kt") + .readText() + assertThat(generatedFileText).isEqualTo( + """ + package test + + import kotlin.Enum + + public class EnumWrapper { + public val enumValue: Enum<*> + } + + """.trimIndent(), + ) + } + + @Test + fun transitiveAliases() { + val compilation = prepareCompilation( + kotlin( + "Example.kt", + """ + package test + + import com.squareup.kotlinpoet.ksp.test.processor.ExampleAnnotation + + typealias Alias23 = (Any) -> Any + typealias Alias77<Q> = List<Q> + typealias Alias73<Q> = Map<String, Q> + typealias Alias55<Q> = Alias73<Q> + typealias Alias99<Q> = Alias55<Q> + typealias Alias43<Q> = Alias77<Q> + typealias Alias47<Q> = Alias43<Q> + typealias Alias41<Z, Q> = (Alias43<Z>) -> Alias47<Q> + + @ExampleAnnotation + interface TransitiveAliases { + fun <T : Alias41<Alias23, out Alias77<Alias73<Int>>>> bar(vararg arg1: T) + } + """, + ), + ) + + val result = compilation.compile() + assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + val generatedFileText = File(compilation.kspSourcesDir, "kotlin/test/TestTransitiveAliases.kt") + .readText() + assertThat(generatedFileText).isEqualTo( + """ + package test + + import kotlin.Int + import kotlin.Unit + + public class TransitiveAliases { + public fun <T : Alias41<Alias23, out Alias77<Alias73<Int>>>> bar(arg1: T): Unit { + } + } + + """.trimIndent(), + ) + } + + @Test + fun aliasAsTypeArgument() { + val compilation = prepareCompilation( + kotlin( + "Example.kt", + """ + package test + + import com.squareup.kotlinpoet.ksp.test.processor.ExampleAnnotation + + typealias Alias997 = Map<String, Int> + + @ExampleAnnotation + interface AliasAsTypeArgument { + fun bar(arg1: List<Alias997>) + } + """, + ), + ) + + val result = compilation.compile() + assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) + val generatedFileText = File(compilation.kspSourcesDir, "kotlin/test/TestAliasAsTypeArgument.kt") + .readText() + + assertThat(generatedFileText).isEqualTo( + """ + package test + + import kotlin.Unit + import kotlin.collections.List + + public class AliasAsTypeArgument { + public fun bar(arg1: List<Alias997>): Unit { + } + } + + """.trimIndent(), + ) + } + + private fun prepareCompilation(vararg sourceFiles: SourceFile): KotlinCompilation { + return KotlinCompilation() + .apply { + workingDir = temporaryFolder.root + inheritClassPath = true + symbolProcessorProviders = listOf(TestProcessorProvider()) + sources = sourceFiles.asList() + verbose = false + kspIncremental = true // The default now + } + } +} diff --git a/kotlinpoet/api/kotlinpoet.api b/kotlinpoet/api/kotlinpoet.api new file mode 100644 index 00000000..59f04dca --- /dev/null +++ b/kotlinpoet/api/kotlinpoet.api @@ -0,0 +1,1164 @@ +public final class com/squareup/kotlinpoet/AnnotationSpec : com/squareup/kotlinpoet/Taggable { + public static final field Companion Lcom/squareup/kotlinpoet/AnnotationSpec$Companion; + public static final fun builder (Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/AnnotationSpec$Builder; + public static final fun builder (Lcom/squareup/kotlinpoet/ParameterizedTypeName;)Lcom/squareup/kotlinpoet/AnnotationSpec$Builder; + public static final fun builder (Ljava/lang/Class;)Lcom/squareup/kotlinpoet/AnnotationSpec$Builder; + public static final fun builder (Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/AnnotationSpec$Builder; + public fun equals (Ljava/lang/Object;)Z + public static final fun get (Ljava/lang/annotation/Annotation;)Lcom/squareup/kotlinpoet/AnnotationSpec; + public static final fun get (Ljava/lang/annotation/Annotation;Z)Lcom/squareup/kotlinpoet/AnnotationSpec; + public static final fun get (Ljavax/lang/model/element/AnnotationMirror;)Lcom/squareup/kotlinpoet/AnnotationSpec; + public final fun getClassName ()Lcom/squareup/kotlinpoet/ClassName; + public final fun getMembers ()Ljava/util/List; + public fun getTags ()Ljava/util/Map; + public final fun getTypeName ()Lcom/squareup/kotlinpoet/TypeName; + public final fun getUseSiteTarget ()Lcom/squareup/kotlinpoet/AnnotationSpec$UseSiteTarget; + public fun hashCode ()I + public fun tag (Ljava/lang/Class;)Ljava/lang/Object; + public fun tag (Lkotlin/reflect/KClass;)Ljava/lang/Object; + public final fun toBuilder ()Lcom/squareup/kotlinpoet/AnnotationSpec$Builder; + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/kotlinpoet/AnnotationSpec$Builder : com/squareup/kotlinpoet/Taggable$Builder { + public static final field Companion Lcom/squareup/kotlinpoet/AnnotationSpec$Builder$Companion; + public final fun addMember (Lcom/squareup/kotlinpoet/CodeBlock;)Lcom/squareup/kotlinpoet/AnnotationSpec$Builder; + public final fun addMember (Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/AnnotationSpec$Builder; + public final fun build ()Lcom/squareup/kotlinpoet/AnnotationSpec; + public final fun getMembers ()Ljava/util/List; + public fun getTags ()Ljava/util/Map; + public fun tag (Ljava/lang/Class;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/AnnotationSpec$Builder; + public synthetic fun tag (Ljava/lang/Class;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/Taggable$Builder; + public fun tag (Lkotlin/reflect/KClass;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/AnnotationSpec$Builder; + public synthetic fun tag (Lkotlin/reflect/KClass;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/Taggable$Builder; + public final fun useSiteTarget (Lcom/squareup/kotlinpoet/AnnotationSpec$UseSiteTarget;)Lcom/squareup/kotlinpoet/AnnotationSpec$Builder; +} + +public final class com/squareup/kotlinpoet/AnnotationSpec$Builder$Companion { +} + +public final class com/squareup/kotlinpoet/AnnotationSpec$Companion { + public final fun builder (Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/AnnotationSpec$Builder; + public final fun builder (Lcom/squareup/kotlinpoet/ParameterizedTypeName;)Lcom/squareup/kotlinpoet/AnnotationSpec$Builder; + public final fun builder (Ljava/lang/Class;)Lcom/squareup/kotlinpoet/AnnotationSpec$Builder; + public final fun builder (Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/AnnotationSpec$Builder; + public final fun get (Ljava/lang/annotation/Annotation;)Lcom/squareup/kotlinpoet/AnnotationSpec; + public final fun get (Ljava/lang/annotation/Annotation;Z)Lcom/squareup/kotlinpoet/AnnotationSpec; + public final fun get (Ljavax/lang/model/element/AnnotationMirror;)Lcom/squareup/kotlinpoet/AnnotationSpec; + public static synthetic fun get$default (Lcom/squareup/kotlinpoet/AnnotationSpec$Companion;Ljava/lang/annotation/Annotation;ZILjava/lang/Object;)Lcom/squareup/kotlinpoet/AnnotationSpec; +} + +public final class com/squareup/kotlinpoet/AnnotationSpec$UseSiteTarget : java/lang/Enum { + public static final field DELEGATE Lcom/squareup/kotlinpoet/AnnotationSpec$UseSiteTarget; + public static final field FIELD Lcom/squareup/kotlinpoet/AnnotationSpec$UseSiteTarget; + public static final field FILE Lcom/squareup/kotlinpoet/AnnotationSpec$UseSiteTarget; + public static final field GET Lcom/squareup/kotlinpoet/AnnotationSpec$UseSiteTarget; + public static final field PARAM Lcom/squareup/kotlinpoet/AnnotationSpec$UseSiteTarget; + public static final field PROPERTY Lcom/squareup/kotlinpoet/AnnotationSpec$UseSiteTarget; + public static final field RECEIVER Lcom/squareup/kotlinpoet/AnnotationSpec$UseSiteTarget; + public static final field SET Lcom/squareup/kotlinpoet/AnnotationSpec$UseSiteTarget; + public static final field SETPARAM Lcom/squareup/kotlinpoet/AnnotationSpec$UseSiteTarget; + public static fun valueOf (Ljava/lang/String;)Lcom/squareup/kotlinpoet/AnnotationSpec$UseSiteTarget; + public static fun values ()[Lcom/squareup/kotlinpoet/AnnotationSpec$UseSiteTarget; +} + +public final class com/squareup/kotlinpoet/ClassName : com/squareup/kotlinpoet/TypeName, java/lang/Comparable { + public static final field Companion Lcom/squareup/kotlinpoet/ClassName$Companion; + public fun <init> (Ljava/lang/String;)V + public synthetic fun <init> (Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;)V + public fun <init> (Ljava/lang/String;Ljava/util/List;)V + public fun <init> (Ljava/lang/String;[Ljava/lang/String;)V + public static final fun bestGuess (Ljava/lang/String;)Lcom/squareup/kotlinpoet/ClassName; + public fun compareTo (Lcom/squareup/kotlinpoet/ClassName;)I + public synthetic fun compareTo (Ljava/lang/Object;)I + public final fun constructorReference ()Lcom/squareup/kotlinpoet/CodeBlock; + public fun copy (ZLjava/util/List;Ljava/util/Map;)Lcom/squareup/kotlinpoet/ClassName; + public synthetic fun copy (ZLjava/util/List;Ljava/util/Map;)Lcom/squareup/kotlinpoet/TypeName; + public final fun enclosingClassName ()Lcom/squareup/kotlinpoet/ClassName; + public final fun getCanonicalName ()Ljava/lang/String; + public final fun getPackageName ()Ljava/lang/String; + public final fun getSimpleName ()Ljava/lang/String; + public final fun getSimpleNames ()Ljava/util/List; + public final fun nestedClass (Ljava/lang/String;)Lcom/squareup/kotlinpoet/ClassName; + public final fun peerClass (Ljava/lang/String;)Lcom/squareup/kotlinpoet/ClassName; + public final fun reflectionName ()Ljava/lang/String; + public final fun topLevelClassName ()Lcom/squareup/kotlinpoet/ClassName; +} + +public final class com/squareup/kotlinpoet/ClassName$Companion { + public final fun bestGuess (Ljava/lang/String;)Lcom/squareup/kotlinpoet/ClassName; +} + +public final class com/squareup/kotlinpoet/ClassNames { + public static final fun get (Ljava/lang/Class;)Lcom/squareup/kotlinpoet/ClassName; + public static final fun get (Ljavax/lang/model/element/TypeElement;)Lcom/squareup/kotlinpoet/ClassName; + public static final fun get (Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/ClassName; +} + +public final class com/squareup/kotlinpoet/CodeBlock { + public static final field Companion Lcom/squareup/kotlinpoet/CodeBlock$Companion; + public synthetic fun <init> (Ljava/util/List;Ljava/util/List;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public static final fun builder ()Lcom/squareup/kotlinpoet/CodeBlock$Builder; + public fun equals (Ljava/lang/Object;)Z + public fun hashCode ()I + public final fun isEmpty ()Z + public final fun isNotEmpty ()Z + public static final fun of (Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/CodeBlock; + public final fun toBuilder ()Lcom/squareup/kotlinpoet/CodeBlock$Builder; + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/kotlinpoet/CodeBlock$Builder { + public fun <init> ()V + public final fun add (Lcom/squareup/kotlinpoet/CodeBlock;)Lcom/squareup/kotlinpoet/CodeBlock$Builder; + public final fun add (Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/CodeBlock$Builder; + public final fun addNamed (Ljava/lang/String;Ljava/util/Map;)Lcom/squareup/kotlinpoet/CodeBlock$Builder; + public final fun addStatement (Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/CodeBlock$Builder; + public final fun beginControlFlow (Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/CodeBlock$Builder; + public final fun build ()Lcom/squareup/kotlinpoet/CodeBlock; + public final fun clear ()Lcom/squareup/kotlinpoet/CodeBlock$Builder; + public final fun endControlFlow ()Lcom/squareup/kotlinpoet/CodeBlock$Builder; + public final fun indent ()Lcom/squareup/kotlinpoet/CodeBlock$Builder; + public final fun isEmpty ()Z + public final fun isNotEmpty ()Z + public final fun nextControlFlow (Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/CodeBlock$Builder; + public final fun unindent ()Lcom/squareup/kotlinpoet/CodeBlock$Builder; +} + +public final class com/squareup/kotlinpoet/CodeBlock$Companion { + public final fun builder ()Lcom/squareup/kotlinpoet/CodeBlock$Builder; + public final fun of (Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/CodeBlock; +} + +public final class com/squareup/kotlinpoet/CodeBlocks { + public static final fun buildCodeBlock (Lkotlin/jvm/functions/Function1;)Lcom/squareup/kotlinpoet/CodeBlock; + public static final fun joinToCode (Ljava/util/Collection;)Lcom/squareup/kotlinpoet/CodeBlock; + public static final fun joinToCode (Ljava/util/Collection;Ljava/lang/CharSequence;)Lcom/squareup/kotlinpoet/CodeBlock; + public static final fun joinToCode (Ljava/util/Collection;Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Lcom/squareup/kotlinpoet/CodeBlock; + public static final fun joinToCode (Ljava/util/Collection;Ljava/lang/CharSequence;Ljava/lang/CharSequence;Ljava/lang/CharSequence;)Lcom/squareup/kotlinpoet/CodeBlock; + public static synthetic fun joinToCode$default (Ljava/util/Collection;Ljava/lang/CharSequence;Ljava/lang/CharSequence;Ljava/lang/CharSequence;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/CodeBlock; + public static final fun withIndent (Lcom/squareup/kotlinpoet/CodeBlock$Builder;Lkotlin/jvm/functions/Function1;)Lcom/squareup/kotlinpoet/CodeBlock$Builder; +} + +public abstract interface class com/squareup/kotlinpoet/ContextReceivable { + public abstract fun getContextReceiverTypes ()Ljava/util/List; +} + +public abstract interface class com/squareup/kotlinpoet/ContextReceivable$Builder { + public abstract fun getContextReceiverTypes ()Ljava/util/List; +} + +public final class com/squareup/kotlinpoet/ContextReceivable$Builder$DefaultImpls { +} + +public abstract interface annotation class com/squareup/kotlinpoet/DelicateKotlinPoetApi : java/lang/annotation/Annotation { + public abstract fun message ()Ljava/lang/String; +} + +public final class com/squareup/kotlinpoet/Dynamic : com/squareup/kotlinpoet/TypeName { + public static final field INSTANCE Lcom/squareup/kotlinpoet/Dynamic; + public synthetic fun copy (ZLjava/util/List;Ljava/util/Map;)Lcom/squareup/kotlinpoet/TypeName; + public fun copy (ZLjava/util/List;Ljava/util/Map;)Ljava/lang/Void; +} + +public abstract interface annotation class com/squareup/kotlinpoet/ExperimentalKotlinPoetApi : java/lang/annotation/Annotation { +} + +public final class com/squareup/kotlinpoet/FileSpec : com/squareup/kotlinpoet/Taggable { + public static final field Companion Lcom/squareup/kotlinpoet/FileSpec$Companion; + public static final fun builder (Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public static final fun builder (Ljava/lang/String;Ljava/lang/String;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public fun equals (Ljava/lang/Object;)Z + public static final fun get (Ljava/lang/String;Lcom/squareup/kotlinpoet/TypeSpec;)Lcom/squareup/kotlinpoet/FileSpec; + public final fun getAnnotations ()Ljava/util/List; + public final fun getBody ()Lcom/squareup/kotlinpoet/CodeBlock; + public final fun getComment ()Lcom/squareup/kotlinpoet/CodeBlock; + public final fun getDefaultImports ()Ljava/util/Set; + public final fun getMembers ()Ljava/util/List; + public final fun getName ()Ljava/lang/String; + public final fun getPackageName ()Ljava/lang/String; + public fun getTags ()Ljava/util/Map; + public fun hashCode ()I + public final fun isScript ()Z + public static final fun scriptBuilder (Ljava/lang/String;Ljava/lang/String;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public fun tag (Ljava/lang/Class;)Ljava/lang/Object; + public fun tag (Lkotlin/reflect/KClass;)Ljava/lang/Object; + public final fun toBuilder ()Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun toBuilder (Ljava/lang/String;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun toBuilder (Ljava/lang/String;Ljava/lang/String;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public static synthetic fun toBuilder$default (Lcom/squareup/kotlinpoet/FileSpec;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun toJavaFileObject ()Ljavax/tools/JavaFileObject; + public fun toString ()Ljava/lang/String; + public final fun writeTo (Ljava/io/File;)V + public final fun writeTo (Ljava/lang/Appendable;)V + public final fun writeTo (Ljava/nio/file/Path;)V + public final fun writeTo (Ljavax/annotation/processing/Filer;)V +} + +public final class com/squareup/kotlinpoet/FileSpec$Builder : com/squareup/kotlinpoet/Taggable$Builder { + public final fun addAliasedImport (Lcom/squareup/kotlinpoet/ClassName;Ljava/lang/String;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addAliasedImport (Lcom/squareup/kotlinpoet/ClassName;Ljava/lang/String;Ljava/lang/String;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addAliasedImport (Lcom/squareup/kotlinpoet/MemberName;Ljava/lang/String;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addAliasedImport (Ljava/lang/Class;Ljava/lang/String;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addAliasedImport (Lkotlin/reflect/KClass;Ljava/lang/String;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addAnnotation (Lcom/squareup/kotlinpoet/AnnotationSpec;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addAnnotation (Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addAnnotation (Ljava/lang/Class;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addAnnotation (Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addBodyComment (Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addCode (Lcom/squareup/kotlinpoet/CodeBlock;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addCode (Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addComment (Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addDefaultPackageImport (Ljava/lang/String;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addFileComment (Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addFunction (Lcom/squareup/kotlinpoet/FunSpec;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addImport (Lcom/squareup/kotlinpoet/ClassName;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addImport (Lcom/squareup/kotlinpoet/ClassName;[Ljava/lang/String;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addImport (Lcom/squareup/kotlinpoet/Import;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addImport (Ljava/lang/Class;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addImport (Ljava/lang/Class;[Ljava/lang/String;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addImport (Ljava/lang/Enum;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addImport (Ljava/lang/String;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addImport (Ljava/lang/String;[Ljava/lang/String;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addImport (Lkotlin/reflect/KClass;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addImport (Lkotlin/reflect/KClass;[Ljava/lang/String;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addKotlinDefaultImports (ZZ)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public static synthetic fun addKotlinDefaultImports$default (Lcom/squareup/kotlinpoet/FileSpec$Builder;ZZILjava/lang/Object;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addNamedCode (Ljava/lang/String;Ljava/util/Map;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addProperty (Lcom/squareup/kotlinpoet/PropertySpec;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addStatement (Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addType (Lcom/squareup/kotlinpoet/TypeSpec;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun addTypeAlias (Lcom/squareup/kotlinpoet/TypeAliasSpec;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun beginControlFlow (Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun build ()Lcom/squareup/kotlinpoet/FileSpec; + public final fun clearBody ()Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun clearComment ()Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun clearImports ()Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun endControlFlow ()Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun getAnnotations ()Ljava/util/List; + public final fun getDefaultImports ()Ljava/util/Set; + public final fun getImports ()Ljava/util/List; + public final fun getMembers ()Ljava/util/List; + public final fun getName ()Ljava/lang/String; + public final fun getPackageName ()Ljava/lang/String; + public fun getTags ()Ljava/util/Map; + public final fun indent (Ljava/lang/String;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun isScript ()Z + public final fun nextControlFlow (Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public fun tag (Ljava/lang/Class;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public synthetic fun tag (Ljava/lang/Class;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/Taggable$Builder; + public fun tag (Lkotlin/reflect/KClass;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public synthetic fun tag (Lkotlin/reflect/KClass;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/Taggable$Builder; +} + +public final class com/squareup/kotlinpoet/FileSpec$Companion { + public final fun builder (Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun builder (Ljava/lang/String;Ljava/lang/String;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public final fun get (Ljava/lang/String;Lcom/squareup/kotlinpoet/TypeSpec;)Lcom/squareup/kotlinpoet/FileSpec; + public final fun scriptBuilder (Ljava/lang/String;Ljava/lang/String;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public static synthetic fun scriptBuilder$default (Lcom/squareup/kotlinpoet/FileSpec$Companion;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/FileSpec$Builder; +} + +public final class com/squareup/kotlinpoet/FunSpec : com/squareup/kotlinpoet/ContextReceivable, com/squareup/kotlinpoet/OriginatingElementsHolder, com/squareup/kotlinpoet/Taggable { + public static final field Companion Lcom/squareup/kotlinpoet/FunSpec$Companion; + public static final fun builder (Ljava/lang/String;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public static final fun constructorBuilder ()Lcom/squareup/kotlinpoet/FunSpec$Builder; + public fun equals (Ljava/lang/Object;)Z + public final fun getAnnotations ()Ljava/util/List; + public final fun getBody ()Lcom/squareup/kotlinpoet/CodeBlock; + public fun getContextReceiverTypes ()Ljava/util/List; + public final fun getDelegateConstructor ()Ljava/lang/String; + public final fun getDelegateConstructorArguments ()Ljava/util/List; + public final fun getKdoc ()Lcom/squareup/kotlinpoet/CodeBlock; + public final fun getModifiers ()Ljava/util/Set; + public final fun getName ()Ljava/lang/String; + public fun getOriginatingElements ()Ljava/util/List; + public final fun getParameters ()Ljava/util/List; + public final fun getReceiverKdoc ()Lcom/squareup/kotlinpoet/CodeBlock; + public final fun getReceiverType ()Lcom/squareup/kotlinpoet/TypeName; + public final fun getReturnKdoc ()Lcom/squareup/kotlinpoet/CodeBlock; + public final fun getReturnType ()Lcom/squareup/kotlinpoet/TypeName; + public fun getTags ()Ljava/util/Map; + public final fun getTypeVariables ()Ljava/util/List; + public static final fun getterBuilder ()Lcom/squareup/kotlinpoet/FunSpec$Builder; + public fun hashCode ()I + public final fun isAccessor ()Z + public final fun isConstructor ()Z + public static final fun overriding (Ljavax/lang/model/element/ExecutableElement;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public static final fun overriding (Ljavax/lang/model/element/ExecutableElement;Ljavax/lang/model/type/DeclaredType;Ljavax/lang/model/util/Types;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public static final fun setterBuilder ()Lcom/squareup/kotlinpoet/FunSpec$Builder; + public fun tag (Ljava/lang/Class;)Ljava/lang/Object; + public fun tag (Lkotlin/reflect/KClass;)Ljava/lang/Object; + public final fun toBuilder ()Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun toBuilder (Ljava/lang/String;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public static synthetic fun toBuilder$default (Lcom/squareup/kotlinpoet/FunSpec;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/kotlinpoet/FunSpec$Builder : com/squareup/kotlinpoet/ContextReceivable$Builder, com/squareup/kotlinpoet/OriginatingElementsHolder$Builder, com/squareup/kotlinpoet/Taggable$Builder { + public final fun addAnnotation (Lcom/squareup/kotlinpoet/AnnotationSpec;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun addAnnotation (Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun addAnnotation (Ljava/lang/Class;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun addAnnotation (Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun addAnnotations (Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun addCode (Lcom/squareup/kotlinpoet/CodeBlock;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun addCode (Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun addComment (Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun addKdoc (Lcom/squareup/kotlinpoet/CodeBlock;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun addKdoc (Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun addModifiers (Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun addModifiers ([Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun addNamedCode (Ljava/lang/String;Ljava/util/Map;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public fun addOriginatingElement (Ljavax/lang/model/element/Element;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public synthetic fun addOriginatingElement (Ljavax/lang/model/element/Element;)Lcom/squareup/kotlinpoet/OriginatingElementsHolder$Builder; + public final fun addParameter (Lcom/squareup/kotlinpoet/ParameterSpec;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun addParameter (Ljava/lang/String;Lcom/squareup/kotlinpoet/TypeName;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun addParameter (Ljava/lang/String;Lcom/squareup/kotlinpoet/TypeName;[Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun addParameter (Ljava/lang/String;Ljava/lang/reflect/Type;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun addParameter (Ljava/lang/String;Ljava/lang/reflect/Type;[Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun addParameter (Ljava/lang/String;Lkotlin/reflect/KClass;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun addParameter (Ljava/lang/String;Lkotlin/reflect/KClass;[Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun addParameters (Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun addStatement (Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun addTypeVariable (Lcom/squareup/kotlinpoet/TypeVariableName;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun addTypeVariables (Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun beginControlFlow (Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun build ()Lcom/squareup/kotlinpoet/FunSpec; + public final fun callSuperConstructor (Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun callSuperConstructor (Ljava/util/List;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun callSuperConstructor ([Lcom/squareup/kotlinpoet/CodeBlock;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun callSuperConstructor ([Ljava/lang/String;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public static synthetic fun callSuperConstructor$default (Lcom/squareup/kotlinpoet/FunSpec$Builder;[Lcom/squareup/kotlinpoet/CodeBlock;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun callThisConstructor (Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun callThisConstructor (Ljava/util/List;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun callThisConstructor ([Lcom/squareup/kotlinpoet/CodeBlock;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun callThisConstructor ([Ljava/lang/String;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public static synthetic fun callThisConstructor$default (Lcom/squareup/kotlinpoet/FunSpec$Builder;[Lcom/squareup/kotlinpoet/CodeBlock;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun clearBody ()Lcom/squareup/kotlinpoet/FunSpec$Builder; + public synthetic fun contextReceivers (Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/ContextReceivable$Builder; + public synthetic fun contextReceivers ([Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/ContextReceivable$Builder; + public final fun endControlFlow ()Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun getAnnotations ()Ljava/util/List; + public fun getContextReceiverTypes ()Ljava/util/List; + public final fun getModifiers ()Ljava/util/List; + public fun getOriginatingElements ()Ljava/util/List; + public final fun getParameters ()Ljava/util/List; + public fun getTags ()Ljava/util/Map; + public final fun getTypeVariables ()Ljava/util/List; + public final fun jvmModifiers (Ljava/lang/Iterable;)V + public final fun nextControlFlow (Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun receiver (Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun receiver (Lcom/squareup/kotlinpoet/TypeName;Lcom/squareup/kotlinpoet/CodeBlock;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun receiver (Ljava/lang/reflect/Type;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun receiver (Ljava/lang/reflect/Type;Lcom/squareup/kotlinpoet/CodeBlock;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun receiver (Ljava/lang/reflect/Type;Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun receiver (Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun receiver (Lkotlin/reflect/KClass;Lcom/squareup/kotlinpoet/CodeBlock;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun receiver (Lkotlin/reflect/KClass;Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public static synthetic fun receiver$default (Lcom/squareup/kotlinpoet/FunSpec$Builder;Lcom/squareup/kotlinpoet/TypeName;Lcom/squareup/kotlinpoet/CodeBlock;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public static synthetic fun receiver$default (Lcom/squareup/kotlinpoet/FunSpec$Builder;Ljava/lang/reflect/Type;Lcom/squareup/kotlinpoet/CodeBlock;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public static synthetic fun receiver$default (Lcom/squareup/kotlinpoet/FunSpec$Builder;Lkotlin/reflect/KClass;Lcom/squareup/kotlinpoet/CodeBlock;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun returns (Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun returns (Lcom/squareup/kotlinpoet/TypeName;Lcom/squareup/kotlinpoet/CodeBlock;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun returns (Ljava/lang/reflect/Type;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun returns (Ljava/lang/reflect/Type;Lcom/squareup/kotlinpoet/CodeBlock;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun returns (Ljava/lang/reflect/Type;Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun returns (Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun returns (Lkotlin/reflect/KClass;Lcom/squareup/kotlinpoet/CodeBlock;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun returns (Lkotlin/reflect/KClass;Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public static synthetic fun returns$default (Lcom/squareup/kotlinpoet/FunSpec$Builder;Lcom/squareup/kotlinpoet/TypeName;Lcom/squareup/kotlinpoet/CodeBlock;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public static synthetic fun returns$default (Lcom/squareup/kotlinpoet/FunSpec$Builder;Ljava/lang/reflect/Type;Lcom/squareup/kotlinpoet/CodeBlock;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public static synthetic fun returns$default (Lcom/squareup/kotlinpoet/FunSpec$Builder;Lkotlin/reflect/KClass;Lcom/squareup/kotlinpoet/CodeBlock;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public fun tag (Ljava/lang/Class;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public synthetic fun tag (Ljava/lang/Class;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/Taggable$Builder; + public fun tag (Lkotlin/reflect/KClass;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public synthetic fun tag (Lkotlin/reflect/KClass;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/Taggable$Builder; +} + +public final class com/squareup/kotlinpoet/FunSpec$Companion { + public final fun builder (Ljava/lang/String;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun constructorBuilder ()Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun getterBuilder ()Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun overriding (Ljavax/lang/model/element/ExecutableElement;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun overriding (Ljavax/lang/model/element/ExecutableElement;Ljavax/lang/model/type/DeclaredType;Ljavax/lang/model/util/Types;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public final fun setterBuilder ()Lcom/squareup/kotlinpoet/FunSpec$Builder; +} + +public final class com/squareup/kotlinpoet/Import : java/lang/Comparable { + public fun compareTo (Lcom/squareup/kotlinpoet/Import;)I + public synthetic fun compareTo (Ljava/lang/Object;)I + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Ljava/lang/String; + public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lcom/squareup/kotlinpoet/Import; + public static synthetic fun copy$default (Lcom/squareup/kotlinpoet/Import;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/Import; + public fun equals (Ljava/lang/Object;)Z + public final fun getAlias ()Ljava/lang/String; + public final fun getQualifiedName ()Ljava/lang/String; + public fun hashCode ()I + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/kotlinpoet/KModifier : java/lang/Enum { + public static final field ABSTRACT Lcom/squareup/kotlinpoet/KModifier; + public static final field ACTUAL Lcom/squareup/kotlinpoet/KModifier; + public static final field ANNOTATION Lcom/squareup/kotlinpoet/KModifier; + public static final field COMPANION Lcom/squareup/kotlinpoet/KModifier; + public static final field CONST Lcom/squareup/kotlinpoet/KModifier; + public static final field CROSSINLINE Lcom/squareup/kotlinpoet/KModifier; + public static final field DATA Lcom/squareup/kotlinpoet/KModifier; + public static final field ENUM Lcom/squareup/kotlinpoet/KModifier; + public static final field EXPECT Lcom/squareup/kotlinpoet/KModifier; + public static final field EXTERNAL Lcom/squareup/kotlinpoet/KModifier; + public static final field FINAL Lcom/squareup/kotlinpoet/KModifier; + public static final field FUN Lcom/squareup/kotlinpoet/KModifier; + public static final field IN Lcom/squareup/kotlinpoet/KModifier; + public static final field INFIX Lcom/squareup/kotlinpoet/KModifier; + public static final field INLINE Lcom/squareup/kotlinpoet/KModifier; + public static final field INNER Lcom/squareup/kotlinpoet/KModifier; + public static final field INTERNAL Lcom/squareup/kotlinpoet/KModifier; + public static final field LATEINIT Lcom/squareup/kotlinpoet/KModifier; + public static final field NOINLINE Lcom/squareup/kotlinpoet/KModifier; + public static final field OPEN Lcom/squareup/kotlinpoet/KModifier; + public static final field OPERATOR Lcom/squareup/kotlinpoet/KModifier; + public static final field OUT Lcom/squareup/kotlinpoet/KModifier; + public static final field OVERRIDE Lcom/squareup/kotlinpoet/KModifier; + public static final field PRIVATE Lcom/squareup/kotlinpoet/KModifier; + public static final field PROTECTED Lcom/squareup/kotlinpoet/KModifier; + public static final field PUBLIC Lcom/squareup/kotlinpoet/KModifier; + public static final field REIFIED Lcom/squareup/kotlinpoet/KModifier; + public static final field SEALED Lcom/squareup/kotlinpoet/KModifier; + public static final field SUSPEND Lcom/squareup/kotlinpoet/KModifier; + public static final field TAILREC Lcom/squareup/kotlinpoet/KModifier; + public static final field VALUE Lcom/squareup/kotlinpoet/KModifier; + public static final field VARARG Lcom/squareup/kotlinpoet/KModifier; + public static fun valueOf (Ljava/lang/String;)Lcom/squareup/kotlinpoet/KModifier; + public static fun values ()[Lcom/squareup/kotlinpoet/KModifier; +} + +public final class com/squareup/kotlinpoet/KOperator : java/lang/Enum { + public static final field CONTAINS Lcom/squareup/kotlinpoet/KOperator; + public static final field DEC Lcom/squareup/kotlinpoet/KOperator; + public static final field DIV Lcom/squareup/kotlinpoet/KOperator; + public static final field DIV_ASSIGN Lcom/squareup/kotlinpoet/KOperator; + public static final field EQUALS Lcom/squareup/kotlinpoet/KOperator; + public static final field GE Lcom/squareup/kotlinpoet/KOperator; + public static final field GT Lcom/squareup/kotlinpoet/KOperator; + public static final field INC Lcom/squareup/kotlinpoet/KOperator; + public static final field ITERATOR Lcom/squareup/kotlinpoet/KOperator; + public static final field LE Lcom/squareup/kotlinpoet/KOperator; + public static final field LT Lcom/squareup/kotlinpoet/KOperator; + public static final field MINUS Lcom/squareup/kotlinpoet/KOperator; + public static final field MINUS_ASSIGN Lcom/squareup/kotlinpoet/KOperator; + public static final field NOT Lcom/squareup/kotlinpoet/KOperator; + public static final field NOT_CONTAINS Lcom/squareup/kotlinpoet/KOperator; + public static final field NOT_EQUALS Lcom/squareup/kotlinpoet/KOperator; + public static final field PLUS Lcom/squareup/kotlinpoet/KOperator; + public static final field PLUS_ASSIGN Lcom/squareup/kotlinpoet/KOperator; + public static final field RANGE_TO Lcom/squareup/kotlinpoet/KOperator; + public static final field REM Lcom/squareup/kotlinpoet/KOperator; + public static final field REM_ASSIGN Lcom/squareup/kotlinpoet/KOperator; + public static final field TIMES Lcom/squareup/kotlinpoet/KOperator; + public static final field TIMES_ASSIGN Lcom/squareup/kotlinpoet/KOperator; + public static final field UNARY_MINUS Lcom/squareup/kotlinpoet/KOperator; + public static final field UNARY_PLUS Lcom/squareup/kotlinpoet/KOperator; + public static fun valueOf (Ljava/lang/String;)Lcom/squareup/kotlinpoet/KOperator; + public static fun values ()[Lcom/squareup/kotlinpoet/KOperator; +} + +public final class com/squareup/kotlinpoet/LambdaTypeName : com/squareup/kotlinpoet/TypeName { + public static final field Companion Lcom/squareup/kotlinpoet/LambdaTypeName$Companion; + public fun copy (ZLjava/util/List;Ljava/util/Map;)Lcom/squareup/kotlinpoet/LambdaTypeName; + public synthetic fun copy (ZLjava/util/List;Ljava/util/Map;)Lcom/squareup/kotlinpoet/TypeName; + public final fun copy (ZLjava/util/List;ZLjava/util/Map;)Lcom/squareup/kotlinpoet/LambdaTypeName; + public static synthetic fun copy$default (Lcom/squareup/kotlinpoet/LambdaTypeName;ZLjava/util/List;ZLjava/util/Map;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/LambdaTypeName; + public static final fun get (Lcom/squareup/kotlinpoet/TypeName;Ljava/util/List;Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/LambdaTypeName; + public static final fun get (Lcom/squareup/kotlinpoet/TypeName;[Lcom/squareup/kotlinpoet/ParameterSpec;Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/LambdaTypeName; + public static final fun get (Lcom/squareup/kotlinpoet/TypeName;[Lcom/squareup/kotlinpoet/TypeName;Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/LambdaTypeName; + public final fun getParameters ()Ljava/util/List; + public final fun getReceiver ()Lcom/squareup/kotlinpoet/TypeName; + public final fun getReturnType ()Lcom/squareup/kotlinpoet/TypeName; + public final fun isSuspending ()Z +} + +public final class com/squareup/kotlinpoet/LambdaTypeName$Companion { + public final fun get (Lcom/squareup/kotlinpoet/TypeName;Ljava/util/List;Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/LambdaTypeName; + public final fun get (Lcom/squareup/kotlinpoet/TypeName;[Lcom/squareup/kotlinpoet/ParameterSpec;Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/LambdaTypeName; + public final fun get (Lcom/squareup/kotlinpoet/TypeName;[Lcom/squareup/kotlinpoet/TypeName;Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/LambdaTypeName; + public static synthetic fun get$default (Lcom/squareup/kotlinpoet/LambdaTypeName$Companion;Lcom/squareup/kotlinpoet/TypeName;Ljava/util/List;Lcom/squareup/kotlinpoet/TypeName;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/LambdaTypeName; + public static synthetic fun get$default (Lcom/squareup/kotlinpoet/LambdaTypeName$Companion;Lcom/squareup/kotlinpoet/TypeName;[Lcom/squareup/kotlinpoet/ParameterSpec;Lcom/squareup/kotlinpoet/TypeName;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/LambdaTypeName; + public static synthetic fun get$default (Lcom/squareup/kotlinpoet/LambdaTypeName$Companion;Lcom/squareup/kotlinpoet/TypeName;[Lcom/squareup/kotlinpoet/TypeName;Lcom/squareup/kotlinpoet/TypeName;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/LambdaTypeName; +} + +public final class com/squareup/kotlinpoet/MemberName { + public static final field Companion Lcom/squareup/kotlinpoet/MemberName$Companion; + public fun <init> (Lcom/squareup/kotlinpoet/ClassName;Lcom/squareup/kotlinpoet/KOperator;)V + public fun <init> (Lcom/squareup/kotlinpoet/ClassName;Ljava/lang/String;)V + public fun <init> (Lcom/squareup/kotlinpoet/ClassName;Ljava/lang/String;Z)V + public fun <init> (Ljava/lang/String;Lcom/squareup/kotlinpoet/KOperator;)V + public fun <init> (Ljava/lang/String;Ljava/lang/String;)V + public fun <init> (Ljava/lang/String;Ljava/lang/String;Z)V + public final fun component1 ()Ljava/lang/String; + public final fun component2 ()Lcom/squareup/kotlinpoet/ClassName; + public final fun component3 ()Ljava/lang/String; + public final fun component4 ()Lcom/squareup/kotlinpoet/KOperator; + public final fun component5 ()Z + public final fun copy (Ljava/lang/String;Lcom/squareup/kotlinpoet/ClassName;Ljava/lang/String;Lcom/squareup/kotlinpoet/KOperator;Z)Lcom/squareup/kotlinpoet/MemberName; + public static synthetic fun copy$default (Lcom/squareup/kotlinpoet/MemberName;Ljava/lang/String;Lcom/squareup/kotlinpoet/ClassName;Ljava/lang/String;Lcom/squareup/kotlinpoet/KOperator;ZILjava/lang/Object;)Lcom/squareup/kotlinpoet/MemberName; + public fun equals (Ljava/lang/Object;)Z + public static final fun get (Ljava/lang/Class;Ljava/lang/String;)Lcom/squareup/kotlinpoet/MemberName; + public static final fun get (Lkotlin/reflect/KClass;Ljava/lang/String;)Lcom/squareup/kotlinpoet/MemberName; + public final fun getCanonicalName ()Ljava/lang/String; + public final fun getEnclosingClassName ()Lcom/squareup/kotlinpoet/ClassName; + public final fun getOperator ()Lcom/squareup/kotlinpoet/KOperator; + public final fun getPackageName ()Ljava/lang/String; + public final fun getSimpleName ()Ljava/lang/String; + public fun hashCode ()I + public final fun isExtension ()Z + public static final synthetic fun member (Lcom/squareup/kotlinpoet/ClassName;Ljava/lang/String;)Lcom/squareup/kotlinpoet/MemberName; + public final fun reference ()Lcom/squareup/kotlinpoet/CodeBlock; + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/kotlinpoet/MemberName$Companion { + public final fun get (Ljava/lang/Class;Ljava/lang/String;)Lcom/squareup/kotlinpoet/MemberName; + public final fun get (Lkotlin/reflect/KClass;Ljava/lang/String;)Lcom/squareup/kotlinpoet/MemberName; + public final synthetic fun member (Lcom/squareup/kotlinpoet/ClassName;Ljava/lang/String;)Lcom/squareup/kotlinpoet/MemberName; +} + +public final class com/squareup/kotlinpoet/NameAllocator { + public fun <init> ()V + public final fun copy ()Lcom/squareup/kotlinpoet/NameAllocator; + public final fun get (Ljava/lang/Object;)Ljava/lang/String; + public final fun newName (Ljava/lang/String;)Ljava/lang/String; + public final fun newName (Ljava/lang/String;Ljava/lang/Object;)Ljava/lang/String; + public static synthetic fun newName$default (Lcom/squareup/kotlinpoet/NameAllocator;Ljava/lang/String;Ljava/lang/Object;ILjava/lang/Object;)Ljava/lang/String; +} + +public abstract interface class com/squareup/kotlinpoet/OriginatingElementsHolder { + public abstract fun getOriginatingElements ()Ljava/util/List; +} + +public abstract interface class com/squareup/kotlinpoet/OriginatingElementsHolder$Builder { + public abstract fun addOriginatingElement (Ljavax/lang/model/element/Element;)Lcom/squareup/kotlinpoet/OriginatingElementsHolder$Builder; + public abstract fun getOriginatingElements ()Ljava/util/List; +} + +public final class com/squareup/kotlinpoet/OriginatingElementsHolder$Builder$DefaultImpls { + public static fun addOriginatingElement (Lcom/squareup/kotlinpoet/OriginatingElementsHolder$Builder;Ljavax/lang/model/element/Element;)Lcom/squareup/kotlinpoet/OriginatingElementsHolder$Builder; +} + +public final class com/squareup/kotlinpoet/ParameterSpec : com/squareup/kotlinpoet/Taggable { + public static final field Companion Lcom/squareup/kotlinpoet/ParameterSpec$Companion; + public fun <init> (Ljava/lang/String;Lcom/squareup/kotlinpoet/TypeName;Ljava/lang/Iterable;)V + public fun <init> (Ljava/lang/String;Lcom/squareup/kotlinpoet/TypeName;[Lcom/squareup/kotlinpoet/KModifier;)V + public static final fun builder (Ljava/lang/String;Lcom/squareup/kotlinpoet/TypeName;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/ParameterSpec$Builder; + public static final fun builder (Ljava/lang/String;Lcom/squareup/kotlinpoet/TypeName;[Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/ParameterSpec$Builder; + public static final fun builder (Ljava/lang/String;Ljava/lang/reflect/Type;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/ParameterSpec$Builder; + public static final fun builder (Ljava/lang/String;Ljava/lang/reflect/Type;[Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/ParameterSpec$Builder; + public static final fun builder (Ljava/lang/String;Lkotlin/reflect/KClass;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/ParameterSpec$Builder; + public static final fun builder (Ljava/lang/String;Lkotlin/reflect/KClass;[Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/ParameterSpec$Builder; + public fun equals (Ljava/lang/Object;)Z + public static final fun get (Ljavax/lang/model/element/VariableElement;)Lcom/squareup/kotlinpoet/ParameterSpec; + public final fun getAnnotations ()Ljava/util/List; + public final fun getDefaultValue ()Lcom/squareup/kotlinpoet/CodeBlock; + public final fun getKdoc ()Lcom/squareup/kotlinpoet/CodeBlock; + public final fun getModifiers ()Ljava/util/Set; + public final fun getName ()Ljava/lang/String; + public fun getTags ()Ljava/util/Map; + public final fun getType ()Lcom/squareup/kotlinpoet/TypeName; + public fun hashCode ()I + public static final fun parametersOf (Ljavax/lang/model/element/ExecutableElement;)Ljava/util/List; + public fun tag (Ljava/lang/Class;)Ljava/lang/Object; + public fun tag (Lkotlin/reflect/KClass;)Ljava/lang/Object; + public final fun toBuilder (Ljava/lang/String;Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/ParameterSpec$Builder; + public static synthetic fun toBuilder$default (Lcom/squareup/kotlinpoet/ParameterSpec;Ljava/lang/String;Lcom/squareup/kotlinpoet/TypeName;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/ParameterSpec$Builder; + public fun toString ()Ljava/lang/String; + public static final fun unnamed (Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/ParameterSpec; + public static final fun unnamed (Ljava/lang/reflect/Type;)Lcom/squareup/kotlinpoet/ParameterSpec; + public static final fun unnamed (Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/ParameterSpec; +} + +public final class com/squareup/kotlinpoet/ParameterSpec$Builder : com/squareup/kotlinpoet/Taggable$Builder { + public final fun addAnnotation (Lcom/squareup/kotlinpoet/AnnotationSpec;)Lcom/squareup/kotlinpoet/ParameterSpec$Builder; + public final fun addAnnotation (Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/ParameterSpec$Builder; + public final fun addAnnotation (Ljava/lang/Class;)Lcom/squareup/kotlinpoet/ParameterSpec$Builder; + public final fun addAnnotation (Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/ParameterSpec$Builder; + public final fun addAnnotations (Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/ParameterSpec$Builder; + public final fun addKdoc (Lcom/squareup/kotlinpoet/CodeBlock;)Lcom/squareup/kotlinpoet/ParameterSpec$Builder; + public final fun addKdoc (Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/ParameterSpec$Builder; + public final fun addModifiers (Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/ParameterSpec$Builder; + public final fun addModifiers ([Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/ParameterSpec$Builder; + public final fun build ()Lcom/squareup/kotlinpoet/ParameterSpec; + public final fun defaultValue (Lcom/squareup/kotlinpoet/CodeBlock;)Lcom/squareup/kotlinpoet/ParameterSpec$Builder; + public final fun defaultValue (Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/ParameterSpec$Builder; + public final fun getAnnotations ()Ljava/util/List; + public final fun getKdoc ()Lcom/squareup/kotlinpoet/CodeBlock$Builder; + public final fun getModifiers ()Ljava/util/List; + public fun getTags ()Ljava/util/Map; + public final fun jvmModifiers (Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/ParameterSpec$Builder; + public fun tag (Ljava/lang/Class;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/ParameterSpec$Builder; + public synthetic fun tag (Ljava/lang/Class;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/Taggable$Builder; + public fun tag (Lkotlin/reflect/KClass;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/ParameterSpec$Builder; + public synthetic fun tag (Lkotlin/reflect/KClass;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/Taggable$Builder; +} + +public final class com/squareup/kotlinpoet/ParameterSpec$Companion { + public final fun builder (Ljava/lang/String;Lcom/squareup/kotlinpoet/TypeName;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/ParameterSpec$Builder; + public final fun builder (Ljava/lang/String;Lcom/squareup/kotlinpoet/TypeName;[Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/ParameterSpec$Builder; + public final fun builder (Ljava/lang/String;Ljava/lang/reflect/Type;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/ParameterSpec$Builder; + public final fun builder (Ljava/lang/String;Ljava/lang/reflect/Type;[Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/ParameterSpec$Builder; + public final fun builder (Ljava/lang/String;Lkotlin/reflect/KClass;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/ParameterSpec$Builder; + public final fun builder (Ljava/lang/String;Lkotlin/reflect/KClass;[Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/ParameterSpec$Builder; + public final fun get (Ljavax/lang/model/element/VariableElement;)Lcom/squareup/kotlinpoet/ParameterSpec; + public final fun parametersOf (Ljavax/lang/model/element/ExecutableElement;)Ljava/util/List; + public final fun unnamed (Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/ParameterSpec; + public final fun unnamed (Ljava/lang/reflect/Type;)Lcom/squareup/kotlinpoet/ParameterSpec; + public final fun unnamed (Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/ParameterSpec; +} + +public final class com/squareup/kotlinpoet/ParameterizedTypeName : com/squareup/kotlinpoet/TypeName { + public static final field Companion Lcom/squareup/kotlinpoet/ParameterizedTypeName$Companion; + public fun copy (ZLjava/util/List;Ljava/util/Map;)Lcom/squareup/kotlinpoet/ParameterizedTypeName; + public synthetic fun copy (ZLjava/util/List;Ljava/util/Map;)Lcom/squareup/kotlinpoet/TypeName; + public final fun copy (ZLjava/util/List;Ljava/util/Map;Ljava/util/List;)Lcom/squareup/kotlinpoet/ParameterizedTypeName; + public static synthetic fun copy$default (Lcom/squareup/kotlinpoet/ParameterizedTypeName;ZLjava/util/List;Ljava/util/Map;Ljava/util/List;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/ParameterizedTypeName; + public static final fun get (Lcom/squareup/kotlinpoet/ClassName;Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/ParameterizedTypeName; + public static final fun get (Lcom/squareup/kotlinpoet/ClassName;Ljava/util/List;)Lcom/squareup/kotlinpoet/ParameterizedTypeName; + public static final fun get (Lcom/squareup/kotlinpoet/ClassName;[Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/ParameterizedTypeName; + public static final fun get (Ljava/lang/Class;Ljava/lang/Class;)Lcom/squareup/kotlinpoet/ParameterizedTypeName; + public static final fun get (Ljava/lang/Class;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/ParameterizedTypeName; + public static final fun get (Ljava/lang/Class;[Ljava/lang/reflect/Type;)Lcom/squareup/kotlinpoet/ParameterizedTypeName; + public static final fun get (Lkotlin/reflect/KClass;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/ParameterizedTypeName; + public static final fun get (Lkotlin/reflect/KClass;Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/ParameterizedTypeName; + public static final fun get (Lkotlin/reflect/KClass;[Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/ParameterizedTypeName; + public final fun getRawType ()Lcom/squareup/kotlinpoet/ClassName; + public final fun getTypeArguments ()Ljava/util/List; + public final fun nestedClass (Ljava/lang/String;Ljava/util/List;)Lcom/squareup/kotlinpoet/ParameterizedTypeName; + public final fun plusParameter (Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/ParameterizedTypeName; + public final fun plusParameter (Ljava/lang/Class;)Lcom/squareup/kotlinpoet/ParameterizedTypeName; + public final fun plusParameter (Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/ParameterizedTypeName; +} + +public final class com/squareup/kotlinpoet/ParameterizedTypeName$Companion { + public final fun get (Lcom/squareup/kotlinpoet/ClassName;Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/ParameterizedTypeName; + public final fun get (Lcom/squareup/kotlinpoet/ClassName;Ljava/util/List;)Lcom/squareup/kotlinpoet/ParameterizedTypeName; + public final fun get (Lcom/squareup/kotlinpoet/ClassName;[Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/ParameterizedTypeName; + public final fun get (Ljava/lang/Class;Ljava/lang/Class;)Lcom/squareup/kotlinpoet/ParameterizedTypeName; + public final fun get (Ljava/lang/Class;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/ParameterizedTypeName; + public final fun get (Ljava/lang/Class;[Ljava/lang/reflect/Type;)Lcom/squareup/kotlinpoet/ParameterizedTypeName; + public final fun get (Lkotlin/reflect/KClass;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/ParameterizedTypeName; + public final fun get (Lkotlin/reflect/KClass;Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/ParameterizedTypeName; + public final fun get (Lkotlin/reflect/KClass;[Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/ParameterizedTypeName; +} + +public final class com/squareup/kotlinpoet/ParameterizedTypeNames { + public static final fun asTypeName (Lkotlin/reflect/KType;)Lcom/squareup/kotlinpoet/TypeName; + public static final fun get (Ljava/lang/reflect/ParameterizedType;)Lcom/squareup/kotlinpoet/ParameterizedTypeName; +} + +public final class com/squareup/kotlinpoet/PropertySpec : com/squareup/kotlinpoet/ContextReceivable, com/squareup/kotlinpoet/OriginatingElementsHolder, com/squareup/kotlinpoet/Taggable { + public static final field Companion Lcom/squareup/kotlinpoet/PropertySpec$Companion; + public static final fun builder (Ljava/lang/String;Lcom/squareup/kotlinpoet/TypeName;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public static final fun builder (Ljava/lang/String;Lcom/squareup/kotlinpoet/TypeName;[Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public static final fun builder (Ljava/lang/String;Ljava/lang/reflect/Type;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public static final fun builder (Ljava/lang/String;Ljava/lang/reflect/Type;[Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public static final fun builder (Ljava/lang/String;Lkotlin/reflect/KClass;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public static final fun builder (Ljava/lang/String;Lkotlin/reflect/KClass;[Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public fun equals (Ljava/lang/Object;)Z + public final fun getAnnotations ()Ljava/util/List; + public fun getContextReceiverTypes ()Ljava/util/List; + public final fun getDelegated ()Z + public final fun getGetter ()Lcom/squareup/kotlinpoet/FunSpec; + public final fun getInitializer ()Lcom/squareup/kotlinpoet/CodeBlock; + public final fun getKdoc ()Lcom/squareup/kotlinpoet/CodeBlock; + public final fun getModifiers ()Ljava/util/Set; + public final fun getMutable ()Z + public final fun getName ()Ljava/lang/String; + public fun getOriginatingElements ()Ljava/util/List; + public final fun getReceiverType ()Lcom/squareup/kotlinpoet/TypeName; + public final fun getSetter ()Lcom/squareup/kotlinpoet/FunSpec; + public fun getTags ()Ljava/util/Map; + public final fun getType ()Lcom/squareup/kotlinpoet/TypeName; + public final fun getTypeVariables ()Ljava/util/List; + public fun hashCode ()I + public fun tag (Ljava/lang/Class;)Ljava/lang/Object; + public fun tag (Lkotlin/reflect/KClass;)Ljava/lang/Object; + public final fun toBuilder ()Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public final fun toBuilder (Ljava/lang/String;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public final fun toBuilder (Ljava/lang/String;Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public static synthetic fun toBuilder$default (Lcom/squareup/kotlinpoet/PropertySpec;Ljava/lang/String;Lcom/squareup/kotlinpoet/TypeName;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/kotlinpoet/PropertySpec$Builder : com/squareup/kotlinpoet/ContextReceivable$Builder, com/squareup/kotlinpoet/OriginatingElementsHolder$Builder, com/squareup/kotlinpoet/Taggable$Builder { + public final fun addAnnotation (Lcom/squareup/kotlinpoet/AnnotationSpec;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public final fun addAnnotation (Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public final fun addAnnotation (Ljava/lang/Class;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public final fun addAnnotation (Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public final fun addAnnotations (Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public final fun addKdoc (Lcom/squareup/kotlinpoet/CodeBlock;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public final fun addKdoc (Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public final fun addModifiers (Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public final fun addModifiers ([Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public synthetic fun addOriginatingElement (Ljavax/lang/model/element/Element;)Lcom/squareup/kotlinpoet/OriginatingElementsHolder$Builder; + public fun addOriginatingElement (Ljavax/lang/model/element/Element;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public final fun addTypeVariable (Lcom/squareup/kotlinpoet/TypeVariableName;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public final fun addTypeVariables (Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public final fun build ()Lcom/squareup/kotlinpoet/PropertySpec; + public synthetic fun contextReceivers (Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/ContextReceivable$Builder; + public synthetic fun contextReceivers ([Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/ContextReceivable$Builder; + public final fun delegate (Lcom/squareup/kotlinpoet/CodeBlock;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public final fun delegate (Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public final fun getAnnotations ()Ljava/util/List; + public fun getContextReceiverTypes ()Ljava/util/List; + public final fun getModifiers ()Ljava/util/List; + public fun getOriginatingElements ()Ljava/util/List; + public fun getTags ()Ljava/util/Map; + public final fun getTypeVariables ()Ljava/util/List; + public final fun getter (Lcom/squareup/kotlinpoet/FunSpec;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public final fun initializer (Lcom/squareup/kotlinpoet/CodeBlock;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public final fun initializer (Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public final fun mutable (Z)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public static synthetic fun mutable$default (Lcom/squareup/kotlinpoet/PropertySpec$Builder;ZILjava/lang/Object;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public final fun receiver (Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public final fun receiver (Ljava/lang/reflect/Type;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public final fun receiver (Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public final fun setter (Lcom/squareup/kotlinpoet/FunSpec;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public fun tag (Ljava/lang/Class;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public synthetic fun tag (Ljava/lang/Class;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/Taggable$Builder; + public fun tag (Lkotlin/reflect/KClass;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public synthetic fun tag (Lkotlin/reflect/KClass;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/Taggable$Builder; +} + +public final class com/squareup/kotlinpoet/PropertySpec$Companion { + public final fun builder (Ljava/lang/String;Lcom/squareup/kotlinpoet/TypeName;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public final fun builder (Ljava/lang/String;Lcom/squareup/kotlinpoet/TypeName;[Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public final fun builder (Ljava/lang/String;Ljava/lang/reflect/Type;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public final fun builder (Ljava/lang/String;Ljava/lang/reflect/Type;[Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public final fun builder (Ljava/lang/String;Lkotlin/reflect/KClass;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public final fun builder (Ljava/lang/String;Lkotlin/reflect/KClass;[Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; +} + +public abstract interface class com/squareup/kotlinpoet/Taggable { + public abstract fun getTags ()Ljava/util/Map; + public abstract fun tag (Ljava/lang/Class;)Ljava/lang/Object; + public abstract fun tag (Lkotlin/reflect/KClass;)Ljava/lang/Object; +} + +public abstract interface class com/squareup/kotlinpoet/Taggable$Builder { + public abstract fun getTags ()Ljava/util/Map; + public abstract fun tag (Ljava/lang/Class;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/Taggable$Builder; + public abstract fun tag (Lkotlin/reflect/KClass;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/Taggable$Builder; +} + +public final class com/squareup/kotlinpoet/Taggable$Builder$DefaultImpls { + public static fun tag (Lcom/squareup/kotlinpoet/Taggable$Builder;Ljava/lang/Class;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/Taggable$Builder; + public static fun tag (Lcom/squareup/kotlinpoet/Taggable$Builder;Lkotlin/reflect/KClass;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/Taggable$Builder; +} + +public final class com/squareup/kotlinpoet/Taggable$DefaultImpls { + public static fun getTags (Lcom/squareup/kotlinpoet/Taggable;)Ljava/util/Map; + public static fun tag (Lcom/squareup/kotlinpoet/Taggable;Ljava/lang/Class;)Ljava/lang/Object; + public static fun tag (Lcom/squareup/kotlinpoet/Taggable;Lkotlin/reflect/KClass;)Ljava/lang/Object; +} + +public final class com/squareup/kotlinpoet/TypeAliasSpec : com/squareup/kotlinpoet/Taggable { + public static final field Companion Lcom/squareup/kotlinpoet/TypeAliasSpec$Companion; + public static final fun builder (Ljava/lang/String;Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/TypeAliasSpec$Builder; + public static final fun builder (Ljava/lang/String;Ljava/lang/reflect/Type;)Lcom/squareup/kotlinpoet/TypeAliasSpec$Builder; + public static final fun builder (Ljava/lang/String;Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/TypeAliasSpec$Builder; + public fun equals (Ljava/lang/Object;)Z + public final fun getAnnotations ()Ljava/util/List; + public final fun getKdoc ()Lcom/squareup/kotlinpoet/CodeBlock; + public final fun getModifiers ()Ljava/util/Set; + public final fun getName ()Ljava/lang/String; + public fun getTags ()Ljava/util/Map; + public final fun getType ()Lcom/squareup/kotlinpoet/TypeName; + public final fun getTypeVariables ()Ljava/util/List; + public fun hashCode ()I + public fun tag (Ljava/lang/Class;)Ljava/lang/Object; + public fun tag (Lkotlin/reflect/KClass;)Ljava/lang/Object; + public final fun toBuilder ()Lcom/squareup/kotlinpoet/TypeAliasSpec$Builder; + public final fun toBuilder (Ljava/lang/String;)Lcom/squareup/kotlinpoet/TypeAliasSpec$Builder; + public final fun toBuilder (Ljava/lang/String;Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/TypeAliasSpec$Builder; + public static synthetic fun toBuilder$default (Lcom/squareup/kotlinpoet/TypeAliasSpec;Ljava/lang/String;Lcom/squareup/kotlinpoet/TypeName;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/TypeAliasSpec$Builder; + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/kotlinpoet/TypeAliasSpec$Builder : com/squareup/kotlinpoet/Taggable$Builder { + public final fun addAnnotation (Lcom/squareup/kotlinpoet/AnnotationSpec;)Lcom/squareup/kotlinpoet/TypeAliasSpec$Builder; + public final fun addAnnotation (Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/TypeAliasSpec$Builder; + public final fun addAnnotation (Ljava/lang/Class;)Lcom/squareup/kotlinpoet/TypeAliasSpec$Builder; + public final fun addAnnotation (Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/TypeAliasSpec$Builder; + public final fun addAnnotations (Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/TypeAliasSpec$Builder; + public final fun addKdoc (Lcom/squareup/kotlinpoet/CodeBlock;)Lcom/squareup/kotlinpoet/TypeAliasSpec$Builder; + public final fun addKdoc (Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/TypeAliasSpec$Builder; + public final fun addModifiers (Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/TypeAliasSpec$Builder; + public final fun addModifiers ([Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/TypeAliasSpec$Builder; + public final fun addTypeVariable (Lcom/squareup/kotlinpoet/TypeVariableName;)Lcom/squareup/kotlinpoet/TypeAliasSpec$Builder; + public final fun addTypeVariables (Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/TypeAliasSpec$Builder; + public final fun build ()Lcom/squareup/kotlinpoet/TypeAliasSpec; + public final fun getAnnotations ()Ljava/util/List; + public final fun getModifiers ()Ljava/util/Set; + public fun getTags ()Ljava/util/Map; + public final fun getTypeVariables ()Ljava/util/Set; + public synthetic fun tag (Ljava/lang/Class;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/Taggable$Builder; + public fun tag (Ljava/lang/Class;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/TypeAliasSpec$Builder; + public synthetic fun tag (Lkotlin/reflect/KClass;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/Taggable$Builder; + public fun tag (Lkotlin/reflect/KClass;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/TypeAliasSpec$Builder; +} + +public final class com/squareup/kotlinpoet/TypeAliasSpec$Companion { + public final fun builder (Ljava/lang/String;Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/TypeAliasSpec$Builder; + public final fun builder (Ljava/lang/String;Ljava/lang/reflect/Type;)Lcom/squareup/kotlinpoet/TypeAliasSpec$Builder; + public final fun builder (Ljava/lang/String;Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/TypeAliasSpec$Builder; +} + +public abstract class com/squareup/kotlinpoet/TypeName : com/squareup/kotlinpoet/Taggable { + public static final field Companion Lcom/squareup/kotlinpoet/TypeName$Companion; + public synthetic fun <init> (ZLjava/util/List;Ljava/util/Map;Lkotlin/jvm/internal/DefaultConstructorMarker;)V + public final fun copy (ZLjava/util/List;)Lcom/squareup/kotlinpoet/TypeName; + public abstract fun copy (ZLjava/util/List;Ljava/util/Map;)Lcom/squareup/kotlinpoet/TypeName; + public static synthetic fun copy$default (Lcom/squareup/kotlinpoet/TypeName;ZLjava/util/List;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/TypeName; + public static synthetic fun copy$default (Lcom/squareup/kotlinpoet/TypeName;ZLjava/util/List;Ljava/util/Map;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/TypeName; + public fun equals (Ljava/lang/Object;)Z + public final fun getAnnotations ()Ljava/util/List; + public fun getTags ()Ljava/util/Map; + public fun hashCode ()I + public final fun isAnnotated ()Z + public final fun isNullable ()Z + public fun tag (Ljava/lang/Class;)Ljava/lang/Object; + public fun tag (Lkotlin/reflect/KClass;)Ljava/lang/Object; + public fun toString ()Ljava/lang/String; +} + +public final class com/squareup/kotlinpoet/TypeName$Companion { +} + +public final class com/squareup/kotlinpoet/TypeNames { + public static final field ANNOTATION Lcom/squareup/kotlinpoet/ClassName; + public static final field ANY Lcom/squareup/kotlinpoet/ClassName; + public static final field ARRAY Lcom/squareup/kotlinpoet/ClassName; + public static final field BOOLEAN Lcom/squareup/kotlinpoet/ClassName; + public static final field BOOLEAN_ARRAY Lcom/squareup/kotlinpoet/ClassName; + public static final field BYTE Lcom/squareup/kotlinpoet/ClassName; + public static final field BYTE_ARRAY Lcom/squareup/kotlinpoet/ClassName; + public static final field CHAR Lcom/squareup/kotlinpoet/ClassName; + public static final field CHAR_ARRAY Lcom/squareup/kotlinpoet/ClassName; + public static final field CHAR_SEQUENCE Lcom/squareup/kotlinpoet/ClassName; + public static final field COLLECTION Lcom/squareup/kotlinpoet/ClassName; + public static final field COMPARABLE Lcom/squareup/kotlinpoet/ClassName; + public static final field DOUBLE Lcom/squareup/kotlinpoet/ClassName; + public static final field DOUBLE_ARRAY Lcom/squareup/kotlinpoet/ClassName; + public static final field DYNAMIC Lcom/squareup/kotlinpoet/Dynamic; + public static final field ENUM Lcom/squareup/kotlinpoet/ClassName; + public static final field FLOAT Lcom/squareup/kotlinpoet/ClassName; + public static final field FLOAT_ARRAY Lcom/squareup/kotlinpoet/ClassName; + public static final field INT Lcom/squareup/kotlinpoet/ClassName; + public static final field INT_ARRAY Lcom/squareup/kotlinpoet/ClassName; + public static final field ITERABLE Lcom/squareup/kotlinpoet/ClassName; + public static final field LIST Lcom/squareup/kotlinpoet/ClassName; + public static final field LONG Lcom/squareup/kotlinpoet/ClassName; + public static final field LONG_ARRAY Lcom/squareup/kotlinpoet/ClassName; + public static final field MAP Lcom/squareup/kotlinpoet/ClassName; + public static final field MAP_ENTRY Lcom/squareup/kotlinpoet/ClassName; + public static final field MUTABLE_COLLECTION Lcom/squareup/kotlinpoet/ClassName; + public static final field MUTABLE_ITERABLE Lcom/squareup/kotlinpoet/ClassName; + public static final field MUTABLE_LIST Lcom/squareup/kotlinpoet/ClassName; + public static final field MUTABLE_MAP Lcom/squareup/kotlinpoet/ClassName; + public static final field MUTABLE_MAP_ENTRY Lcom/squareup/kotlinpoet/ClassName; + public static final field MUTABLE_SET Lcom/squareup/kotlinpoet/ClassName; + public static final field NOTHING Lcom/squareup/kotlinpoet/ClassName; + public static final field NUMBER Lcom/squareup/kotlinpoet/ClassName; + public static final field SET Lcom/squareup/kotlinpoet/ClassName; + public static final field SHORT Lcom/squareup/kotlinpoet/ClassName; + public static final field SHORT_ARRAY Lcom/squareup/kotlinpoet/ClassName; + public static final field STAR Lcom/squareup/kotlinpoet/WildcardTypeName; + public static final field STRING Lcom/squareup/kotlinpoet/ClassName; + public static final field THROWABLE Lcom/squareup/kotlinpoet/ClassName; + public static final field UNIT Lcom/squareup/kotlinpoet/ClassName; + public static final field U_BYTE Lcom/squareup/kotlinpoet/ClassName; + public static final field U_BYTE_ARRAY Lcom/squareup/kotlinpoet/ClassName; + public static final field U_INT Lcom/squareup/kotlinpoet/ClassName; + public static final field U_INT_ARRAY Lcom/squareup/kotlinpoet/ClassName; + public static final field U_LONG Lcom/squareup/kotlinpoet/ClassName; + public static final field U_LONG_ARRAY Lcom/squareup/kotlinpoet/ClassName; + public static final field U_SHORT Lcom/squareup/kotlinpoet/ClassName; + public static final field U_SHORT_ARRAY Lcom/squareup/kotlinpoet/ClassName; + public static final fun get (Ljava/lang/reflect/Type;)Lcom/squareup/kotlinpoet/TypeName; + public static final fun get (Ljavax/lang/model/type/TypeMirror;)Lcom/squareup/kotlinpoet/TypeName; + public static final fun get (Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/ClassName; +} + +public final class com/squareup/kotlinpoet/TypeSpec : com/squareup/kotlinpoet/ContextReceivable, com/squareup/kotlinpoet/OriginatingElementsHolder, com/squareup/kotlinpoet/Taggable { + public static final field Companion Lcom/squareup/kotlinpoet/TypeSpec$Companion; + public static final fun annotationBuilder (Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public static final fun annotationBuilder (Ljava/lang/String;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public static final fun anonymousClassBuilder ()Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public static final fun classBuilder (Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public static final fun classBuilder (Ljava/lang/String;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public static final fun companionObjectBuilder ()Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public static final fun companionObjectBuilder (Ljava/lang/String;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public static final fun enumBuilder (Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public static final fun enumBuilder (Ljava/lang/String;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public fun equals (Ljava/lang/Object;)Z + public static final fun expectClassBuilder (Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public static final fun expectClassBuilder (Ljava/lang/String;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public static final fun funInterfaceBuilder (Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public static final fun funInterfaceBuilder (Ljava/lang/String;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun getAnnotationSpecs ()Ljava/util/List; + public fun getContextReceiverTypes ()Ljava/util/List; + public final fun getEnumConstants ()Ljava/util/Map; + public final fun getFunSpecs ()Ljava/util/List; + public final fun getInitializerBlock ()Lcom/squareup/kotlinpoet/CodeBlock; + public final fun getInitializerIndex ()I + public final fun getKdoc ()Lcom/squareup/kotlinpoet/CodeBlock; + public final fun getKind ()Lcom/squareup/kotlinpoet/TypeSpec$Kind; + public final fun getModifiers ()Ljava/util/Set; + public final fun getName ()Ljava/lang/String; + public fun getOriginatingElements ()Ljava/util/List; + public final fun getPrimaryConstructor ()Lcom/squareup/kotlinpoet/FunSpec; + public final fun getPropertySpecs ()Ljava/util/List; + public final fun getSuperclass ()Lcom/squareup/kotlinpoet/TypeName; + public final fun getSuperclassConstructorParameters ()Ljava/util/List; + public final fun getSuperinterfaces ()Ljava/util/Map; + public fun getTags ()Ljava/util/Map; + public final fun getTypeSpecs ()Ljava/util/List; + public final fun getTypeVariables ()Ljava/util/List; + public fun hashCode ()I + public static final fun interfaceBuilder (Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public static final fun interfaceBuilder (Ljava/lang/String;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun isAnnotation ()Z + public final fun isAnonymousClass ()Z + public final fun isCompanion ()Z + public final fun isEnum ()Z + public final fun isFunctionalInterface ()Z + public static final fun objectBuilder (Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public static final fun objectBuilder (Ljava/lang/String;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public fun tag (Ljava/lang/Class;)Ljava/lang/Object; + public fun tag (Lkotlin/reflect/KClass;)Ljava/lang/Object; + public final fun toBuilder ()Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun toBuilder (Lcom/squareup/kotlinpoet/TypeSpec$Kind;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun toBuilder (Lcom/squareup/kotlinpoet/TypeSpec$Kind;Ljava/lang/String;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public static synthetic fun toBuilder$default (Lcom/squareup/kotlinpoet/TypeSpec;Lcom/squareup/kotlinpoet/TypeSpec$Kind;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public fun toString ()Ljava/lang/String; + public static final fun valueClassBuilder (Ljava/lang/String;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; +} + +public final class com/squareup/kotlinpoet/TypeSpec$Builder : com/squareup/kotlinpoet/ContextReceivable$Builder, com/squareup/kotlinpoet/OriginatingElementsHolder$Builder, com/squareup/kotlinpoet/Taggable$Builder { + public final fun addAnnotation (Lcom/squareup/kotlinpoet/AnnotationSpec;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addAnnotation (Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addAnnotation (Ljava/lang/Class;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addAnnotation (Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addAnnotations (Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addEnumConstant (Ljava/lang/String;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addEnumConstant (Ljava/lang/String;Lcom/squareup/kotlinpoet/TypeSpec;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public static synthetic fun addEnumConstant$default (Lcom/squareup/kotlinpoet/TypeSpec$Builder;Ljava/lang/String;Lcom/squareup/kotlinpoet/TypeSpec;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addFunction (Lcom/squareup/kotlinpoet/FunSpec;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addFunctions (Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addInitializerBlock (Lcom/squareup/kotlinpoet/CodeBlock;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addKdoc (Lcom/squareup/kotlinpoet/CodeBlock;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addKdoc (Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addModifiers (Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addModifiers ([Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public synthetic fun addOriginatingElement (Ljavax/lang/model/element/Element;)Lcom/squareup/kotlinpoet/OriginatingElementsHolder$Builder; + public fun addOriginatingElement (Ljavax/lang/model/element/Element;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addProperties (Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addProperty (Lcom/squareup/kotlinpoet/PropertySpec;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addProperty (Ljava/lang/String;Lcom/squareup/kotlinpoet/TypeName;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addProperty (Ljava/lang/String;Lcom/squareup/kotlinpoet/TypeName;[Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addProperty (Ljava/lang/String;Ljava/lang/reflect/Type;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addProperty (Ljava/lang/String;Ljava/lang/reflect/Type;[Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addProperty (Ljava/lang/String;Lkotlin/reflect/KClass;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addProperty (Ljava/lang/String;Lkotlin/reflect/KClass;[Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addSuperclassConstructorParameter (Lcom/squareup/kotlinpoet/CodeBlock;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addSuperclassConstructorParameter (Ljava/lang/String;[Ljava/lang/Object;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addSuperinterface (Lcom/squareup/kotlinpoet/TypeName;Lcom/squareup/kotlinpoet/CodeBlock;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addSuperinterface (Lcom/squareup/kotlinpoet/TypeName;Ljava/lang/String;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addSuperinterface (Ljava/lang/reflect/Type;Lcom/squareup/kotlinpoet/CodeBlock;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addSuperinterface (Lkotlin/reflect/KClass;Lcom/squareup/kotlinpoet/CodeBlock;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addSuperinterface (Lkotlin/reflect/KClass;Ljava/lang/String;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public static synthetic fun addSuperinterface$default (Lcom/squareup/kotlinpoet/TypeSpec$Builder;Lcom/squareup/kotlinpoet/TypeName;Lcom/squareup/kotlinpoet/CodeBlock;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public static synthetic fun addSuperinterface$default (Lcom/squareup/kotlinpoet/TypeSpec$Builder;Ljava/lang/reflect/Type;Lcom/squareup/kotlinpoet/CodeBlock;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public static synthetic fun addSuperinterface$default (Lcom/squareup/kotlinpoet/TypeSpec$Builder;Lkotlin/reflect/KClass;Lcom/squareup/kotlinpoet/CodeBlock;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addSuperinterfaces (Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addType (Lcom/squareup/kotlinpoet/TypeSpec;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addTypeVariable (Lcom/squareup/kotlinpoet/TypeVariableName;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addTypeVariables (Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun addTypes (Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun build ()Lcom/squareup/kotlinpoet/TypeSpec; + public synthetic fun contextReceivers (Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/ContextReceivable$Builder; + public synthetic fun contextReceivers ([Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/ContextReceivable$Builder; + public final fun getAnnotationSpecs ()Ljava/util/List; + public final fun getEnumConstants ()Ljava/util/Map; + public final fun getFunSpecs ()Ljava/util/List; + public final fun getInitializerIndex ()I + public final fun getModifiers ()Ljava/util/Set; + public fun getOriginatingElements ()Ljava/util/List; + public final fun getPropertySpecs ()Ljava/util/List; + public final fun getSuperclassConstructorParameters ()Ljava/util/List; + public final fun getSuperinterfaces ()Ljava/util/Map; + public fun getTags ()Ljava/util/Map; + public final fun getTypeSpecs ()Ljava/util/List; + public final fun getTypeVariables ()Ljava/util/List; + public final fun primaryConstructor (Lcom/squareup/kotlinpoet/FunSpec;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun setInitializerIndex (I)V + public final fun superclass (Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun superclass (Ljava/lang/reflect/Type;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun superclass (Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public synthetic fun tag (Ljava/lang/Class;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/Taggable$Builder; + public fun tag (Ljava/lang/Class;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public synthetic fun tag (Lkotlin/reflect/KClass;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/Taggable$Builder; + public fun tag (Lkotlin/reflect/KClass;Ljava/lang/Object;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; +} + +public final class com/squareup/kotlinpoet/TypeSpec$Companion { + public final fun annotationBuilder (Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun annotationBuilder (Ljava/lang/String;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun anonymousClassBuilder ()Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun classBuilder (Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun classBuilder (Ljava/lang/String;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun companionObjectBuilder ()Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun companionObjectBuilder (Ljava/lang/String;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public static synthetic fun companionObjectBuilder$default (Lcom/squareup/kotlinpoet/TypeSpec$Companion;Ljava/lang/String;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun enumBuilder (Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun enumBuilder (Ljava/lang/String;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun expectClassBuilder (Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun expectClassBuilder (Ljava/lang/String;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun funInterfaceBuilder (Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun funInterfaceBuilder (Ljava/lang/String;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun interfaceBuilder (Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun interfaceBuilder (Ljava/lang/String;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun objectBuilder (Lcom/squareup/kotlinpoet/ClassName;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun objectBuilder (Ljava/lang/String;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public final fun valueClassBuilder (Ljava/lang/String;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; +} + +public final class com/squareup/kotlinpoet/TypeSpec$Kind : java/lang/Enum { + public static final field CLASS Lcom/squareup/kotlinpoet/TypeSpec$Kind; + public static final field INTERFACE Lcom/squareup/kotlinpoet/TypeSpec$Kind; + public static final field OBJECT Lcom/squareup/kotlinpoet/TypeSpec$Kind; + public static fun valueOf (Ljava/lang/String;)Lcom/squareup/kotlinpoet/TypeSpec$Kind; + public static fun values ()[Lcom/squareup/kotlinpoet/TypeSpec$Kind; +} + +public final class com/squareup/kotlinpoet/TypeVariableName : com/squareup/kotlinpoet/TypeName { + public static final field Companion Lcom/squareup/kotlinpoet/TypeVariableName$Companion; + public final fun copy (ZLjava/util/List;Ljava/util/List;ZLjava/util/Map;)Lcom/squareup/kotlinpoet/TypeVariableName; + public synthetic fun copy (ZLjava/util/List;Ljava/util/Map;)Lcom/squareup/kotlinpoet/TypeName; + public fun copy (ZLjava/util/List;Ljava/util/Map;)Lcom/squareup/kotlinpoet/TypeVariableName; + public static synthetic fun copy$default (Lcom/squareup/kotlinpoet/TypeVariableName;ZLjava/util/List;Ljava/util/List;ZLjava/util/Map;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/TypeVariableName; + public static final fun get (Ljava/lang/String;)Lcom/squareup/kotlinpoet/TypeVariableName; + public static final fun get (Ljava/lang/String;Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/TypeVariableName; + public static final fun get (Ljava/lang/String;Ljava/util/List;)Lcom/squareup/kotlinpoet/TypeVariableName; + public static final fun get (Ljava/lang/String;Ljava/util/List;Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/TypeVariableName; + public static final fun get (Ljava/lang/String;[Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/TypeVariableName; + public static final fun get (Ljava/lang/String;[Lcom/squareup/kotlinpoet/TypeName;Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/TypeVariableName; + public static final fun get (Ljava/lang/String;[Ljava/lang/reflect/Type;)Lcom/squareup/kotlinpoet/TypeVariableName; + public static final fun get (Ljava/lang/String;[Ljava/lang/reflect/Type;Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/TypeVariableName; + public static final fun get (Ljava/lang/String;[Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/TypeVariableName; + public static final fun get (Ljava/lang/String;[Lkotlin/reflect/KClass;Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/TypeVariableName; + public final fun getBounds ()Ljava/util/List; + public final fun getName ()Ljava/lang/String; + public final fun getVariance ()Lcom/squareup/kotlinpoet/KModifier; + public static final fun getWithClasses (Ljava/lang/String;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/TypeVariableName; + public static final fun getWithClasses (Ljava/lang/String;Ljava/lang/Iterable;Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/TypeVariableName; + public static final fun getWithTypes (Ljava/lang/String;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/TypeVariableName; + public static final fun getWithTypes (Ljava/lang/String;Ljava/lang/Iterable;Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/TypeVariableName; + public final fun isReified ()Z +} + +public final class com/squareup/kotlinpoet/TypeVariableName$Companion { + public final fun get (Ljava/lang/String;)Lcom/squareup/kotlinpoet/TypeVariableName; + public final fun get (Ljava/lang/String;Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/TypeVariableName; + public final fun get (Ljava/lang/String;Ljava/util/List;)Lcom/squareup/kotlinpoet/TypeVariableName; + public final fun get (Ljava/lang/String;Ljava/util/List;Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/TypeVariableName; + public final fun get (Ljava/lang/String;[Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/TypeVariableName; + public final fun get (Ljava/lang/String;[Lcom/squareup/kotlinpoet/TypeName;Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/TypeVariableName; + public final fun get (Ljava/lang/String;[Ljava/lang/reflect/Type;)Lcom/squareup/kotlinpoet/TypeVariableName; + public final fun get (Ljava/lang/String;[Ljava/lang/reflect/Type;Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/TypeVariableName; + public final fun get (Ljava/lang/String;[Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/TypeVariableName; + public final fun get (Ljava/lang/String;[Lkotlin/reflect/KClass;Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/TypeVariableName; + public static synthetic fun get$default (Lcom/squareup/kotlinpoet/TypeVariableName$Companion;Ljava/lang/String;Lcom/squareup/kotlinpoet/KModifier;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/TypeVariableName; + public static synthetic fun get$default (Lcom/squareup/kotlinpoet/TypeVariableName$Companion;Ljava/lang/String;Ljava/util/List;Lcom/squareup/kotlinpoet/KModifier;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/TypeVariableName; + public static synthetic fun get$default (Lcom/squareup/kotlinpoet/TypeVariableName$Companion;Ljava/lang/String;[Lcom/squareup/kotlinpoet/TypeName;Lcom/squareup/kotlinpoet/KModifier;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/TypeVariableName; + public static synthetic fun get$default (Lcom/squareup/kotlinpoet/TypeVariableName$Companion;Ljava/lang/String;[Ljava/lang/reflect/Type;Lcom/squareup/kotlinpoet/KModifier;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/TypeVariableName; + public static synthetic fun get$default (Lcom/squareup/kotlinpoet/TypeVariableName$Companion;Ljava/lang/String;[Lkotlin/reflect/KClass;Lcom/squareup/kotlinpoet/KModifier;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/TypeVariableName; + public final fun getWithClasses (Ljava/lang/String;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/TypeVariableName; + public final fun getWithClasses (Ljava/lang/String;Ljava/lang/Iterable;Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/TypeVariableName; + public static synthetic fun getWithClasses$default (Lcom/squareup/kotlinpoet/TypeVariableName$Companion;Ljava/lang/String;Ljava/lang/Iterable;Lcom/squareup/kotlinpoet/KModifier;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/TypeVariableName; + public final fun getWithTypes (Ljava/lang/String;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/TypeVariableName; + public final fun getWithTypes (Ljava/lang/String;Ljava/lang/Iterable;Lcom/squareup/kotlinpoet/KModifier;)Lcom/squareup/kotlinpoet/TypeVariableName; + public static synthetic fun getWithTypes$default (Lcom/squareup/kotlinpoet/TypeVariableName$Companion;Ljava/lang/String;Ljava/lang/Iterable;Lcom/squareup/kotlinpoet/KModifier;ILjava/lang/Object;)Lcom/squareup/kotlinpoet/TypeVariableName; +} + +public final class com/squareup/kotlinpoet/TypeVariableNames { + public static final fun asTypeVariableName (Lkotlin/reflect/KTypeParameter;)Lcom/squareup/kotlinpoet/TypeVariableName; + public static final fun get (Ljavax/lang/model/element/TypeParameterElement;)Lcom/squareup/kotlinpoet/TypeVariableName; + public static final fun get (Ljavax/lang/model/type/TypeVariable;)Lcom/squareup/kotlinpoet/TypeVariableName; +} + +public final class com/squareup/kotlinpoet/WildcardTypeName : com/squareup/kotlinpoet/TypeName { + public static final field Companion Lcom/squareup/kotlinpoet/WildcardTypeName$Companion; + public static final fun consumerOf (Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/WildcardTypeName; + public static final fun consumerOf (Ljava/lang/reflect/Type;)Lcom/squareup/kotlinpoet/WildcardTypeName; + public static final fun consumerOf (Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/WildcardTypeName; + public synthetic fun copy (ZLjava/util/List;Ljava/util/Map;)Lcom/squareup/kotlinpoet/TypeName; + public fun copy (ZLjava/util/List;Ljava/util/Map;)Lcom/squareup/kotlinpoet/WildcardTypeName; + public final fun getInTypes ()Ljava/util/List; + public final fun getOutTypes ()Ljava/util/List; + public static final fun producerOf (Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/WildcardTypeName; + public static final fun producerOf (Ljava/lang/reflect/Type;)Lcom/squareup/kotlinpoet/WildcardTypeName; + public static final fun producerOf (Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/WildcardTypeName; +} + +public final class com/squareup/kotlinpoet/WildcardTypeName$Companion { + public final fun consumerOf (Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/WildcardTypeName; + public final fun consumerOf (Ljava/lang/reflect/Type;)Lcom/squareup/kotlinpoet/WildcardTypeName; + public final fun consumerOf (Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/WildcardTypeName; + public final fun producerOf (Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/WildcardTypeName; + public final fun producerOf (Ljava/lang/reflect/Type;)Lcom/squareup/kotlinpoet/WildcardTypeName; + public final fun producerOf (Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/WildcardTypeName; +} + +public final class com/squareup/kotlinpoet/WildcardTypeNames { + public static final fun get (Ljava/lang/reflect/WildcardType;)Lcom/squareup/kotlinpoet/TypeName; + public static final fun get (Ljavax/lang/model/type/WildcardType;)Lcom/squareup/kotlinpoet/TypeName; +} + +public final class com/squareup/kotlinpoet/jvm/JvmAnnotations { + public static final fun jvmDefault (Lcom/squareup/kotlinpoet/FunSpec$Builder;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public static final fun jvmDefault (Lcom/squareup/kotlinpoet/PropertySpec$Builder;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public static final fun jvmField (Lcom/squareup/kotlinpoet/PropertySpec$Builder;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public static final fun jvmInline (Lcom/squareup/kotlinpoet/TypeSpec$Builder;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public static final fun jvmMultifileClass (Lcom/squareup/kotlinpoet/FileSpec$Builder;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public static final fun jvmName (Lcom/squareup/kotlinpoet/FileSpec$Builder;Ljava/lang/String;)Lcom/squareup/kotlinpoet/FileSpec$Builder; + public static final fun jvmName (Lcom/squareup/kotlinpoet/FunSpec$Builder;Ljava/lang/String;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public static final fun jvmOverloads (Lcom/squareup/kotlinpoet/FunSpec$Builder;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public static final fun jvmRecord (Lcom/squareup/kotlinpoet/TypeSpec$Builder;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public static final fun jvmStatic (Lcom/squareup/kotlinpoet/FunSpec$Builder;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public static final fun jvmStatic (Lcom/squareup/kotlinpoet/PropertySpec$Builder;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public static final fun jvmSuppressWildcards (Lcom/squareup/kotlinpoet/FunSpec$Builder;Z)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public static final fun jvmSuppressWildcards (Lcom/squareup/kotlinpoet/PropertySpec$Builder;Z)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public static final fun jvmSuppressWildcards (Lcom/squareup/kotlinpoet/TypeName;Z)Lcom/squareup/kotlinpoet/TypeName; + public static final fun jvmSuppressWildcards (Lcom/squareup/kotlinpoet/TypeSpec$Builder;Z)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public static synthetic fun jvmSuppressWildcards$default (Lcom/squareup/kotlinpoet/FunSpec$Builder;ZILjava/lang/Object;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public static synthetic fun jvmSuppressWildcards$default (Lcom/squareup/kotlinpoet/PropertySpec$Builder;ZILjava/lang/Object;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public static synthetic fun jvmSuppressWildcards$default (Lcom/squareup/kotlinpoet/TypeName;ZILjava/lang/Object;)Lcom/squareup/kotlinpoet/TypeName; + public static synthetic fun jvmSuppressWildcards$default (Lcom/squareup/kotlinpoet/TypeSpec$Builder;ZILjava/lang/Object;)Lcom/squareup/kotlinpoet/TypeSpec$Builder; + public static final fun jvmWildcard (Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/TypeName; + public static final fun strictfp (Lcom/squareup/kotlinpoet/FunSpec$Builder;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public static final fun synchronized (Lcom/squareup/kotlinpoet/FunSpec$Builder;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public static final fun throws (Lcom/squareup/kotlinpoet/FunSpec$Builder;Ljava/lang/Iterable;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public static final fun throws (Lcom/squareup/kotlinpoet/FunSpec$Builder;[Lcom/squareup/kotlinpoet/TypeName;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public static final fun throws (Lcom/squareup/kotlinpoet/FunSpec$Builder;[Ljava/lang/reflect/Type;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public static final fun throws (Lcom/squareup/kotlinpoet/FunSpec$Builder;[Lkotlin/reflect/KClass;)Lcom/squareup/kotlinpoet/FunSpec$Builder; + public static final fun transient (Lcom/squareup/kotlinpoet/PropertySpec$Builder;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; + public static final fun volatile (Lcom/squareup/kotlinpoet/PropertySpec$Builder;)Lcom/squareup/kotlinpoet/PropertySpec$Builder; +} + +public final class com/squareup/kotlinpoet/tags/TypeAliasTag { + public fun <init> (Lcom/squareup/kotlinpoet/TypeName;)V + public final fun getAbbreviatedType ()Lcom/squareup/kotlinpoet/TypeName; +} + diff --git a/kotlinpoet/build.gradle.kts b/kotlinpoet/build.gradle.kts new file mode 100644 index 00000000..830f870f --- /dev/null +++ b/kotlinpoet/build.gradle.kts @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +tasks.jar { + manifest { + attributes("Automatic-Module-Name" to "com.squareup.kotlinpoet") + } +} + +tasks.compileTestKotlin { + kotlinOptions { + freeCompilerArgs = listOf("-opt-in=com.squareup.kotlinpoet.DelicateKotlinPoetApi") + } +} + +spotless { + kotlin { + targetExclude( + // Non-Square licensed files + "src/main/java/com/squareup/kotlinpoet/ClassName.kt", + "src/test/java/com/squareup/kotlinpoet/AbstractTypesTest.kt", + "src/test/java/com/squareup/kotlinpoet/ClassNameTest.kt", + "src/test/java/com/squareup/kotlinpoet/TypesEclipseTest.kt", + "src/test/java/com/squareup/kotlinpoet/TypesTest.kt", + ) + } +} + +dependencies { + implementation(libs.kotlin.reflect) + testImplementation(libs.kotlin.junit) + testImplementation(libs.truth) + testImplementation(libs.compileTesting) + testImplementation(libs.jimfs) + testImplementation(libs.ecj) + testImplementation(libs.kotlinCompileTesting) + testImplementation(libs.kotlin.annotationProcessingEmbeddable) + testImplementation(libs.kotlin.compilerEmbeddable) +} diff --git a/kotlinpoet/gradle.properties b/kotlinpoet/gradle.properties new file mode 100644 index 00000000..7258dd19 --- /dev/null +++ b/kotlinpoet/gradle.properties @@ -0,0 +1,4 @@ +POM_ARTIFACT_ID=kotlinpoet +POM_NAME=KotlinPoet +POM_DESCRIPTION=Use beautiful Kotlin code to generate beautiful Kotlin code. +POM_PACKAGING=jar diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/AnnotationSpec.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/AnnotationSpec.kt new file mode 100644 index 00000000..d44264b6 --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/AnnotationSpec.kt @@ -0,0 +1,269 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import java.lang.reflect.Array +import java.util.Objects +import javax.lang.model.element.AnnotationMirror +import javax.lang.model.element.AnnotationValue +import javax.lang.model.element.TypeElement +import javax.lang.model.element.VariableElement +import javax.lang.model.type.TypeMirror +import javax.lang.model.util.SimpleAnnotationValueVisitor7 +import kotlin.reflect.KClass + +/** A generated annotation on a declaration. */ +public class AnnotationSpec private constructor( + builder: Builder, + private val tagMap: TagMap = builder.buildTagMap(), +) : Taggable by tagMap { + @Deprecated( + message = "Use typeName instead. This property will be removed in KotlinPoet 2.0.", + replaceWith = ReplaceWith("typeName"), + ) + public val className: ClassName + get() = typeName as? ClassName ?: error("ClassName is not available. Call typeName instead.") + public val typeName: TypeName = builder.typeName + public val members: List<CodeBlock> = builder.members.toImmutableList() + public val useSiteTarget: UseSiteTarget? = builder.useSiteTarget + + internal fun emit(codeWriter: CodeWriter, inline: Boolean, asParameter: Boolean = false) { + if (!asParameter) { + codeWriter.emit("@") + } + if (useSiteTarget != null) { + codeWriter.emit(useSiteTarget.keyword + ":") + } + codeWriter.emitCode("%T", typeName) + + if (members.isEmpty() && !asParameter) { + // @Singleton + return + } + + val whitespace = if (inline) "" else "\n" + val memberSeparator = if (inline) ", " else ",\n" + val memberSuffix = if (!inline && members.size > 1) "," else "" + + // Inline: + // @Column(name = "updated_at", nullable = false) + // + // Not inline: + // @Column( + // name = "updated_at", + // nullable = false, + // ) + + codeWriter.emit("(") + if (members.size > 1) codeWriter.emit(whitespace).indent(1) + codeWriter.emitCode( + codeBlock = members + .map { if (inline) it.replaceAll("[⇥|⇤]", "") else it } + .joinToCode(separator = memberSeparator, suffix = memberSuffix), + isConstantContext = true, + ) + if (members.size > 1) codeWriter.unindent(1).emit(whitespace) + codeWriter.emit(")") + } + + public fun toBuilder(): Builder { + val builder = Builder(typeName) + builder.members += members + builder.useSiteTarget = useSiteTarget + builder.tags += tagMap.tags + return builder + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null) return false + if (javaClass != other.javaClass) return false + return toString() == other.toString() + } + + override fun hashCode(): Int = toString().hashCode() + + override fun toString(): String = buildCodeString { + emit(this, inline = true, asParameter = false) + } + + public enum class UseSiteTarget(internal val keyword: String) { + FILE("file"), + PROPERTY("property"), + FIELD("field"), + GET("get"), + SET("set"), + RECEIVER("receiver"), + PARAM("param"), + SETPARAM("setparam"), + DELEGATE("delegate"), + } + + public class Builder internal constructor( + internal val typeName: TypeName, + ) : Taggable.Builder<Builder> { + internal var useSiteTarget: UseSiteTarget? = null + + public val members: MutableList<CodeBlock> = mutableListOf() + override val tags: MutableMap<KClass<*>, Any> = mutableMapOf() + + public fun addMember(format: String, vararg args: Any): Builder = + addMember(CodeBlock.of(format, *args)) + + public fun addMember(codeBlock: CodeBlock): Builder = apply { + members += codeBlock + } + + public fun useSiteTarget(useSiteTarget: UseSiteTarget?): Builder = apply { + this.useSiteTarget = useSiteTarget + } + + public fun build(): AnnotationSpec = AnnotationSpec(this) + + public companion object { + /** + * Creates a [CodeBlock] with parameter `format` depending on the given `value` object. + * Handles a number of special cases, such as appending "f" to `Float` values, and uses + * `%L` for other types. + */ + internal fun memberForValue(value: Any) = when (value) { + is Class<*> -> CodeBlock.of("%T::class", value) + is Enum<*> -> CodeBlock.of("%T.%L", value.javaClass, value.name) + is String -> CodeBlock.of("%S", value) + is Float -> CodeBlock.of("%Lf", value) + is Char -> CodeBlock.of("'%L'", characterLiteralWithoutSingleQuotes(value)) + else -> CodeBlock.of("%L", value) + } + } + } + + /** + * Annotation value visitor adding members to the given builder instance. + */ + @OptIn(DelicateKotlinPoetApi::class) + private class Visitor( + val builder: CodeBlock.Builder, + ) : SimpleAnnotationValueVisitor7<CodeBlock.Builder, String>(builder) { + + override fun defaultAction(o: Any, name: String) = + builder.add(Builder.memberForValue(o)) + + override fun visitAnnotation(a: AnnotationMirror, name: String) = + builder.add("%L", get(a)) + + override fun visitEnumConstant(c: VariableElement, name: String) = + builder.add("%T.%L", c.asType().asTypeName(), c.simpleName) + + override fun visitType(t: TypeMirror, name: String) = + builder.add("%T::class", t.asTypeName()) + + override fun visitArray(values: List<AnnotationValue>, name: String): CodeBlock.Builder { + builder.add("arrayOf(⇥⇥") + values.forEachIndexed { index, value -> + if (index > 0) builder.add(", ") + value.accept(this, name) + } + builder.add("⇤⇤)") + return builder + } + } + + public companion object { + @DelicateKotlinPoetApi( + message = "Java reflection APIs don't give complete information on Kotlin types. Consider " + + "using the kotlinpoet-metadata APIs instead.", + ) + @JvmStatic + @JvmOverloads + public fun get( + annotation: Annotation, + includeDefaultValues: Boolean = false, + ): AnnotationSpec { + try { + @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") + val javaAnnotation = annotation as java.lang.annotation.Annotation + val builder = builder(javaAnnotation.annotationType()) + .tag(annotation) + val methods = annotation.annotationType().declaredMethods.sortedBy { it.name } + for (method in methods) { + val value = method.invoke(annotation) + if (!includeDefaultValues) { + if (Objects.deepEquals(value, method.defaultValue)) { + continue + } + } + val member = CodeBlock.builder() + member.add("%L = ", method.name) + if (value.javaClass.isArray) { + member.add("arrayOf(⇥⇥") + for (i in 0 until Array.getLength(value)) { + if (i > 0) member.add(", ") + member.add(Builder.memberForValue(Array.get(value, i))) + } + member.add("⇤⇤)") + builder.addMember(member.build()) + continue + } + if (value is Annotation) { + member.add("%L", get(value)) + builder.addMember(member.build()) + continue + } + member.add("%L", Builder.memberForValue(value)) + builder.addMember(member.build()) + } + return builder.build() + } catch (e: Exception) { + throw RuntimeException("Reflecting $annotation failed!", e) + } + } + + @DelicateKotlinPoetApi( + message = "Mirror APIs don't give complete information on Kotlin types. Consider using" + + " the kotlinpoet-metadata APIs instead.", + ) + @JvmStatic + public fun get(annotation: AnnotationMirror): AnnotationSpec { + val element = annotation.annotationType.asElement() as TypeElement + val builder = builder(element.asClassName()).tag(annotation) + for (executableElement in annotation.elementValues.keys) { + val member = CodeBlock.builder() + val visitor = Visitor(member) + val name = executableElement.simpleName.toString() + member.add("%L = ", name) + val value = annotation.elementValues[executableElement]!! + value.accept(visitor, name) + builder.addMember(member.build()) + } + return builder.build() + } + + @JvmStatic public fun builder(type: ClassName): Builder = Builder(type) + + @JvmStatic public fun builder(type: ParameterizedTypeName): Builder = Builder(type) + + @DelicateKotlinPoetApi( + message = "Java reflection APIs don't give complete information on Kotlin types. Consider " + + "using the kotlinpoet-metadata APIs instead.", + ) + @JvmStatic + public fun builder(type: Class<out Annotation>): Builder = + builder(type.asClassName()) + + @JvmStatic public fun builder(type: KClass<out Annotation>): Builder = + builder(type.asClassName()) + } +} diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/ClassName.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/ClassName.kt new file mode 100644 index 00000000..119d05af --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/ClassName.kt @@ -0,0 +1,277 @@ +/* + * Copyright (C) 2014 Google, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:JvmName("ClassNames") + +package com.squareup.kotlinpoet + +import javax.lang.model.element.Element +import javax.lang.model.element.ElementKind +import javax.lang.model.element.NestingKind.MEMBER +import javax.lang.model.element.NestingKind.TOP_LEVEL +import javax.lang.model.element.PackageElement +import javax.lang.model.element.TypeElement +import kotlin.reflect.KClass + +/** A fully-qualified class name for top-level and member classes. */ +public class ClassName internal constructor( + names: List<String>, + nullable: Boolean = false, + annotations: List<AnnotationSpec> = emptyList(), + tags: Map<KClass<*>, Any> = emptyMap() +) : TypeName(nullable, annotations, TagMap(tags)), Comparable<ClassName> { + /** + * Returns a class name created from the given parts. For example, calling this with package name + * `"java.util"` and simple names `"Map"`, `"Entry"` yields `Map.Entry`. + */ + @Deprecated("", level = DeprecationLevel.HIDDEN) + public constructor(packageName: String, simpleName: String, vararg simpleNames: String) : + this(listOf(packageName, simpleName, *simpleNames)) + + @Deprecated( + "Simple names must not be empty. Did you forget an argument?", + level = DeprecationLevel.ERROR, + replaceWith = ReplaceWith("ClassName(packageName, TODO())"), + ) + public constructor(packageName: String) : this(packageName, listOf()) + + /** + * Returns a class name created from the given parts. For example, calling this with package name + * `"java.util"` and simple names `"Map"`, `"Entry"` yields `Map.Entry`. + */ + public constructor(packageName: String, vararg simpleNames: String) : + this(listOf(packageName, *simpleNames)) { + require(simpleNames.isNotEmpty()) { "simpleNames must not be empty" } + require(simpleNames.none { it.isEmpty() }) { + "simpleNames must not contain empty items: ${simpleNames.contentToString()}" + } + } + + /** + * Returns a class name created from the given parts. For example, calling this with package name + * `"java.util"` and simple names `"Map"`, `"Entry"` yields `Map.Entry`. + */ + public constructor(packageName: String, simpleNames: List<String>) : + this(mutableListOf(packageName).apply { addAll(simpleNames) }) { + require(simpleNames.isNotEmpty()) { "simpleNames must not be empty" } + require(simpleNames.none { it.isEmpty() }) { + "simpleNames must not contain empty items: $simpleNames" + } + } + + /** From top to bottom. This will be `["java.util", "Map", "Entry"]` for `Map.Entry`. */ + private val names = names.toImmutableList() + + /** Fully qualified name using `.` as a separator, like `kotlin.collections.Map.Entry`. */ + public val canonicalName: String = if (names[0].isEmpty()) + names.subList(1, names.size).joinToString(".") else + names.joinToString(".") + + /** Package name, like `"kotlin.collections"` for `Map.Entry`. */ + public val packageName: String get() = names[0] + + /** Simple name of this class, like `"Entry"` for `Map.Entry`. */ + public val simpleName: String get() = names[names.size - 1] + + /** + * The enclosing classes, outermost first, followed by the simple name. This is `["Map", "Entry"]` + * for `Map.Entry`. + */ + public val simpleNames: List<String> get() = names.subList(1, names.size) + + override fun copy( + nullable: Boolean, + annotations: List<AnnotationSpec>, + tags: Map<KClass<*>, Any> + ): ClassName { + return ClassName(names, nullable, annotations, tags) + } + + /** + * Returns the enclosing class, like `Map` for `Map.Entry`. Returns null if this class is not + * nested in another class. + */ + public fun enclosingClassName(): ClassName? { + return if (names.size != 2) + ClassName(names.subList(0, names.size - 1)) else + null + } + + /** + * Returns the top class in this nesting group. Equivalent to chained calls to + * [ClassName.enclosingClassName] until the result's enclosing class is null. + */ + public fun topLevelClassName(): ClassName = ClassName(names.subList(0, 2)) + + /** + * Fully qualified name using `.` to separate package from the top level class name, and `$` to + * separate nested classes, like `kotlin.collections.Map$Entry`. + */ + public fun reflectionName(): String { + // trivial case: no nested names + if (names.size == 2) { + return if (packageName.isEmpty()) + names[1] else + packageName + "." + names[1] + } + // concat top level class name and nested names + return buildString { + append(topLevelClassName().canonicalName) + for (name in simpleNames.subList(1, simpleNames.size)) { + append('$').append(name) + } + } + } + + /** + * Callable reference to the constructor of this class. Emits the enclosing class if one exists, + * followed by the reference operator `::`, followed by either [simpleName] or the + * fully-qualified name if this is a top-level class. + * + * Note: As `::$packageName.$simpleName` is not valid syntax, an aliased import may be required + * for a top-level class with a conflicting name. + */ + public fun constructorReference(): CodeBlock { + val enclosing = enclosingClassName() + return if (enclosing != null) { + CodeBlock.of("%T::%N", enclosing, simpleName) + } else { + CodeBlock.of("::%T", this) + } + } + + /** Returns a new [ClassName] instance for the specified `name` as nested inside this class. */ + public fun nestedClass(name: String): ClassName = ClassName(names + name) + + /** + * Returns a class that shares the same enclosing package or class. If this class is enclosed by + * another class, this is equivalent to `enclosingClassName().nestedClass(name)`. Otherwise + * it is equivalent to `get(packageName(), name)`. + */ + public fun peerClass(name: String): ClassName { + val result = names.toMutableList() + result[result.size - 1] = name + return ClassName(result) + } + + /** + * Orders by the fully-qualified name. Nested types are ordered immediately after their + * enclosing type. For example, the following types are ordered by this method: + * + * ``` + * com.example.Robot + * com.example.Robot.Motor + * com.example.RoboticVacuum + * ``` + */ + override fun compareTo(other: ClassName): Int = canonicalName.compareTo(other.canonicalName) + + override fun emit(out: CodeWriter) = + out.emit(out.lookupName(this).escapeSegmentsIfNecessary()) + + public companion object { + /** + * Returns a new [ClassName] instance for the given fully-qualified class name string. This + * method assumes that the input is ASCII and follows typical Java style (lowercase package + * names, UpperCamelCase class names) and may produce incorrect results or throw + * [IllegalArgumentException] otherwise. For that reason, the constructor should be preferred as + * it can create [ClassName] instances without such restrictions. + */ + @JvmStatic public fun bestGuess(classNameString: String): ClassName { + val names = mutableListOf<String>() + + // Add the package name, like "java.util.concurrent", or "" for no package. + var p = 0 + while (p < classNameString.length && Character.isLowerCase(classNameString.codePointAt(p))) { + p = classNameString.indexOf('.', p) + 1 + require(p != 0) { "couldn't make a guess for $classNameString" } + } + names += if (p != 0) classNameString.substring(0, p - 1) else "" + + // Add the class names, like "Map" and "Entry". + for (part in classNameString.substring(p).split('.')) { + require(part.isNotEmpty() && Character.isUpperCase(part.codePointAt(0))) { + "couldn't make a guess for $classNameString" + } + + names += part + } + + require(names.size >= 2) { "couldn't make a guess for $classNameString" } + return ClassName(names) + } + } +} + +@DelicateKotlinPoetApi( + message = "Java reflection APIs don't give complete information on Kotlin types. Consider using" + + " the kotlinpoet-metadata APIs instead." +) +@JvmName("get") +public fun Class<*>.asClassName(): ClassName { + require(!isPrimitive) { "primitive types cannot be represented as a ClassName" } + require(Void.TYPE != this) { "'void' type cannot be represented as a ClassName" } + require(!isArray) { "array types cannot be represented as a ClassName" } + val names = mutableListOf<String>() + var c = this + while (true) { + names += c.simpleName + val enclosing = c.enclosingClass ?: break + c = enclosing + } + // Avoid unreliable Class.getPackage(). https://github.com/square/javapoet/issues/295 + val lastDot = c.name.lastIndexOf('.') + if (lastDot != -1) names += c.name.substring(0, lastDot) + names.reverse() + return ClassName(names) +} + +@JvmName("get") +public fun KClass<*>.asClassName(): ClassName { + qualifiedName?.let { return ClassName.bestGuess(it) } + throw IllegalArgumentException("$this cannot be represented as a ClassName") +} + +/** Returns the class name for `element`. */ +@DelicateKotlinPoetApi( + message = "Element APIs don't give complete information on Kotlin types. Consider using" + + " the kotlinpoet-metadata APIs instead." +) +@JvmName("get") +public fun TypeElement.asClassName(): ClassName { + fun isClassOrInterface(e: Element) = e.kind.isClass || e.kind.isInterface + + fun getPackage(type: Element): PackageElement { + var t = type + while (t.kind != ElementKind.PACKAGE) { + t = t.enclosingElement + } + return t as PackageElement + } + + val names = mutableListOf<String>() + var e: Element = this + while (isClassOrInterface(e)) { + val eType = e as TypeElement + require(eType.nestingKind.isOneOf(TOP_LEVEL, MEMBER)) { + "unexpected type testing" + } + names += eType.simpleName.toString() + e = eType.enclosingElement + } + names += getPackage(this).qualifiedName.toString() + names.reverse() + return ClassName(names) +} diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/CodeBlock.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/CodeBlock.kt new file mode 100644 index 00000000..61b4b202 --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/CodeBlock.kt @@ -0,0 +1,522 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:JvmName("CodeBlocks") + +package com.squareup.kotlinpoet + +import java.lang.reflect.Type +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols +import javax.lang.model.element.Element +import javax.lang.model.type.TypeMirror +import kotlin.reflect.KClass + +/** + * A fragment of a .kt file, potentially containing declarations, statements, and documentation. + * Code blocks are not necessarily well-formed Kotlin code, and are not validated. This class + * assumes kotlinc will check correctness later! + * + * Code blocks support placeholders like [java.text.Format]. This class primarily uses a percent + * sign `%` but has its own set of permitted placeholders: + * + * * `%L` emits a *literal* value with no escaping. Arguments for literals may be strings, + * primitives, [type declarations][TypeSpec], [annotations][AnnotationSpec] and even other code + * blocks. + * * `%N` emits a *name*, using name collision avoidance where necessary. Arguments for names may + * be strings (actually any [character sequence][CharSequence]), [parameters][ParameterSpec], + * [properties][PropertySpec], [functions][FunSpec], and [types][TypeSpec]. + * * `%S` escapes the value as a *string*, wraps it with double quotes, and emits that. For + * example, `6" sandwich` is emitted `"6\" sandwich"`. `%S` will also escape all dollar signs + * (`$`), use `%P` for string templates. + * * `%P` - Similar to `%S`, but doesn't escape dollar signs (`$`) to allow creation of string + * templates. If the string contains dollar signs that should be escaped - use `%S`. + * * `%T` emits a *type* reference. Types will be imported if possible. Arguments for types may be + * [classes][Class]. + * * `%M` emits a *member* reference. A member is either a function or a property. If the member is + * importable, e.g. it's a top-level function or a property declared inside an object, the import + * will be resolved if possible. Arguments for members must be of type [MemberName]. + * * `%%` emits a percent sign. + * * `·` emits a space that never wraps. KotlinPoet prefers to wrap lines longer than 100 columns. + * It does this by replacing normal spaces with a newline and indent. Note that spaces in strings + * are never wrapped. + * * `⇥` increases the indentation level. + * * `⇤` decreases the indentation level. + * * `«` begins a statement. For multiline statements, every line after the first line is + * double-indented. + * * `»` ends a statement. + */ +public class CodeBlock private constructor( + internal val formatParts: List<String>, + internal val args: List<Any?>, +) { + /** A heterogeneous list containing string literals and value placeholders. */ + + public fun isEmpty(): Boolean = formatParts.isEmpty() + + public fun isNotEmpty(): Boolean = !isEmpty() + + /** + * Returns a code block with `prefix` stripped off, or null if this code block doesn't start with + * `prefix`. + * + * This is a pretty basic implementation that might not cover cases like mismatched whitespace. We + * could offer something more lenient if necessary. + */ + internal fun withoutPrefix(prefix: CodeBlock): CodeBlock? { + if (formatParts.size < prefix.formatParts.size) return null + if (args.size < prefix.args.size) return null + + var prefixArgCount = 0 + var firstFormatPart: String? = null + + // Walk through the formatParts of prefix to confirm that it's a of this. + prefix.formatParts.forEachIndexed { index, formatPart -> + if (formatParts[index] != formatPart) { + // We've found a format part that doesn't match. If this is the very last format part check + // for a string prefix match. If that doesn't match, we're done. + if (index == prefix.formatParts.size - 1 && formatParts[index].startsWith(formatPart)) { + firstFormatPart = formatParts[index].substring(formatPart.length) + } else { + return null + } + } + + // If the matching format part has an argument, check that too. + if (formatPart.startsWith("%") && !formatPart[1].isMultiCharNoArgPlaceholder) { + if (args[prefixArgCount] != prefix.args[prefixArgCount]) { + return null // Argument doesn't match. + } + prefixArgCount++ + } + } + + // We found a prefix. Prepare the suffix as a result. + val resultFormatParts = ArrayList<String>() + firstFormatPart?.let { + resultFormatParts.add(it) + } + for (i in prefix.formatParts.size until formatParts.size) { + resultFormatParts.add(formatParts[i]) + } + + val resultArgs = ArrayList<Any?>() + for (i in prefix.args.size until args.size) { + resultArgs.add(args[i]) + } + + return CodeBlock(resultFormatParts, resultArgs) + } + + /** + * Returns a copy of the code block without leading and trailing no-arg placeholders + * (`⇥`, `⇤`, `«`, `»`). + */ + internal fun trim(): CodeBlock { + var start = 0 + var end = formatParts.size + while (start < end && formatParts[start] in NO_ARG_PLACEHOLDERS) { + start++ + } + while (start < end && formatParts[end - 1] in NO_ARG_PLACEHOLDERS) { + end-- + } + return when { + start > 0 || end < formatParts.size -> CodeBlock(formatParts.subList(start, end), args) + else -> this + } + } + + /** + * Returns a copy of the code block with selected format parts replaced, similar to + * [java.lang.String.replaceAll]. + * + * **Warning!** This method leaves the arguments list unchanged. Take care when replacing + * placeholders with arguments, such as `%L`, as it can result in a code block, where + * placeholders don't match their arguments. + */ + internal fun replaceAll(oldValue: String, newValue: String) = + CodeBlock(formatParts.map { it.replace(oldValue, newValue) }, args) + + internal fun hasStatements() = formatParts.any { "«" in it } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null) return false + if (javaClass != other.javaClass) return false + return toString() == other.toString() + } + + override fun hashCode(): Int = toString().hashCode() + + override fun toString(): String = buildCodeString { emitCode(this@CodeBlock) } + + internal fun toString(codeWriter: CodeWriter): String = buildCodeString(codeWriter) { + emitCode(this@CodeBlock) + } + + public fun toBuilder(): Builder { + val builder = Builder() + builder.formatParts += formatParts + builder.args.addAll(args) + return builder + } + + public class Builder { + internal val formatParts = mutableListOf<String>() + internal val args = mutableListOf<Any?>() + + public fun isEmpty(): Boolean = formatParts.isEmpty() + + public fun isNotEmpty(): Boolean = !isEmpty() + + /** + * Adds code using named arguments. + * + * Named arguments specify their name after the '%' followed by : and the corresponding type + * character. Argument names consist of characters in `a-z, A-Z, 0-9, and _` and must start + * with a lowercase character. + * + * For example, to refer to the type [java.lang.Integer] with the argument name `clazz` use a + * format string containing `%clazz:T` and include the key `clazz` with value + * `java.lang.Integer.class` in the argument map. + */ + public fun addNamed(format: String, arguments: Map<String, *>): Builder = apply { + var p = 0 + + for (argument in arguments.keys) { + require(LOWERCASE matches argument) { + "argument '$argument' must start with a lowercase character" + } + } + + while (p < format.length) { + val nextP = format.nextPotentialPlaceholderPosition(startIndex = p) + if (nextP == -1) { + formatParts += format.substring(p, format.length) + break + } + + if (p != nextP) { + formatParts += format.substring(p, nextP) + p = nextP + } + + var matchResult: MatchResult? = null + val colon = format.indexOf(':', p) + if (colon != -1) { + val endIndex = (colon + 2).coerceAtMost(format.length) + matchResult = NAMED_ARGUMENT.matchEntire(format.substring(p, endIndex)) + } + if (matchResult != null) { + val argumentName = matchResult.groupValues[ARG_NAME] + require(arguments.containsKey(argumentName)) { + "Missing named argument for %$argumentName" + } + val formatChar = matchResult.groupValues[TYPE_NAME].first() + addArgument(format, formatChar, arguments[argumentName]) + formatParts += "%$formatChar" + p += matchResult.range.last + 1 + } else if (format[p].isSingleCharNoArgPlaceholder) { + formatParts += format.substring(p, p + 1) + p++ + } else { + require(p < format.length - 1) { "dangling % at end" } + require(format[p + 1].isMultiCharNoArgPlaceholder) { + "unknown format %${format[p + 1]} at ${p + 1} in '$format'" + } + formatParts += format.substring(p, p + 2) + p += 2 + } + } + } + + /** + * Add code with positional or relative arguments. + * + * Relative arguments map 1:1 with the placeholders in the format string. + * + * Positional arguments use an index after the placeholder to identify which argument index + * to use. For example, for a literal to reference the 3rd argument: "%3L" (1 based index) + * + * Mixing relative and positional arguments in a call to add is invalid and will result in an + * error. + */ + public fun add(format: String, vararg args: Any?): Builder = apply { + var hasRelative = false + var hasIndexed = false + + var relativeParameterCount = 0 + val indexedParameterCount = IntArray(args.size) + + var p = 0 + while (p < format.length) { + if (format[p].isSingleCharNoArgPlaceholder) { + formatParts += format[p].toString() + p++ + continue + } + + if (format[p] != '%') { + var nextP = format.nextPotentialPlaceholderPosition(startIndex = p + 1) + if (nextP == -1) nextP = format.length + formatParts += format.substring(p, nextP) + p = nextP + continue + } + + p++ // '%'. + + // Consume zero or more digits, leaving 'c' as the first non-digit char after the '%'. + val indexStart = p + var c: Char + do { + require(p < format.length) { "dangling format characters in '$format'" } + c = format[p++] + } while (c in '0'..'9') + val indexEnd = p - 1 + + // If 'c' doesn't take an argument, we're done. + if (c.isMultiCharNoArgPlaceholder) { + require(indexStart == indexEnd) { "%% may not have an index" } + formatParts += "%$c" + continue + } + + // Find either the indexed argument, or the relative argument. (0-based). + val index: Int + if (indexStart < indexEnd) { + index = Integer.parseInt(format.substring(indexStart, indexEnd)) - 1 + hasIndexed = true + if (args.isNotEmpty()) { + indexedParameterCount[index % args.size]++ // modulo is needed, checked below anyway + } + } else { + index = relativeParameterCount + hasRelative = true + relativeParameterCount++ + } + + require(index >= 0 && index < args.size) { + "index ${index + 1} for '${format.substring( + indexStart - 1, + indexEnd + 1, + )}' not in range (received ${args.size} arguments)" + } + require(!hasIndexed || !hasRelative) { "cannot mix indexed and positional parameters" } + + addArgument(format, c, args[index]) + + formatParts += "%$c" + } + + if (hasRelative) { + require(relativeParameterCount >= args.size) { + "unused arguments: expected $relativeParameterCount, received ${args.size}" + } + } + if (hasIndexed) { + val unused = mutableListOf<String>() + for (i in args.indices) { + if (indexedParameterCount[i] == 0) { + unused += "%" + (i + 1) + } + } + val s = if (unused.size == 1) "" else "s" + require(unused.isEmpty()) { "unused argument$s: ${unused.joinToString(", ")}" } + } + } + + private fun addArgument(format: String, c: Char, arg: Any?) { + when (c) { + 'N' -> this.args += argToName(arg).escapeIfNecessary() + 'L' -> this.args += argToLiteral(arg) + 'S' -> this.args += argToString(arg) + 'P' -> this.args += if (arg is CodeBlock) arg else argToString(arg) + 'T' -> this.args += argToType(arg) + 'M' -> this.args += arg + else -> throw IllegalArgumentException( + String.format("invalid format string: '%s'", format), + ) + } + } + + private fun argToName(o: Any?) = when (o) { + is CharSequence -> o.toString() + is ParameterSpec -> o.name + is PropertySpec -> o.name + is FunSpec -> o.name + is TypeSpec -> o.name!! + is MemberName -> o.simpleName + else -> throw IllegalArgumentException("expected name but was $o") + } + + private fun argToLiteral(o: Any?) = if (o is Number) formatNumericValue(o) else o + + private fun argToString(o: Any?) = o?.toString() + + private fun formatNumericValue(o: Number): Any? { + val format = DecimalFormatSymbols().apply { + decimalSeparator = '.' + groupingSeparator = '_' + } + + val precision = if (o is Float || o is Double) o.toString().split(".").last().length else 0 + + val pattern = when (o) { + is Float, is Double -> "###,##0.0" + "#".repeat(precision - 1) + else -> "###,##0" + } + + return DecimalFormat(pattern, format).format(o) + } + + private fun logDeprecationWarning(o: Any) { + println( + "Deprecation warning: converting $o to TypeName. Conversion of TypeMirror and" + + " TypeElement is deprecated in KotlinPoet, use kotlin-metadata APIs instead.", + ) + } + + private fun argToType(o: Any?) = when (o) { + is TypeName -> o + is TypeMirror -> { + logDeprecationWarning(o) + o.asTypeName() + } + is Element -> { + logDeprecationWarning(o) + o.asType().asTypeName() + } + is Type -> o.asTypeName() + is KClass<*> -> o.asTypeName() + else -> throw IllegalArgumentException("expected type but was $o") + } + + /** + * @param controlFlow the control flow construct and its code, such as `if (foo == 5)`. + * Shouldn't contain newline characters. Can contain opening braces, e.g. + * `beginControlFlow("list.forEach { element ->")`. If there's no opening brace at the end + * of the string, it will be added. + */ + public fun beginControlFlow(controlFlow: String, vararg args: Any?): Builder = apply { + add(controlFlow.withOpeningBrace(), *args) + indent() + } + + private fun String.withOpeningBrace(): String { + for (i in length - 1 downTo 0) { + if (this[i] == '{') { + return "$this\n" + } else if (this[i] == '}') { + break + } + } + return "$this·{\n" + } + + /** + * @param controlFlow the control flow construct and its code, such as "else if (foo == 10)". + * Shouldn't contain braces or newline characters. + */ + public fun nextControlFlow(controlFlow: String, vararg args: Any?): Builder = apply { + unindent() + add("}·$controlFlow·{\n", *args) + indent() + } + + public fun endControlFlow(): Builder = apply { + unindent() + add("}\n") + } + + public fun addStatement(format: String, vararg args: Any?): Builder = apply { + add("«") + add(format, *args) + add("\n»") + } + + public fun add(codeBlock: CodeBlock): Builder = apply { + formatParts += codeBlock.formatParts + args.addAll(codeBlock.args) + } + + public fun indent(): Builder = apply { + formatParts += "⇥" + } + + public fun unindent(): Builder = apply { + formatParts += "⇤" + } + + public fun clear(): Builder = apply { + formatParts.clear() + args.clear() + } + + public fun build(): CodeBlock = CodeBlock(formatParts.toImmutableList(), args.toImmutableList()) + } + + public companion object { + private val NAMED_ARGUMENT = Regex("%([\\w_]+):([\\w]).*") + private val LOWERCASE = Regex("[a-z]+[\\w_]*") + private const val ARG_NAME = 1 + private const val TYPE_NAME = 2 + private val NO_ARG_PLACEHOLDERS = setOf("⇥", "⇤", "«", "»") + internal val EMPTY = CodeBlock(emptyList(), emptyList()) + + @JvmStatic public fun of(format: String, vararg args: Any?): CodeBlock = + Builder().add(format, *args).build() + + @JvmStatic public fun builder(): Builder = Builder() + + internal val Char.isMultiCharNoArgPlaceholder get() = this == '%' + internal val Char.isSingleCharNoArgPlaceholder get() = isOneOf('⇥', '⇤', '«', '»') + internal val String.isPlaceholder + get() = (length == 1 && first().isSingleCharNoArgPlaceholder) || + (length == 2 && first().isMultiCharNoArgPlaceholder) + + internal fun String.nextPotentialPlaceholderPosition(startIndex: Int) = + indexOfAny(charArrayOf('%', '«', '»', '⇥', '⇤'), startIndex) + } +} + +@JvmOverloads +public fun Collection<CodeBlock>.joinToCode( + separator: CharSequence = ", ", + prefix: CharSequence = "", + suffix: CharSequence = "", +): CodeBlock { + val blocks = toTypedArray() + val placeholders = Array(blocks.size) { "%L" } + return CodeBlock.of(placeholders.joinToString(separator, prefix, suffix), *blocks) +} + +/** + * Builds new [CodeBlock] by populating newly created [CodeBlock.Builder] using provided + * [builderAction] and then converting it to [CodeBlock]. + */ +public inline fun buildCodeBlock(builderAction: CodeBlock.Builder.() -> Unit): CodeBlock { + return CodeBlock.builder().apply(builderAction).build() +} + +/** + * Calls [CodeBlock.Builder.indent] then executes the provided [builderAction] on the + * [CodeBlock.Builder] and then executes [CodeBlock.Builder.unindent] before returning the + * original [CodeBlock.Builder]. + */ +public inline fun CodeBlock.Builder.withIndent(builderAction: CodeBlock.Builder.() -> Unit): CodeBlock.Builder { + return indent().also(builderAction).unindent() +} diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/CodeWriter.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/CodeWriter.kt new file mode 100644 index 00000000..2884c65b --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/CodeWriter.kt @@ -0,0 +1,778 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import java.io.Closeable +import kotlin.math.min + +/** Sentinel value that indicates that no user-provided package has been set. */ +private val NO_PACKAGE = String() + +internal val NULLABLE_ANY = ANY.copy(nullable = true) + +private fun extractMemberName(part: String): String { + require(Character.isJavaIdentifierStart(part[0])) { "not an identifier: $part" } + for (i in 1..part.length) { + if (!part.substring(0, i).isIdentifier) { + return part.substring(0, i - 1) + } + } + return part +} + +internal inline fun buildCodeString(builderAction: CodeWriter.() -> Unit): String { + val stringBuilder = StringBuilder() + CodeWriter(stringBuilder, columnLimit = Integer.MAX_VALUE).use { + it.builderAction() + } + return stringBuilder.toString() +} + +internal fun buildCodeString( + codeWriter: CodeWriter, + builderAction: CodeWriter.() -> Unit, +): String { + val stringBuilder = StringBuilder() + codeWriter.emitInto(stringBuilder, builderAction) + return stringBuilder.toString() +} + +/** + * Converts a [FileSpec] to a string suitable to both human- and kotlinc-consumption. This honors + * imports, indentation, and deferred variable names. + */ +internal class CodeWriter constructor( + out: Appendable, + private val indent: String = DEFAULT_INDENT, + imports: Map<String, Import> = emptyMap(), + private val importedTypes: Map<String, ClassName> = emptyMap(), + private val importedMembers: Map<String, MemberName> = emptyMap(), + columnLimit: Int = 100, +) : Closeable { + private var out = LineWrapper(out, indent, columnLimit) + private var indentLevel = 0 + + private var kdoc = false + private var comment = false + private var packageName = NO_PACKAGE + private val typeSpecStack = mutableListOf<TypeSpec>() + private val memberImportNames = mutableSetOf<String>() + private val importableTypes = mutableMapOf<String, List<ClassName>>().withDefault { emptyList() } + private val importableMembers = mutableMapOf<String, List<MemberName>>().withDefault { emptyList() } + private val referencedNames = mutableSetOf<String>() + private var trailingNewline = false + + val imports = imports.also { + for ((memberName, _) in imports) { + val lastDotIndex = memberName.lastIndexOf('.') + if (lastDotIndex >= 0) { + memberImportNames.add(memberName.substring(0, lastDotIndex)) + } + } + } + + /** + * When emitting a statement, this is the line of the statement currently being written. The first + * line of a statement is indented normally and subsequent wrapped lines are double-indented. This + * is -1 when the currently-written line isn't part of a statement. + */ + var statementLine = -1 + + fun indent(levels: Int = 1) = apply { + indentLevel += levels + } + + fun unindent(levels: Int = 1) = apply { + require(indentLevel - levels >= 0) { "cannot unindent $levels from $indentLevel" } + indentLevel -= levels + } + + fun pushPackage(packageName: String) = apply { + check(this.packageName === NO_PACKAGE) { "package already set: ${this.packageName}" } + this.packageName = packageName + } + + fun popPackage() = apply { + check(packageName !== NO_PACKAGE) { "package already set: $packageName" } + packageName = NO_PACKAGE + } + + fun pushType(type: TypeSpec) = apply { + this.typeSpecStack.add(type) + } + + fun popType() = apply { + this.typeSpecStack.removeAt(typeSpecStack.size - 1) + } + + fun emitComment(codeBlock: CodeBlock) { + trailingNewline = true // Force the '//' prefix for the comment. + comment = true + try { + emitCode(codeBlock) + emit("\n") + } finally { + comment = false + } + } + + fun emitKdoc(kdocCodeBlock: CodeBlock) { + if (kdocCodeBlock.isEmpty()) return + + emit("/**\n") + kdoc = true + try { + emitCode(kdocCodeBlock, ensureTrailingNewline = true) + } finally { + kdoc = false + } + emit(" */\n") + } + + fun emitAnnotations(annotations: List<AnnotationSpec>, inline: Boolean) { + for (annotationSpec in annotations) { + annotationSpec.emit(this, inline) + emit(if (inline) " " else "\n") + } + } + + /** + * Emits `modifiers` in the standard order. Modifiers in `implicitModifiers` will not + * be emitted except for [KModifier.PUBLIC] + */ + fun emitModifiers( + modifiers: Set<KModifier>, + implicitModifiers: Set<KModifier> = emptySet(), + ) { + if (shouldEmitPublicModifier(modifiers, implicitModifiers)) { + emit(KModifier.PUBLIC.keyword) + emit(" ") + } + val uniqueNonPublicExplicitOnlyModifiers = + modifiers + .filterNot { it == KModifier.PUBLIC } + .filterNot { implicitModifiers.contains(it) } + .toEnumSet() + for (modifier in uniqueNonPublicExplicitOnlyModifiers) { + emit(modifier.keyword) + emit(" ") + } + } + + /** + * Emits the `context` block for [contextReceivers]. + */ + fun emitContextReceivers(contextReceivers: List<TypeName>, suffix: String = "") { + if (contextReceivers.isNotEmpty()) { + val receivers = contextReceivers + .map { CodeBlock.of("%T", it) } + .joinToCode(prefix = "context(", suffix = ")") + emitCode(receivers) + emit(suffix) + } + } + + /** + * Emit type variables with their bounds. If a type variable has more than a single bound - call + * [emitWhereBlock] with same input to produce an additional `where` block. + * + * This should only be used when declaring type variables; everywhere else bounds are omitted. + */ + fun emitTypeVariables(typeVariables: List<TypeVariableName>) { + if (typeVariables.isEmpty()) return + + emit("<") + typeVariables.forEachIndexed { index, typeVariable -> + if (index > 0) emit(", ") + if (typeVariable.variance != null) { + emit("${typeVariable.variance.keyword} ") + } + if (typeVariable.isReified) { + emit("reified ") + } + emitCode("%L", typeVariable.name) + if (typeVariable.bounds.size == 1 && typeVariable.bounds[0] != NULLABLE_ANY) { + emitCode(" : %T", typeVariable.bounds[0]) + } + } + emit(">") + } + + /** + * Emit a `where` block containing type bounds for each type variable that has at least two + * bounds. + */ + fun emitWhereBlock(typeVariables: List<TypeVariableName>) { + if (typeVariables.isEmpty()) return + + var firstBound = true + for (typeVariable in typeVariables) { + if (typeVariable.bounds.size > 1) { + for (bound in typeVariable.bounds) { + if (!firstBound) emitCode(", ") else emitCode(" where ") + emitCode("%L : %T", typeVariable.name, bound) + firstBound = false + } + } + } + } + + fun emitCode(s: String) = emitCode(CodeBlock.of(s)) + + fun emitCode(format: String, vararg args: Any?) = emitCode(CodeBlock.of(format, *args)) + + fun emitCode( + codeBlock: CodeBlock, + isConstantContext: Boolean = false, + ensureTrailingNewline: Boolean = false, + ) = apply { + var a = 0 + var deferredTypeName: ClassName? = null // used by "import static" logic + val partIterator = codeBlock.formatParts.listIterator() + while (partIterator.hasNext()) { + when (val part = partIterator.next()) { + "%L" -> emitLiteral(codeBlock.args[a++], isConstantContext) + + "%N" -> emit(codeBlock.args[a++] as String) + + "%S" -> { + val string = codeBlock.args[a++] as String? + // Emit null as a literal null: no quotes. + val literal = if (string != null) { + stringLiteralWithQuotes( + string, + isInsideRawString = false, + isConstantContext = isConstantContext, + ) + } else { + "null" + } + emit(literal, nonWrapping = true) + } + + "%P" -> { + val string = codeBlock.args[a++]?.let { arg -> + if (arg is CodeBlock) { + arg.toString(this@CodeWriter) + } else { + arg as String? + } + } + // Emit null as a literal null: no quotes. + val literal = if (string != null) { + stringLiteralWithQuotes( + string, + isInsideRawString = true, + isConstantContext = isConstantContext, + ) + } else { + "null" + } + emit(literal, nonWrapping = true) + } + + "%T" -> { + var typeName = codeBlock.args[a++] as TypeName + if (typeName.isAnnotated) { + typeName.emitAnnotations(this) + typeName = typeName.copy(annotations = emptyList()) + } + // defer "typeName.emit(this)" if next format part will be handled by the default case + var defer = false + if (typeName is ClassName && partIterator.hasNext()) { + if (!codeBlock.formatParts[partIterator.nextIndex()].startsWith("%")) { + val candidate = typeName + if (candidate.canonicalName in memberImportNames) { + check(deferredTypeName == null) { "pending type for static import?!" } + deferredTypeName = candidate + defer = true + } + } + } + if (!defer) typeName.emit(this) + typeName.emitNullable(this) + } + + "%M" -> { + val memberName = codeBlock.args[a++] as MemberName + memberName.emit(this) + } + + "%%" -> emit("%") + + "⇥" -> indent() + + "⇤" -> unindent() + + "«" -> { + check(statementLine == -1) { + """ + |Can't open a new statement until the current statement is closed (opening « followed + |by another « without a closing »). + |Current code block: + |- Format parts: ${codeBlock.formatParts.map(::escapeCharacterLiterals)} + |- Arguments: ${codeBlock.args} + | + """.trimMargin() + } + statementLine = 0 + } + + "»" -> { + check(statementLine != -1) { + """ + |Can't close a statement that hasn't been opened (closing » is not preceded by an + |opening «). + |Current code block: + |- Format parts: ${codeBlock.formatParts.map(::escapeCharacterLiterals)} + |- Arguments: ${codeBlock.args} + | + """.trimMargin() + } + if (statementLine > 0) { + unindent(2) // End a multi-line statement. Decrease the indentation level. + } + statementLine = -1 + } + + else -> { + // Handle deferred type. + var doBreak = false + if (deferredTypeName != null) { + if (part.startsWith(".")) { + if (emitStaticImportMember(deferredTypeName.canonicalName, part)) { + // Okay, static import hit and all was emitted, so clean-up and jump to next part. + deferredTypeName = null + doBreak = true + } + } + if (!doBreak) { + deferredTypeName!!.emit(this) + deferredTypeName = null + } + } + if (!doBreak) { + emit(part) + } + } + } + } + if (ensureTrailingNewline && out.hasPendingSegments) { + emit("\n") + } + } + + private fun emitStaticImportMember(canonical: String, part: String): Boolean { + val partWithoutLeadingDot = part.substring(1) + if (partWithoutLeadingDot.isEmpty()) return false + val first = partWithoutLeadingDot[0] + if (!Character.isJavaIdentifierStart(first)) return false + val explicit = imports[canonical + "." + extractMemberName(partWithoutLeadingDot)] + if (explicit != null) { + if (explicit.alias != null) { + val memberName = extractMemberName(partWithoutLeadingDot) + emit(partWithoutLeadingDot.replaceFirst(memberName, explicit.alias)) + } else { + emit(partWithoutLeadingDot) + } + return true + } + return false + } + + private fun emitLiteral(o: Any?, isConstantContext: Boolean) { + when (o) { + is TypeSpec -> o.emit(this, null) + is AnnotationSpec -> o.emit(this, inline = true, asParameter = isConstantContext) + is PropertySpec -> o.emit(this, emptySet()) + is FunSpec -> o.emit( + codeWriter = this, + enclosingName = null, + implicitModifiers = setOf(KModifier.PUBLIC), + includeKdocTags = true, + ) + is TypeAliasSpec -> o.emit(this) + is CodeBlock -> emitCode(o, isConstantContext = isConstantContext) + else -> emit(o.toString()) + } + } + + /** + * Returns the best name to identify `className` with in the current context. This uses the + * available imports and the current scope to find the shortest name available. It does not honor + * names visible due to inheritance. + */ + fun lookupName(className: ClassName): String { + // Find the shortest suffix of className that resolves to className. This uses both local type + // names (so `Entry` in `Map` refers to `Map.Entry`). Also uses imports. + var nameResolved = false + var c: ClassName? = className + while (c != null) { + val alias = imports[c.canonicalName]?.alias + val simpleName = alias ?: c.simpleName + val resolved = resolve(simpleName) + nameResolved = resolved != null + + // We don't care about nullability and type annotations here, as it's irrelevant for imports. + if (resolved == c.copy(nullable = false, annotations = emptyList())) { + if (alias != null) return alias + val suffixOffset = c.simpleNames.size - 1 + referencedNames.add(className.topLevelClassName().simpleName) + return className.simpleNames.subList( + suffixOffset, + className.simpleNames.size, + ).joinToString(".") + } + c = c.enclosingClassName() + } + + // If the name resolved but wasn't a match, we're stuck with the fully qualified name. + if (nameResolved) { + return className.canonicalName + } + + // If the class is in the same package, we're done. + if (packageName == className.packageName) { + referencedNames.add(className.topLevelClassName().simpleName) + return className.simpleNames.joinToString(".") + } + + // We'll have to use the fully-qualified name. Mark the type as importable for a future pass. + if (!kdoc) { + importableType(className) + } + + return className.canonicalName + } + + fun lookupName(memberName: MemberName): String { + val simpleName = imports[memberName.canonicalName]?.alias ?: memberName.simpleName + // Match an imported member. + val importedMember = importedMembers[simpleName] + if (importedMember == memberName) { + return simpleName + } else if (importedMember != null && memberName.enclosingClassName != null) { + val enclosingClassName = lookupName(memberName.enclosingClassName) + return "$enclosingClassName.$simpleName" + } + + // If the member is in the same package, we're done. + if (packageName == memberName.packageName && memberName.enclosingClassName == null) { + referencedNames.add(memberName.simpleName) + return memberName.simpleName + } + + // We'll have to use the fully-qualified name. + // Mark the member as importable for a future pass unless the name clashes with + // a method in the current context + if (!kdoc && ( + memberName.isExtension || + !isMethodNameUsedInCurrentContext(memberName.simpleName) + ) + ) { + importableMember(memberName) + } + + return memberName.canonicalName + } + + // TODO(luqasn): also honor superclass members when resolving names. + private fun isMethodNameUsedInCurrentContext(simpleName: String): Boolean { + for (it in typeSpecStack.reversed()) { + if (it.funSpecs.any { it.name == simpleName }) { + return true + } + if (!it.modifiers.contains(KModifier.INNER)) { + break + } + } + return false + } + + private fun importableType(className: ClassName) { + val topLevelClassName = className.topLevelClassName() + val simpleName = imports[className.canonicalName]?.alias ?: topLevelClassName.simpleName + // Check for name clashes with members. + if (simpleName !in importableMembers) { + importableTypes[simpleName] = importableTypes.getValue(simpleName) + topLevelClassName + } + } + + private fun importableMember(memberName: MemberName) { + if (memberName.packageName.isNotEmpty()) { + val simpleName = imports[memberName.canonicalName]?.alias ?: memberName.simpleName + // Check for name clashes with types. + if (simpleName !in importableTypes) { + importableMembers[simpleName] = importableMembers.getValue(simpleName) + memberName + } + } + } + + /** + * Returns the class or enum value referenced by `simpleName`, using the current nesting context and + * imports. + */ + // TODO(jwilson): also honor superclass members when resolving names. + private fun resolve(simpleName: String): ClassName? { + // Match a child of the current (potentially nested) class. + for (i in typeSpecStack.indices.reversed()) { + val typeSpec = typeSpecStack[i] + if (simpleName in typeSpec.nestedTypesSimpleNames) { + return stackClassName(i, simpleName) + } + } + + if (typeSpecStack.size > 0) { + val typeSpec = typeSpecStack[0] + if (typeSpec.name == simpleName) { + // Match the top-level class. + return ClassName(packageName, simpleName) + } + if (typeSpec.isEnum && typeSpec.enumConstants.keys.contains(simpleName)) { + // Match a top level enum value. + // Enum values are not proper classes but can still be modeled using ClassName. + return ClassName(packageName, typeSpec.name!!).nestedClass(simpleName) + } + } + + // Match an imported type. + val importedType = importedTypes[simpleName] + if (importedType != null) return importedType + + // No match. + return null + } + + /** Returns the class named `simpleName` when nested in the class at `stackDepth`. */ + private fun stackClassName(stackDepth: Int, simpleName: String): ClassName { + var className = ClassName(packageName, typeSpecStack[0].name!!) + for (i in 1..stackDepth) { + className = className.nestedClass(typeSpecStack[i].name!!) + } + return className.nestedClass(simpleName) + } + + /** + * Emits `s` with indentation as required. It's important that all code that writes to + * [CodeWriter.out] does it through here, since we emit indentation lazily in order to avoid + * unnecessary trailing whitespace. + */ + fun emit(s: String, nonWrapping: Boolean = false) = apply { + var first = true + for (line in s.split('\n')) { + // Emit a newline character. Make sure blank lines in KDoc & comments look good. + if (!first) { + if ((kdoc || comment) && trailingNewline) { + emitIndentation() + out.appendNonWrapping(if (kdoc) " *" else "//") + } + out.newline() + trailingNewline = true + if (statementLine != -1) { + if (statementLine == 0) { + indent(2) // Begin multiple-line statement. Increase the indentation level. + } + statementLine++ + } + } + + first = false + if (line.isEmpty()) continue // Don't indent empty lines. + + // Emit indentation and comment prefix if necessary. + if (trailingNewline) { + emitIndentation() + if (kdoc) { + out.appendNonWrapping(" * ") + } else if (comment) { + out.appendNonWrapping("// ") + } + } + + if (nonWrapping) { + out.appendNonWrapping(line) + } else { + out.append( + line, + indentLevel = if (kdoc) indentLevel else indentLevel + 2, + linePrefix = if (kdoc) " * " else "", + ) + } + trailingNewline = false + } + } + + private fun emitIndentation() { + for (j in 0 until indentLevel) { + out.appendNonWrapping(indent) + } + } + + /** + * Returns whether a [KModifier.PUBLIC] should be emitted. + * + * If [modifiers] contains [KModifier.PUBLIC], this method always returns `true`. + * + * Otherwise, this will return `true` when [KModifier.PUBLIC] is one of the [implicitModifiers] + * and there are no other opposing modifiers (like [KModifier.PROTECTED] etc.) supplied by the + * consumer in [modifiers]. + */ + private fun shouldEmitPublicModifier( + modifiers: Set<KModifier>, + implicitModifiers: Set<KModifier>, + ): Boolean { + if (modifiers.contains(KModifier.PUBLIC)) { + return true + } + + if (!implicitModifiers.contains(KModifier.PUBLIC)) { + return false + } + + val hasOtherConsumerSpecifiedVisibility = + modifiers.containsAnyOf(KModifier.PRIVATE, KModifier.INTERNAL, KModifier.PROTECTED) + + return !hasOtherConsumerSpecifiedVisibility + } + + /** + * Returns the types that should have been imported for this code. If there were any simple name + * collisions, import aliases will be generated. + */ + private fun suggestedTypeImports(): Map<String, Set<ClassName>> { + return importableTypes.filterKeys { it !in referencedNames }.mapValues { it.value.toSet() } + } + + /** + * Returns the members that should have been imported for this code. If there were any simple name + * collisions, import aliases will be generated. + */ + private fun suggestedMemberImports(): Map<String, Set<MemberName>> { + return importableMembers.filterKeys { it !in referencedNames }.mapValues { it.value.toSet() } + } + + /** + * Perform emitting actions on the current [CodeWriter] using a custom [Appendable]. The + * [CodeWriter] will continue using the old [Appendable] after this method returns. + */ + inline fun emitInto(out: Appendable, action: CodeWriter.() -> Unit) { + val codeWrapper = this + LineWrapper(out, indent = DEFAULT_INDENT, columnLimit = Int.MAX_VALUE).use { newOut -> + val oldOut = codeWrapper.out + codeWrapper.out = newOut + action() + codeWrapper.out = oldOut + } + } + + override fun close() { + out.close() + } + + companion object { + /** + * Makes a pass to collect imports by executing [emitStep], and returns an instance of + * [CodeWriter] pre-initialized with collected imports. + */ + fun withCollectedImports( + out: Appendable, + indent: String, + memberImports: Map<String, Import>, + emitStep: (importsCollector: CodeWriter) -> Unit, + ): CodeWriter { + // First pass: emit the entire class, just to collect the types we'll need to import. + val importsCollector = CodeWriter( + NullAppendable, + indent, + memberImports, + columnLimit = Integer.MAX_VALUE, + ) + emitStep(importsCollector) + val generatedImports = mutableMapOf<String, Import>() + val suggestedTypeImports = importsCollector.suggestedTypeImports() + .generateImports( + generatedImports, + canonicalName = ClassName::canonicalName, + packageName = ClassName::packageName, + capitalizeAliases = true, + ) + val suggestedMemberImports = importsCollector.suggestedMemberImports() + .generateImports( + generatedImports, + canonicalName = MemberName::canonicalName, + packageName = MemberName::packageName, + capitalizeAliases = false, + ) + importsCollector.close() + + return CodeWriter( + out, + indent, + memberImports + generatedImports.filterKeys { it !in memberImports }, + suggestedTypeImports, + suggestedMemberImports, + ) + } + + private fun <T> Map<String, Set<T>>.generateImports( + generatedImports: MutableMap<String, Import>, + canonicalName: T.() -> String, + packageName: T.() -> String, + capitalizeAliases: Boolean, + ): Map<String, T> { + return flatMap { (simpleName, qualifiedNames) -> + if (qualifiedNames.size == 1) { + listOf(simpleName to qualifiedNames.first()).also { + val canonicalName = qualifiedNames.first().canonicalName() + generatedImports[canonicalName] = Import(canonicalName) + } + } else { + generateImportAliases(simpleName, qualifiedNames, packageName, capitalizeAliases) + .onEach { (alias, qualifiedName) -> + val canonicalName = qualifiedName.canonicalName() + generatedImports[canonicalName] = Import(canonicalName, alias) + } + } + }.toMap() + } + + private fun <T> generateImportAliases( + simpleName: String, + qualifiedNames: Set<T>, + packageName: T.() -> String, + capitalizeAliases: Boolean, + ): List<Pair<String, T>> { + val packageNameSegments = qualifiedNames.associateWith { qualifiedName -> + qualifiedName.packageName().split('.').map { it.replaceFirstChar(Char::uppercaseChar) } + } + val aliasNames = mutableMapOf<String, T>() + var segmentsToUse = 0 + // Iterate until we have unique aliases for all names. + while (aliasNames.size != qualifiedNames.size) { + segmentsToUse += 1 + aliasNames.clear() + for ((qualifiedName, segments) in packageNameSegments) { + val aliasPrefix = segments.takeLast(min(segmentsToUse, segments.size)) + .joinToString(separator = "") + .replaceFirstChar { if (!capitalizeAliases) it.lowercaseChar() else it } + val aliasName = aliasPrefix + simpleName.replaceFirstChar(Char::uppercaseChar) + aliasNames[aliasName] = qualifiedName + } + } + return aliasNames.toList() + } + } +} diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/ContextReceivable.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/ContextReceivable.kt new file mode 100644 index 00000000..929096bc --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/ContextReceivable.kt @@ -0,0 +1,54 @@ +/* + * Copyright (C) 2022 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +/** A KotlinPoet spec type that can have a context receiver. */ +public interface ContextReceivable { + + /** The originating elements of this type. */ + @ExperimentalKotlinPoetApi + public val contextReceiverTypes: List<TypeName> + + /** The builder analogue to [ContextReceivable] types. */ + public interface Builder<out T : Builder<T>> { + + /** Mutable map of the current originating elements this builder contains. */ + @ExperimentalKotlinPoetApi + public val contextReceiverTypes: MutableList<TypeName> + + /** Adds the given [receiverTypes] to this type's list of originating elements. */ + @Suppress("UNCHECKED_CAST") + @ExperimentalKotlinPoetApi + public fun contextReceivers(receiverTypes: Iterable<TypeName>): T = apply { + contextReceiverTypes += receiverTypes + } as T + + /** Adds the given [receiverTypes] to this type's list of originating elements. */ + @ExperimentalKotlinPoetApi + public fun contextReceivers(vararg receiverTypes: TypeName): T = + contextReceivers(receiverTypes.toList()) + } +} + +@ExperimentalKotlinPoetApi +internal fun ContextReceivable.Builder<*>.buildContextReceivers() = + ContextReceivers(contextReceiverTypes.toImmutableList()) + +@JvmInline +@ExperimentalKotlinPoetApi +internal value class ContextReceivers( + override val contextReceiverTypes: List<TypeName>, +) : ContextReceivable diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/DelicateKotlinPoetApi.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/DelicateKotlinPoetApi.kt new file mode 100644 index 00000000..11369334 --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/DelicateKotlinPoetApi.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +/** + * Marks declarations in the KotlinPoet API that are **delicate** — + * they have limited use-case and shall be used with care in general code. + * Any use of a delicate declaration has to be carefully reviewed to make sure it is + * properly used and does not create problems like lossy Java -> Kotlin type parsing. + * Carefully read documentation and [message] of any declaration marked as `DelicateKotlinPoetApi`. + */ +@MustBeDocumented +@Retention(value = AnnotationRetention.BINARY) +@RequiresOptIn( + level = RequiresOptIn.Level.WARNING, + message = "This is a delicate API and its use requires care." + + " Make sure you fully read and understand documentation of the declaration that is marked as a delicate API.", +) +public annotation class DelicateKotlinPoetApi(val message: String) diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/Dynamic.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/Dynamic.kt new file mode 100644 index 00000000..413b7b7a --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/Dynamic.kt @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import kotlin.reflect.KClass + +public object Dynamic : TypeName(false, emptyList(), TagMap(emptyMap())) { + + override fun copy( + nullable: Boolean, + annotations: List<AnnotationSpec>, + tags: Map<KClass<*>, Any>, + ): Nothing = throw UnsupportedOperationException("dynamic doesn't support copying") + + override fun emit(out: CodeWriter) = out.apply { + emit("dynamic") + } +} diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/ExperimentalKotlinPoetApi.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/ExperimentalKotlinPoetApi.kt new file mode 100644 index 00000000..2e58556d --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/ExperimentalKotlinPoetApi.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2022 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import kotlin.annotation.AnnotationTarget.CLASS +import kotlin.annotation.AnnotationTarget.FUNCTION +import kotlin.annotation.AnnotationTarget.PROPERTY +import kotlin.annotation.AnnotationTarget.TYPEALIAS + +/** + * Indicates that a given API is experimental and subject to change. + */ +@RequiresOptIn +@Retention(AnnotationRetention.BINARY) +@Target(CLASS, FUNCTION, PROPERTY, TYPEALIAS) +public annotation class ExperimentalKotlinPoetApi diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/FileSpec.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/FileSpec.kt new file mode 100644 index 00000000..b2f2549d --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/FileSpec.kt @@ -0,0 +1,551 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget.FILE +import java.io.ByteArrayInputStream +import java.io.File +import java.io.IOException +import java.io.InputStream +import java.io.OutputStreamWriter +import java.net.URI +import java.nio.charset.StandardCharsets.UTF_8 +import java.nio.file.Files +import java.nio.file.Path +import javax.annotation.processing.Filer +import javax.tools.JavaFileObject +import javax.tools.JavaFileObject.Kind +import javax.tools.SimpleJavaFileObject +import javax.tools.StandardLocation +import kotlin.reflect.KClass + +/** + * A Kotlin file containing top level objects like classes, objects, functions, properties, and type + * aliases. + * + * Items are output in the following order: + * - Comment + * - Annotations + * - Package + * - Imports + * - Members + */ +public class FileSpec private constructor( + builder: Builder, + private val tagMap: TagMap = builder.buildTagMap(), +) : Taggable by tagMap { + public val annotations: List<AnnotationSpec> = builder.annotations.toImmutableList() + public val comment: CodeBlock = builder.comment.build() + public val packageName: String = builder.packageName + public val name: String = builder.name + public val members: List<Any> = builder.members.toList() + public val defaultImports: Set<String> = builder.defaultImports.toSet() + public val body: CodeBlock = builder.body.build() + public val isScript: Boolean = builder.isScript + private val memberImports = builder.memberImports.associateBy(Import::qualifiedName) + private val indent = builder.indent + private val extension = if (isScript) "kts" else "kt" + + @Throws(IOException::class) + public fun writeTo(out: Appendable) { + val codeWriter = CodeWriter.withCollectedImports( + out = out, + indent = indent, + memberImports = memberImports, + emitStep = { importsCollector -> emit(importsCollector, collectingImports = true) }, + ) + emit(codeWriter, collectingImports = false) + codeWriter.close() + } + + /** Writes this to `directory` as UTF-8 using the standard directory structure. */ + @Throws(IOException::class) + public fun writeTo(directory: Path) { + require(Files.notExists(directory) || Files.isDirectory(directory)) { + "path $directory exists but is not a directory." + } + var outputDirectory = directory + if (packageName.isNotEmpty()) { + for (packageComponent in packageName.split('.').dropLastWhile { it.isEmpty() }) { + outputDirectory = outputDirectory.resolve(packageComponent) + } + } + + Files.createDirectories(outputDirectory) + + val outputPath = outputDirectory.resolve("$name.$extension") + OutputStreamWriter(Files.newOutputStream(outputPath), UTF_8).use { writer -> writeTo(writer) } + } + + /** Writes this to `directory` as UTF-8 using the standard directory structure. */ + @Throws(IOException::class) + public fun writeTo(directory: File): Unit = writeTo(directory.toPath()) + + /** Writes this to `filer`. */ + @Throws(IOException::class) + public fun writeTo(filer: Filer) { + val originatingElements = members.asSequence() + .filterIsInstance<OriginatingElementsHolder>() + .flatMap { it.originatingElements.asSequence() } + .toSet() + val filerSourceFile = filer.createResource( + StandardLocation.SOURCE_OUTPUT, + packageName, + "$name.$extension", + *originatingElements.toTypedArray(), + ) + try { + filerSourceFile.openWriter().use { writer -> writeTo(writer) } + } catch (e: Exception) { + try { + filerSourceFile.delete() + } catch (ignored: Exception) { + } + throw e + } + } + + private fun emit(codeWriter: CodeWriter, collectingImports: Boolean) { + if (comment.isNotEmpty()) { + codeWriter.emitComment(comment) + } + + if (annotations.isNotEmpty()) { + codeWriter.emitAnnotations(annotations, inline = false) + codeWriter.emit("\n") + } + + codeWriter.pushPackage(packageName) + + val escapedPackageName = packageName.escapeSegmentsIfNecessary() + + if (escapedPackageName.isNotEmpty()) { + codeWriter.emitCode("package·%L\n", escapedPackageName) + codeWriter.emit("\n") + } + + // If we don't have default imports or are collecting them, we don't need to filter + var isDefaultImport: (String) -> Boolean = { false } + if (!collectingImports && defaultImports.isNotEmpty()) { + val defaultImports = defaultImports.map(String::escapeSegmentsIfNecessary) + isDefaultImport = { importName -> + importName.substringBeforeLast(".") in defaultImports + } + } + // Aliased imports should always appear at the bottom of the imports list. + val (aliasedImports, nonAliasedImports) = codeWriter.imports.values + .partition { it.alias != null } + val imports = nonAliasedImports.asSequence().map { it.toString() } + .filterNot(isDefaultImport) + .toSortedSet() + .plus(aliasedImports.map { it.toString() }.toSortedSet()) + + if (imports.isNotEmpty()) { + for (import in imports) { + codeWriter.emitCode("import·%L", import) + codeWriter.emit("\n") + } + codeWriter.emit("\n") + } + + if (isScript) { + codeWriter.emitCode(body) + } else { + members.forEachIndexed { index, member -> + if (index > 0) codeWriter.emit("\n") + when (member) { + is TypeSpec -> member.emit(codeWriter, null) + is FunSpec -> member.emit(codeWriter, null, setOf(KModifier.PUBLIC), true) + is PropertySpec -> member.emit(codeWriter, setOf(KModifier.PUBLIC)) + is TypeAliasSpec -> member.emit(codeWriter) + else -> throw AssertionError() + } + } + } + + codeWriter.popPackage() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null) return false + if (javaClass != other.javaClass) return false + return toString() == other.toString() + } + + override fun hashCode(): Int = toString().hashCode() + + override fun toString(): String = buildString { writeTo(this) } + + public fun toJavaFileObject(): JavaFileObject { + val uri = URI.create( + if (packageName.isEmpty()) { + name + } else { + packageName.replace('.', '/') + '/' + name + } + ".$extension", + ) + return object : SimpleJavaFileObject(uri, Kind.SOURCE) { + private val lastModified = System.currentTimeMillis() + override fun getCharContent(ignoreEncodingErrors: Boolean): String { + return this@FileSpec.toString() + } + + override fun openInputStream(): InputStream { + return ByteArrayInputStream(getCharContent(true).toByteArray(UTF_8)) + } + + override fun getLastModified() = lastModified + } + } + + @JvmOverloads + public fun toBuilder(packageName: String = this.packageName, name: String = this.name): Builder { + val builder = Builder(packageName, name, isScript) + builder.annotations.addAll(annotations) + builder.comment.add(comment) + builder.members.addAll(this.members) + builder.indent = indent + builder.memberImports.addAll(memberImports.values) + builder.defaultImports.addAll(defaultImports) + builder.tags += tagMap.tags + builder.body.add(body) + return builder + } + + public class Builder internal constructor( + public val packageName: String, + public val name: String, + public val isScript: Boolean, + ) : Taggable.Builder<Builder> { + internal val comment = CodeBlock.builder() + internal val memberImports = sortedSetOf<Import>() + internal var indent = DEFAULT_INDENT + override val tags: MutableMap<KClass<*>, Any> = mutableMapOf() + + public val defaultImports: MutableSet<String> = mutableSetOf() + public val imports: List<Import> get() = memberImports.toList() + public val members: MutableList<Any> = mutableListOf() + public val annotations: MutableList<AnnotationSpec> = mutableListOf() + internal val body = CodeBlock.builder() + + /** + * Add an annotation to the file. + * + * The annotation must either have a [`file` use-site target][AnnotationSpec.UseSiteTarget.FILE] + * or not have a use-site target specified (in which case it will be changed to `file`). + */ + public fun addAnnotation(annotationSpec: AnnotationSpec): Builder = apply { + val spec = when (annotationSpec.useSiteTarget) { + FILE -> annotationSpec + null -> annotationSpec.toBuilder().useSiteTarget(FILE).build() + else -> error( + "Use-site target ${annotationSpec.useSiteTarget} not supported for file annotations.", + ) + } + annotations += spec + } + + public fun addAnnotation(annotation: ClassName): Builder = + addAnnotation(AnnotationSpec.builder(annotation).build()) + + public fun addAnnotation(annotation: Class<*>): Builder = + addAnnotation(annotation.asClassName()) + + public fun addAnnotation(annotation: KClass<*>): Builder = + addAnnotation(annotation.asClassName()) + + /** Adds a file-site comment. This is prefixed to the start of the file and different from [addBodyComment]. */ + public fun addFileComment(format: String, vararg args: Any): Builder = apply { + comment.add(format.replace(' ', '·'), *args) + } + + @Deprecated( + "Use addFileComment() instead.", + ReplaceWith("addFileComment(format, args)"), + DeprecationLevel.ERROR, + ) + public fun addComment(format: String, vararg args: Any): Builder = addFileComment(format, *args) + + public fun clearComment(): Builder = apply { + comment.clear() + } + + public fun addType(typeSpec: TypeSpec): Builder = apply { + if (isScript) { + body.add("%L", typeSpec) + } else { + members += typeSpec + } + } + + public fun addFunction(funSpec: FunSpec): Builder = apply { + require(!funSpec.isConstructor && !funSpec.isAccessor) { + "cannot add ${funSpec.name} to file $name" + } + if (isScript) { + body.add("%L", funSpec) + } else { + members += funSpec + } + } + + public fun addProperty(propertySpec: PropertySpec): Builder = apply { + if (isScript) { + body.add("%L", propertySpec) + } else { + members += propertySpec + } + } + + public fun addTypeAlias(typeAliasSpec: TypeAliasSpec): Builder = apply { + if (isScript) { + body.add("%L", typeAliasSpec) + } else { + members += typeAliasSpec + } + } + + public fun addImport(constant: Enum<*>): Builder = addImport( + (constant as java.lang.Enum<*>).declaringClass.asClassName(), + constant.name, + ) + + public fun addImport(`class`: Class<*>, vararg names: String): Builder = apply { + require(names.isNotEmpty()) { "names array is empty" } + addImport(`class`.asClassName(), names.toList()) + } + + public fun addImport(`class`: KClass<*>, vararg names: String): Builder = apply { + require(names.isNotEmpty()) { "names array is empty" } + addImport(`class`.asClassName(), names.toList()) + } + + public fun addImport(className: ClassName, vararg names: String): Builder = apply { + require(names.isNotEmpty()) { "names array is empty" } + addImport(className, names.toList()) + } + + public fun addImport(`class`: Class<*>, names: Iterable<String>): Builder = + addImport(`class`.asClassName(), names) + + public fun addImport(`class`: KClass<*>, names: Iterable<String>): Builder = + addImport(`class`.asClassName(), names) + + public fun addImport(className: ClassName, names: Iterable<String>): Builder = apply { + require("*" !in names) { "Wildcard imports are not allowed" } + for (name in names) { + memberImports += Import(className.canonicalName + "." + name) + } + } + + public fun addImport(packageName: String, vararg names: String): Builder = apply { + require(names.isNotEmpty()) { "names array is empty" } + addImport(packageName, names.toList()) + } + + public fun addImport(packageName: String, names: Iterable<String>): Builder = apply { + require("*" !in names) { "Wildcard imports are not allowed" } + for (name in names) { + memberImports += if (packageName.isNotEmpty()) { + Import("$packageName.$name") + } else { + Import(name) + } + } + } + + public fun addImport(import: Import): Builder = apply { + memberImports += import + } + + public fun clearImports(): Builder = apply { + memberImports.clear() + } + + public fun addAliasedImport(`class`: Class<*>, `as`: String): Builder = + addAliasedImport(`class`.asClassName(), `as`) + + public fun addAliasedImport(`class`: KClass<*>, `as`: String): Builder = + addAliasedImport(`class`.asClassName(), `as`) + + public fun addAliasedImport(className: ClassName, `as`: String): Builder = apply { + memberImports += Import(className.canonicalName, `as`) + } + + public fun addAliasedImport( + className: ClassName, + memberName: String, + `as`: String, + ): Builder = apply { + memberImports += Import("${className.canonicalName}.$memberName", `as`) + } + + public fun addAliasedImport(memberName: MemberName, `as`: String): Builder = apply { + memberImports += Import(memberName.canonicalName, `as`) + } + + /** + * Adds a default import for the given [packageName]. + * + * The format of this should be the qualified name of the package, e.g. `kotlin`, `java.lang`, + * `org.gradle.api`, etc. + */ + public fun addDefaultPackageImport(packageName: String): Builder = apply { + defaultImports += packageName + } + + /** + * Adds Kotlin's standard default package imports as described + * [here](https://kotlinlang.org/docs/packages.html#default-imports). + */ + public fun addKotlinDefaultImports( + includeJvm: Boolean = true, + includeJs: Boolean = true, + ): Builder = apply { + defaultImports += KOTLIN_DEFAULT_IMPORTS + if (includeJvm) { + defaultImports += KOTLIN_DEFAULT_JVM_IMPORTS + } + if (includeJs) { + defaultImports += KOTLIN_DEFAULT_JS_IMPORTS + } + } + + public fun indent(indent: String): Builder = apply { + this.indent = indent + } + + public fun addCode(format: String, vararg args: Any?): Builder = apply { + check(isScript) { + "addCode() is only allowed in script files" + } + body.add(format, *args) + } + + public fun addNamedCode(format: String, args: Map<String, *>): Builder = apply { + check(isScript) { + "addNamedCode() is only allowed in script files" + } + body.addNamed(format, args) + } + + public fun addCode(codeBlock: CodeBlock): Builder = apply { + check(isScript) { + "addCode() is only allowed in script files" + } + body.add(codeBlock) + } + + /** Adds a comment to the body of this script file in the order that it was added. */ + public fun addBodyComment(format: String, vararg args: Any): Builder = apply { + check(isScript) { + "addBodyComment() is only allowed in script files" + } + body.add("//·${format.replace(' ', '·')}\n", *args) + } + + /** + * @param controlFlow the control flow construct and its code, such as "if (foo == 5)". + * Shouldn't contain braces or newline characters. + */ + public fun beginControlFlow(controlFlow: String, vararg args: Any): Builder = apply { + check(isScript) { + "beginControlFlow() is only allowed in script files" + } + body.beginControlFlow(controlFlow, *args) + } + + /** + * @param controlFlow the control flow construct and its code, such as "else if (foo == 10)". + * Shouldn't contain braces or newline characters. + */ + public fun nextControlFlow(controlFlow: String, vararg args: Any): Builder = apply { + check(isScript) { + "nextControlFlow() is only allowed in script files" + } + body.nextControlFlow(controlFlow, *args) + } + + public fun endControlFlow(): Builder = apply { + check(isScript) { + "endControlFlow() is only allowed in script files" + } + body.endControlFlow() + } + + public fun addStatement(format: String, vararg args: Any): Builder = apply { + check(isScript) { + "addStatement() is only allowed in script files" + } + body.addStatement(format, *args) + } + + public fun clearBody(): Builder = apply { + check(isScript) { + "clearBody() is only allowed in script files" + } + body.clear() + } + + public fun build(): FileSpec { + for (annotationSpec in annotations) { + if (annotationSpec.useSiteTarget != FILE) { + error( + "Use-site target ${annotationSpec.useSiteTarget} not supported for file annotations.", + ) + } + } + return FileSpec(this) + } + } + + public companion object { + @JvmStatic public fun get(packageName: String, typeSpec: TypeSpec): FileSpec { + val fileName = typeSpec.name + ?: throw IllegalArgumentException("file name required but type has no name") + return builder(packageName, fileName).addType(typeSpec).build() + } + + @JvmStatic public fun builder(className: ClassName): Builder { + require(className.simpleNames.size == 1) { + "nested types can't be used to name a file: ${className.simpleNames.joinToString(".")}" + } + return builder(className.packageName, className.simpleName) + } + + @JvmStatic public fun builder(packageName: String, fileName: String): Builder = + Builder(packageName, fileName, isScript = false) + + @JvmStatic public fun scriptBuilder(fileName: String, packageName: String = ""): Builder = + Builder(packageName, fileName, isScript = true) + } +} + +internal const val DEFAULT_INDENT = " " + +private val KOTLIN_DEFAULT_IMPORTS = setOf( + "kotlin", + "kotlin.annotation", + "kotlin.collections", + "kotlin.comparisons", + "kotlin.io", + "kotlin.ranges", + "kotlin.sequences", + "kotlin.text", +) +private val KOTLIN_DEFAULT_JVM_IMPORTS = setOf("java.lang") +private val KOTLIN_DEFAULT_JS_IMPORTS = setOf("kotlin.js") diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/FunSpec.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/FunSpec.kt new file mode 100644 index 00000000..2aa75ffc --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/FunSpec.kt @@ -0,0 +1,669 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.squareup.kotlinpoet.KModifier.ABSTRACT +import com.squareup.kotlinpoet.KModifier.EXPECT +import com.squareup.kotlinpoet.KModifier.EXTERNAL +import com.squareup.kotlinpoet.KModifier.INLINE +import com.squareup.kotlinpoet.KModifier.VARARG +import java.lang.reflect.Type +import javax.lang.model.element.Element +import javax.lang.model.element.ExecutableElement +import javax.lang.model.element.Modifier +import javax.lang.model.type.DeclaredType +import javax.lang.model.type.ExecutableType +import javax.lang.model.type.TypeVariable +import javax.lang.model.util.Types +import kotlin.DeprecationLevel.WARNING +import kotlin.reflect.KClass + +/** A generated function declaration. */ +@OptIn(ExperimentalKotlinPoetApi::class) +public class FunSpec private constructor( + builder: Builder, + private val tagMap: TagMap = builder.buildTagMap(), + private val delegateOriginatingElementsHolder: OriginatingElementsHolder = builder.buildOriginatingElements(), + private val contextReceivers: ContextReceivers = builder.buildContextReceivers(), +) : Taggable by tagMap, OriginatingElementsHolder by delegateOriginatingElementsHolder, ContextReceivable by contextReceivers { + public val name: String = builder.name + public val kdoc: CodeBlock = builder.kdoc.build() + public val returnKdoc: CodeBlock = builder.returnKdoc + public val receiverKdoc: CodeBlock = builder.receiverKdoc + public val annotations: List<AnnotationSpec> = builder.annotations.toImmutableList() + public val modifiers: Set<KModifier> = builder.modifiers.toImmutableSet() + public val typeVariables: List<TypeVariableName> = builder.typeVariables.toImmutableList() + public val receiverType: TypeName? = builder.receiverType + + public val returnType: TypeName? = builder.returnType + public val parameters: List<ParameterSpec> = builder.parameters.toImmutableList() + public val delegateConstructor: String? = builder.delegateConstructor + public val delegateConstructorArguments: List<CodeBlock> = + builder.delegateConstructorArguments.toImmutableList() + public val body: CodeBlock = builder.body.build() + private val isExternalGetter = name == GETTER && builder.modifiers.contains(EXTERNAL) + private val isEmptySetter = name == SETTER && parameters.isEmpty() + + init { + require(body.isEmpty() || !builder.modifiers.containsAnyOf(ABSTRACT, EXPECT)) { + "abstract or expect function ${builder.name} cannot have code" + } + if (name == GETTER) { + require(!isExternalGetter || body.isEmpty()) { + "external getter cannot have code" + } + } else if (name == SETTER) { + require(parameters.size <= 1) { + "$name can have at most one parameter" + } + require(parameters.isNotEmpty() || body.isEmpty()) { + "parameterless setter cannot have code" + } + } + require(INLINE in modifiers || typeVariables.none { it.isReified }) { + "only type parameters of inline functions can be reified!" + } + } + + internal fun parameter(name: String) = parameters.firstOrNull { it.name == name } + + internal fun emit( + codeWriter: CodeWriter, + enclosingName: String?, + implicitModifiers: Set<KModifier>, + includeKdocTags: Boolean = false, + ) { + if (includeKdocTags) { + codeWriter.emitKdoc(kdocWithTags()) + } else { + codeWriter.emitKdoc(kdoc.ensureEndsWithNewLine()) + } + codeWriter.emitContextReceivers(contextReceiverTypes, suffix = "\n") + codeWriter.emitAnnotations(annotations, false) + codeWriter.emitModifiers(modifiers, implicitModifiers) + + if (!isConstructor && !name.isAccessor) { + codeWriter.emitCode("fun·") + } + + if (typeVariables.isNotEmpty()) { + codeWriter.emitTypeVariables(typeVariables) + codeWriter.emit(" ") + } + emitSignature(codeWriter, enclosingName) + codeWriter.emitWhereBlock(typeVariables) + + if (shouldOmitBody(implicitModifiers)) { + codeWriter.emit("\n") + return + } + + val asExpressionBody = body.asExpressionBody() + + if (asExpressionBody != null) { + codeWriter.emitCode(CodeBlock.of(" = %L", asExpressionBody), ensureTrailingNewline = true) + } else if (!isEmptySetter) { + codeWriter.emitCode("·{\n") + codeWriter.indent() + codeWriter.emitCode(body.returnsWithoutLinebreak(), ensureTrailingNewline = true) + codeWriter.unindent() + codeWriter.emit("}\n") + } else { + codeWriter.emit("\n") + } + } + + private fun shouldOmitBody(implicitModifiers: Set<KModifier>): Boolean { + if (canNotHaveBody(implicitModifiers)) { + check(body.isEmpty()) { "function $name cannot have code" } + return true + } + return canBodyBeOmitted(implicitModifiers) && body.isEmpty() + } + + private fun canNotHaveBody(implicitModifiers: Set<KModifier>) = + ABSTRACT in modifiers || EXPECT in modifiers + implicitModifiers + + private fun canBodyBeOmitted(implicitModifiers: Set<KModifier>) = isConstructor || + EXTERNAL in (modifiers + implicitModifiers) || + ABSTRACT in modifiers + + private fun emitSignature(codeWriter: CodeWriter, enclosingName: String?) { + if (isConstructor) { + codeWriter.emitCode("constructor", enclosingName) + } else if (name == GETTER) { + codeWriter.emitCode("get") + } else if (name == SETTER) { + codeWriter.emitCode("set") + } else { + if (receiverType != null) { + if (receiverType is LambdaTypeName) { + codeWriter.emitCode("(%T).", receiverType) + } else { + codeWriter.emitCode("%T.", receiverType) + } + } + codeWriter.emitCode("%N", this) + } + + if (!isEmptySetter && !isExternalGetter) { + parameters.emit(codeWriter) { param -> + param.emit(codeWriter, includeType = name != SETTER) + } + } + + if (returnType != null) { + codeWriter.emitCode(": %T", returnType) + } else if (emitUnitReturnType()) { + codeWriter.emitCode(": %T", UNIT) + } + + if (delegateConstructor != null) { + codeWriter.emitCode( + delegateConstructorArguments + .joinToCode(prefix = " : $delegateConstructor(", suffix = ")"), + ) + } + } + + public val isConstructor: Boolean get() = name.isConstructor + + public val isAccessor: Boolean get() = name.isAccessor + + private fun kdocWithTags(): CodeBlock { + return with(kdoc.ensureEndsWithNewLine().toBuilder()) { + var newLineAdded = false + val isNotEmpty = isNotEmpty() + if (receiverKdoc.isNotEmpty()) { + if (isNotEmpty) { + add("\n") + newLineAdded = true + } + add("@receiver %L", receiverKdoc.ensureEndsWithNewLine()) + } + parameters.forEachIndexed { index, parameterSpec -> + if (parameterSpec.kdoc.isNotEmpty()) { + if (!newLineAdded && index == 0 && isNotEmpty) { + add("\n") + newLineAdded = true + } + add("@param %L %L", parameterSpec.name, parameterSpec.kdoc.ensureEndsWithNewLine()) + } + } + if (returnKdoc.isNotEmpty()) { + if (!newLineAdded && isNotEmpty) { + add("\n") + newLineAdded = true + } + add("@return %L", returnKdoc.ensureEndsWithNewLine()) + } + build() + } + } + + /** + * Returns whether [Unit] should be emitted as the return type. + * + * [Unit] is emitted as return type on a function unless: + * - It's a constructor + * - It's a getter/setter on a property + * - It's an expression body + */ + private fun emitUnitReturnType(): Boolean { + if (isConstructor) { + return false + } + if (name == GETTER || name == SETTER) { + // Getter/setters don't emit return types + return false + } + + return body.asExpressionBody() == null + } + + private fun CodeBlock.asExpressionBody(): CodeBlock? { + val codeBlock = this.trim() + val asReturnExpressionBody = codeBlock.withoutPrefix(RETURN_EXPRESSION_BODY_PREFIX_SPACE) + ?: codeBlock.withoutPrefix(RETURN_EXPRESSION_BODY_PREFIX_NBSP) + if (asReturnExpressionBody != null) { + return asReturnExpressionBody + } + if (codeBlock.withoutPrefix(THROW_EXPRESSION_BODY_PREFIX_SPACE) != null || + codeBlock.withoutPrefix(THROW_EXPRESSION_BODY_PREFIX_NBSP) != null + ) { + return codeBlock + } + return null + } + + private fun CodeBlock.returnsWithoutLinebreak(): CodeBlock { + val returnWithSpace = RETURN_EXPRESSION_BODY_PREFIX_SPACE.formatParts[0] + val returnWithNbsp = RETURN_EXPRESSION_BODY_PREFIX_NBSP.formatParts[0] + var originCodeBlockBuilder: CodeBlock.Builder? = null + for ((i, formatPart) in formatParts.withIndex()) { + if (formatPart.startsWith(returnWithSpace)) { + val builder = originCodeBlockBuilder ?: toBuilder() + originCodeBlockBuilder = builder + builder.formatParts[i] = formatPart.replaceFirst(returnWithSpace, returnWithNbsp) + } + } + return originCodeBlockBuilder?.build() ?: this + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null) return false + if (javaClass != other.javaClass) return false + return toString() == other.toString() + } + + override fun hashCode(): Int = toString().hashCode() + + override fun toString(): String = buildCodeString { + emit( + codeWriter = this, + enclosingName = "Constructor", + implicitModifiers = TypeSpec.Kind.CLASS.implicitFunctionModifiers(), + includeKdocTags = true, + ) + } + + @JvmOverloads + public fun toBuilder(name: String = this.name): Builder { + val builder = Builder(name) + builder.kdoc.add(kdoc) + builder.returnKdoc = returnKdoc + builder.receiverKdoc = receiverKdoc + builder.annotations += annotations + builder.modifiers += modifiers + builder.typeVariables += typeVariables + builder.returnType = returnType + builder.parameters += parameters + builder.delegateConstructor = delegateConstructor + builder.delegateConstructorArguments += delegateConstructorArguments + builder.body.add(body) + builder.receiverType = receiverType + builder.tags += tagMap.tags + builder.originatingElements += originatingElements + builder.contextReceiverTypes += contextReceiverTypes + return builder + } + + public class Builder internal constructor( + internal val name: String, + ) : Taggable.Builder<Builder>, OriginatingElementsHolder.Builder<Builder>, ContextReceivable.Builder<Builder> { + internal val kdoc = CodeBlock.builder() + internal var returnKdoc = CodeBlock.EMPTY + internal var receiverKdoc = CodeBlock.EMPTY + internal var receiverType: TypeName? = null + internal var returnType: TypeName? = null + internal var delegateConstructor: String? = null + internal var delegateConstructorArguments = listOf<CodeBlock>() + internal val body = CodeBlock.builder() + + public val annotations: MutableList<AnnotationSpec> = mutableListOf() + public val modifiers: MutableList<KModifier> = mutableListOf() + public val typeVariables: MutableList<TypeVariableName> = mutableListOf() + public val parameters: MutableList<ParameterSpec> = mutableListOf() + override val tags: MutableMap<KClass<*>, Any> = mutableMapOf() + override val originatingElements: MutableList<Element> = mutableListOf() + override val contextReceiverTypes: MutableList<TypeName> = mutableListOf() + + public fun addKdoc(format: String, vararg args: Any): Builder = apply { + kdoc.add(format, *args) + } + + public fun addKdoc(block: CodeBlock): Builder = apply { + kdoc.add(block) + } + + public fun addAnnotations(annotationSpecs: Iterable<AnnotationSpec>): Builder = apply { + this.annotations += annotationSpecs + } + + public fun addAnnotation(annotationSpec: AnnotationSpec): Builder = apply { + annotations += annotationSpec + } + + public fun addAnnotation(annotation: ClassName): Builder = apply { + annotations += AnnotationSpec.builder(annotation).build() + } + + public fun addAnnotation(annotation: Class<*>): Builder = + addAnnotation(annotation.asClassName()) + + public fun addAnnotation(annotation: KClass<*>): Builder = + addAnnotation(annotation.asClassName()) + + public fun addModifiers(vararg modifiers: KModifier): Builder = apply { + this.modifiers += modifiers + } + + public fun addModifiers(modifiers: Iterable<KModifier>): Builder = apply { + this.modifiers += modifiers + } + + public fun jvmModifiers(modifiers: Iterable<Modifier>) { + var visibility = KModifier.INTERNAL + for (modifier in modifiers) { + when (modifier) { + Modifier.PUBLIC -> visibility = KModifier.PUBLIC + Modifier.PROTECTED -> visibility = KModifier.PROTECTED + Modifier.PRIVATE -> visibility = KModifier.PRIVATE + Modifier.ABSTRACT -> this.modifiers += KModifier.ABSTRACT + Modifier.FINAL -> this.modifiers += KModifier.FINAL + Modifier.NATIVE -> this.modifiers += KModifier.EXTERNAL + Modifier.DEFAULT -> Unit + Modifier.STATIC -> addAnnotation(JvmStatic::class) + Modifier.SYNCHRONIZED -> addAnnotation(Synchronized::class) + Modifier.STRICTFP -> addAnnotation(Strictfp::class) + else -> throw IllegalArgumentException("unexpected fun modifier $modifier") + } + } + this.modifiers += visibility + } + + public fun addTypeVariables(typeVariables: Iterable<TypeVariableName>): Builder = apply { + this.typeVariables += typeVariables + } + + public fun addTypeVariable(typeVariable: TypeVariableName): Builder = apply { + typeVariables += typeVariable + } + + @ExperimentalKotlinPoetApi + override fun contextReceivers(receiverTypes: Iterable<TypeName>): Builder = apply { + check(!name.isConstructor) { "constructors cannot have context receivers" } + check(!name.isAccessor) { "$name cannot have context receivers" } + contextReceiverTypes += receiverTypes + } + + @JvmOverloads public fun receiver( + receiverType: TypeName, + kdoc: CodeBlock = CodeBlock.EMPTY, + ): Builder = apply { + check(!name.isConstructor) { "$name cannot have receiver type" } + this.receiverType = receiverType + this.receiverKdoc = kdoc + } + + @JvmOverloads public fun receiver( + receiverType: Type, + kdoc: CodeBlock = CodeBlock.EMPTY, + ): Builder = receiver(receiverType.asTypeName(), kdoc) + + public fun receiver( + receiverType: Type, + kdoc: String, + vararg args: Any, + ): Builder = receiver(receiverType, CodeBlock.of(kdoc, args)) + + @JvmOverloads public fun receiver( + receiverType: KClass<*>, + kdoc: CodeBlock = CodeBlock.EMPTY, + ): Builder = receiver(receiverType.asTypeName(), kdoc) + + public fun receiver( + receiverType: KClass<*>, + kdoc: String, + vararg args: Any, + ): Builder = receiver(receiverType, CodeBlock.of(kdoc, args)) + + @JvmOverloads public fun returns( + returnType: TypeName, + kdoc: CodeBlock = CodeBlock.EMPTY, + ): Builder = apply { + check(!name.isConstructor && !name.isAccessor) { "$name cannot have a return type" } + this.returnType = returnType + this.returnKdoc = kdoc + } + + @JvmOverloads public fun returns(returnType: Type, kdoc: CodeBlock = CodeBlock.EMPTY): Builder = + returns(returnType.asTypeName(), kdoc) + + public fun returns(returnType: Type, kdoc: String, vararg args: Any): Builder = + returns(returnType.asTypeName(), CodeBlock.of(kdoc, args)) + + @JvmOverloads public fun returns( + returnType: KClass<*>, + kdoc: CodeBlock = CodeBlock.EMPTY, + ): Builder = returns(returnType.asTypeName(), kdoc) + + public fun returns(returnType: KClass<*>, kdoc: String, vararg args: Any): Builder = + returns(returnType.asTypeName(), CodeBlock.of(kdoc, args)) + + public fun addParameters(parameterSpecs: Iterable<ParameterSpec>): Builder = apply { + for (parameterSpec in parameterSpecs) { + addParameter(parameterSpec) + } + } + + public fun addParameter(parameterSpec: ParameterSpec): Builder = apply { + parameters += parameterSpec + } + + public fun callThisConstructor(args: List<CodeBlock>): Builder = apply { + callConstructor("this", args) + } + + public fun callThisConstructor(args: Iterable<CodeBlock>): Builder = apply { + callConstructor("this", args.toList()) + } + + public fun callThisConstructor(vararg args: String): Builder = apply { + callConstructor("this", args.map { CodeBlock.of(it) }) + } + + public fun callThisConstructor(vararg args: CodeBlock = emptyArray()): Builder = apply { + callConstructor("this", args.toList()) + } + + public fun callSuperConstructor(args: Iterable<CodeBlock>): Builder = apply { + callConstructor("super", args.toList()) + } + + public fun callSuperConstructor(args: List<CodeBlock>): Builder = apply { + callConstructor("super", args) + } + + public fun callSuperConstructor(vararg args: String): Builder = apply { + callConstructor("super", args.map { CodeBlock.of(it) }) + } + + public fun callSuperConstructor(vararg args: CodeBlock = emptyArray()): Builder = apply { + callConstructor("super", args.toList()) + } + + private fun callConstructor(constructor: String, args: List<CodeBlock>) { + check(name.isConstructor) { "only constructors can delegate to other constructors!" } + delegateConstructor = constructor + delegateConstructorArguments = args + } + + public fun addParameter(name: String, type: TypeName, vararg modifiers: KModifier): Builder = + addParameter(ParameterSpec.builder(name, type, *modifiers).build()) + + public fun addParameter(name: String, type: Type, vararg modifiers: KModifier): Builder = + addParameter(name, type.asTypeName(), *modifiers) + + public fun addParameter(name: String, type: KClass<*>, vararg modifiers: KModifier): Builder = + addParameter(name, type.asTypeName(), *modifiers) + + public fun addParameter(name: String, type: TypeName, modifiers: Iterable<KModifier>): Builder = + addParameter(ParameterSpec.builder(name, type, modifiers).build()) + + public fun addParameter(name: String, type: Type, modifiers: Iterable<KModifier>): Builder = + addParameter(name, type.asTypeName(), modifiers) + + public fun addParameter( + name: String, + type: KClass<*>, + modifiers: Iterable<KModifier>, + ): Builder = addParameter(name, type.asTypeName(), modifiers) + + public fun addCode(format: String, vararg args: Any?): Builder = apply { + body.add(format, *args) + } + + public fun addNamedCode(format: String, args: Map<String, *>): Builder = apply { + body.addNamed(format, args) + } + + public fun addCode(codeBlock: CodeBlock): Builder = apply { + body.add(codeBlock) + } + + public fun addComment(format: String, vararg args: Any): Builder = apply { + body.add("//·${format.replace(' ', '·')}\n", *args) + } + + /** + * @param controlFlow the control flow construct and its code, such as "if (foo == 5)". + * * Shouldn't contain braces or newline characters. + */ + public fun beginControlFlow(controlFlow: String, vararg args: Any): Builder = apply { + body.beginControlFlow(controlFlow, *args) + } + + /** + * @param controlFlow the control flow construct and its code, such as "else if (foo == 10)". + * * Shouldn't contain braces or newline characters. + */ + public fun nextControlFlow(controlFlow: String, vararg args: Any): Builder = apply { + body.nextControlFlow(controlFlow, *args) + } + + public fun endControlFlow(): Builder = apply { + body.endControlFlow() + } + + public fun addStatement(format: String, vararg args: Any): Builder = apply { + body.addStatement(format, *args) + } + + public fun clearBody(): Builder = apply { + body.clear() + } + + public fun build(): FunSpec { + check(typeVariables.isEmpty() || !name.isAccessor) { "$name cannot have type variables" } + check(!(name == GETTER && parameters.isNotEmpty())) { "$name cannot have parameters" } + check(!(name == SETTER && parameters.size > 1)) { "$name can have at most one parameter" } + return FunSpec(this) + } + } + + public companion object { + private const val CONSTRUCTOR = "constructor()" + internal const val GETTER = "get()" + internal const val SETTER = "set()" + + internal val String.isConstructor get() = this == CONSTRUCTOR + internal val String.isAccessor get() = this.isOneOf(GETTER, SETTER) + + private val RETURN_EXPRESSION_BODY_PREFIX_SPACE = CodeBlock.of("return ") + private val RETURN_EXPRESSION_BODY_PREFIX_NBSP = CodeBlock.of("return·") + private val THROW_EXPRESSION_BODY_PREFIX_SPACE = CodeBlock.of("throw ") + private val THROW_EXPRESSION_BODY_PREFIX_NBSP = CodeBlock.of("throw·") + + @JvmStatic public fun builder(name: String): Builder = Builder(name) + + @JvmStatic public fun constructorBuilder(): Builder = Builder(CONSTRUCTOR) + + @JvmStatic public fun getterBuilder(): Builder = Builder(GETTER) + + @JvmStatic public fun setterBuilder(): Builder = Builder(SETTER) + + @DelicateKotlinPoetApi( + message = "Element APIs don't give complete information on Kotlin types. Consider using" + + " the kotlinpoet-metadata APIs instead.", + ) + @JvmStatic + public fun overriding(method: ExecutableElement): Builder { + var modifiers: Set<Modifier> = method.modifiers + require( + Modifier.PRIVATE !in modifiers && + Modifier.FINAL !in modifiers && + Modifier.STATIC !in modifiers, + ) { + "cannot override method with modifiers: $modifiers" + } + + val methodName = method.simpleName.toString() + val funBuilder = builder(methodName) + + funBuilder.addModifiers(KModifier.OVERRIDE) + + modifiers = modifiers.toMutableSet() + modifiers.remove(Modifier.ABSTRACT) + funBuilder.jvmModifiers(modifiers) + + method.typeParameters + .map { it.asType() as TypeVariable } + .map { it.asTypeVariableName() } + .forEach { funBuilder.addTypeVariable(it) } + + funBuilder.returns(method.returnType.asTypeName()) + funBuilder.addParameters(ParameterSpec.parametersOf(method)) + if (method.isVarArgs) { + funBuilder.parameters[funBuilder.parameters.lastIndex] = funBuilder.parameters.last() + .toBuilder() + .addModifiers(VARARG) + .build() + } + + if (method.thrownTypes.isNotEmpty()) { + val throwsValueString = method.thrownTypes.joinToString { "%T::class" } + funBuilder.addAnnotation( + AnnotationSpec.builder(Throws::class) + .addMember(throwsValueString, *method.thrownTypes.toTypedArray()) + .build(), + ) + } + + return funBuilder + } + + @Deprecated( + message = "Element APIs don't give complete information on Kotlin types. Consider using" + + " the kotlinpoet-metadata APIs instead.", + level = WARNING, + ) + @JvmStatic + public fun overriding( + method: ExecutableElement, + enclosing: DeclaredType, + types: Types, + ): Builder { + val executableType = types.asMemberOf(enclosing, method) as ExecutableType + val resolvedParameterTypes = executableType.parameterTypes + val resolvedReturnType = executableType.returnType + + val builder = overriding(method) + builder.returns(resolvedReturnType.asTypeName()) + var i = 0 + val size = builder.parameters.size + while (i < size) { + val parameter = builder.parameters[i] + val type = resolvedParameterTypes[i].asTypeName() + builder.parameters[i] = parameter.toBuilder(parameter.name, type).build() + i++ + } + + return builder + } + } +} diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/Import.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/Import.kt new file mode 100644 index 00000000..59b6b12e --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/Import.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2017 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +public data class Import internal constructor( + val qualifiedName: String, + val alias: String? = null, +) : Comparable<Import> { + + private val importString = buildString { + append(qualifiedName.escapeSegmentsIfNecessary()) + if (alias != null) { + append("·as·${alias.escapeIfNecessary()}") + } + } + + override fun toString(): String = importString + + override fun compareTo(other: Import): Int = importString.compareTo(other.importString) +} diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/KModifier.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/KModifier.kt new file mode 100644 index 00000000..81d03570 --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/KModifier.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2017 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.squareup.kotlinpoet.KModifier.INTERNAL +import com.squareup.kotlinpoet.KModifier.PRIVATE +import com.squareup.kotlinpoet.KModifier.PROTECTED +import com.squareup.kotlinpoet.KModifier.PUBLIC +import java.util.EnumSet + +public enum class KModifier( + internal val keyword: String, + private vararg val targets: Target, +) { + // Modifier order defined here: + // https://kotlinlang.org/docs/reference/coding-conventions.html#modifiers + + // Access. + PUBLIC("public", Target.PROPERTY), + PROTECTED("protected", Target.PROPERTY), + PRIVATE("private", Target.PROPERTY), + INTERNAL("internal", Target.PROPERTY), + + // Multiplatform modules. + EXPECT("expect", Target.CLASS, Target.FUNCTION, Target.PROPERTY), + ACTUAL("actual", Target.CLASS, Target.FUNCTION, Target.PROPERTY), + + FINAL("final", Target.CLASS, Target.FUNCTION, Target.PROPERTY), + OPEN("open", Target.CLASS, Target.FUNCTION, Target.PROPERTY), + ABSTRACT("abstract", Target.CLASS, Target.FUNCTION, Target.PROPERTY), + SEALED("sealed", Target.CLASS), + CONST("const", Target.PROPERTY), + + EXTERNAL("external", Target.CLASS, Target.FUNCTION, Target.PROPERTY), + OVERRIDE("override", Target.FUNCTION, Target.PROPERTY), + LATEINIT("lateinit", Target.PROPERTY), + TAILREC("tailrec", Target.FUNCTION), + VARARG("vararg", Target.PARAMETER), + SUSPEND("suspend", Target.FUNCTION), + INNER("inner", Target.CLASS), + + ENUM("enum", Target.CLASS), + ANNOTATION("annotation", Target.CLASS), + VALUE("value", Target.CLASS), + FUN("fun", Target.INTERFACE), + + COMPANION("companion", Target.CLASS), + + // Call-site compiler tips. + INLINE("inline", Target.FUNCTION), + NOINLINE("noinline", Target.PARAMETER), + CROSSINLINE("crossinline", Target.PARAMETER), + REIFIED("reified", Target.TYPE_PARAMETER), + + INFIX("infix", Target.FUNCTION), + OPERATOR("operator", Target.FUNCTION), + + DATA("data", Target.CLASS), + + IN("in", Target.VARIANCE_ANNOTATION), + OUT("out", Target.VARIANCE_ANNOTATION), + ; + + internal enum class Target { + CLASS, + VARIANCE_ANNOTATION, + PARAMETER, + TYPE_PARAMETER, + FUNCTION, + PROPERTY, + INTERFACE, + } + + internal fun checkTarget(target: Target) { + require(target in targets) { "unexpected modifier $this for $target" } + } +} + +internal val VISIBILITY_MODIFIERS: Set<KModifier> = EnumSet.of(PUBLIC, INTERNAL, PROTECTED, PRIVATE) diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/KOperator.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/KOperator.kt new file mode 100644 index 00000000..461a39f2 --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/KOperator.kt @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2020 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +public enum class KOperator( + internal val operator: String, + internal val functionName: String, +) { + UNARY_PLUS("+", "unaryPlus"), + PLUS("+", "plus"), + UNARY_MINUS("-", "unaryMinus"), + MINUS("-", "minus"), + TIMES("*", "times"), + DIV("/", "div"), + REM("%", "rem"), + PLUS_ASSIGN("+=", "plusAssign"), + MINUS_ASSIGN("-=", "minusAssign"), + TIMES_ASSIGN("*=", "timesAssign"), + DIV_ASSIGN("/=", "divAssign"), + REM_ASSIGN("%=", "remAssign"), + INC("++", "inc"), + DEC("--", "dec"), + EQUALS("==", "equals"), + NOT_EQUALS("!=", "equals"), + NOT("!", "not"), + RANGE_TO("..", "rangeTo"), + CONTAINS("in", "contains"), + NOT_CONTAINS("!in", "contains"), + GT(">", "compareTo"), + LT("<", "compareTo"), + GE(">=", "compareTo"), + LE("<=", "compareTo"), + ITERATOR("in", "iterator"), +} diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/LambdaTypeName.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/LambdaTypeName.kt new file mode 100644 index 00000000..efb3b837 --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/LambdaTypeName.kt @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import kotlin.reflect.KClass + +@OptIn(ExperimentalKotlinPoetApi::class) +public class LambdaTypeName private constructor( + public val receiver: TypeName? = null, + @property:ExperimentalKotlinPoetApi + public val contextReceivers: List<TypeName> = emptyList(), + parameters: List<ParameterSpec> = emptyList(), + public val returnType: TypeName = UNIT, + nullable: Boolean = false, + public val isSuspending: Boolean = false, + annotations: List<AnnotationSpec> = emptyList(), + tags: Map<KClass<*>, Any> = emptyMap(), +) : TypeName(nullable, annotations, TagMap(tags)) { + public val parameters: List<ParameterSpec> = parameters.toImmutableList() + + init { + for (param in parameters) { + require(param.annotations.isEmpty()) { "Parameters with annotations are not allowed" } + require(param.modifiers.isEmpty()) { "Parameters with modifiers are not allowed" } + require(param.defaultValue == null) { "Parameters with default values are not allowed" } + } + } + + override fun copy( + nullable: Boolean, + annotations: List<AnnotationSpec>, + tags: Map<KClass<*>, Any>, + ): LambdaTypeName { + return copy(nullable, annotations, this.isSuspending, tags) + } + + public fun copy( + nullable: Boolean = this.isNullable, + annotations: List<AnnotationSpec> = this.annotations.toList(), + suspending: Boolean = this.isSuspending, + tags: Map<KClass<*>, Any> = this.tags.toMap(), + ): LambdaTypeName { + return LambdaTypeName(receiver, contextReceivers, parameters, returnType, nullable, suspending, annotations, tags) + } + + override fun emit(out: CodeWriter): CodeWriter { + if (isNullable) { + out.emit("(") + } + + if (isSuspending) { + out.emit("suspend·") + } + + out.emitContextReceivers(contextReceivers, suffix = "·") + + receiver?.let { + if (it.isAnnotated) { + out.emitCode("(%T).", it) + } else { + out.emitCode("%T.", it) + } + } + + parameters.emit(out) + out.emitCode(if (returnType is LambdaTypeName) "·->·(%T)" else "·->·%T", returnType) + + if (isNullable) { + out.emit(")") + } + return out + } + + public companion object { + /** Returns a lambda type with `returnType` and parameters listed in `parameters`. */ + @ExperimentalKotlinPoetApi @JvmStatic + public fun get( + receiver: TypeName? = null, + parameters: List<ParameterSpec> = emptyList(), + returnType: TypeName, + contextReceivers: List<TypeName> = emptyList(), + ): LambdaTypeName = LambdaTypeName(receiver, contextReceivers, parameters, returnType) + + /** Returns a lambda type with `returnType` and parameters listed in `parameters`. */ + @JvmStatic public fun get( + receiver: TypeName? = null, + parameters: List<ParameterSpec> = emptyList(), + returnType: TypeName, + ): LambdaTypeName = LambdaTypeName(receiver, emptyList(), parameters, returnType) + + /** Returns a lambda type with `returnType` and parameters listed in `parameters`. */ + @JvmStatic public fun get( + receiver: TypeName? = null, + vararg parameters: TypeName = emptyArray(), + returnType: TypeName, + ): LambdaTypeName { + return LambdaTypeName( + receiver, + emptyList(), + parameters.toList().map { ParameterSpec.unnamed(it) }, + returnType, + ) + } + + /** Returns a lambda type with `returnType` and parameters listed in `parameters`. */ + @JvmStatic public fun get( + receiver: TypeName? = null, + vararg parameters: ParameterSpec = emptyArray(), + returnType: TypeName, + ): LambdaTypeName = LambdaTypeName(receiver, emptyList(), parameters.toList(), returnType) + } +} diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/LineWrapper.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/LineWrapper.kt new file mode 100644 index 00000000..83a155b0 --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/LineWrapper.kt @@ -0,0 +1,175 @@ +/* + * Copyright (C) 2016 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import java.io.Closeable + +/** + * Implements soft line wrapping on an appendable. To use, append characters using + * [LineWrapper.append], which will replace spaces with newlines where necessary. Use + * [LineWrapper.appendNonWrapping] to append a string that never wraps. + */ +internal class LineWrapper( + private val out: Appendable, + private val indent: String, + private val columnLimit: Int, +) : Closeable { + + private var closed = false + + /** + * Segments of the current line to be joined by spaces or wraps. Never empty, but contains a lone + * empty string if no data has been emitted since the last newline. + */ + private val segments = mutableListOf("") + + /** Number of indents in wraps. -1 if the current line has no wraps. */ + private var indentLevel = -1 + + /** Optional prefix that will be prepended to wrapped lines. */ + private var linePrefix = "" + + /** @return whether or not there are pending segments for the current line. */ + val hasPendingSegments get() = segments.size != 1 || segments[0].isNotEmpty() + + /** Emit `s` replacing its spaces with line wraps as necessary. */ + fun append(s: String, indentLevel: Int = -1, linePrefix: String = "") { + check(!closed) { "closed" } + + var pos = 0 + while (pos < s.length) { + when (s[pos]) { + ' ' -> { + // Each space starts a new empty segment. + this.indentLevel = indentLevel + this.linePrefix = linePrefix + segments += "" + pos++ + } + + '\n' -> { + // Each newline emits the current segments. + newline() + pos++ + } + + '·' -> { + // Render · as a non-breaking space. + segments[segments.size - 1] += " " + pos++ + } + + else -> { + var next = s.indexOfAny(SPECIAL_CHARACTERS, pos) + if (next == -1) next = s.length + segments[segments.size - 1] += s.substring(pos, next) + pos = next + } + } + } + } + + /** Emit `s` leaving spaces as-is. */ + fun appendNonWrapping(s: String) { + check(!closed) { "closed" } + require(!s.contains("\n")) + + segments[segments.size - 1] += s + } + + fun newline() { + check(!closed) { "closed" } + + emitCurrentLine() + out.append("\n") + indentLevel = -1 + } + + /** Flush any outstanding text and forbid future writes to this line wrapper. */ + override fun close() { + emitCurrentLine() + closed = true + } + + private fun emitCurrentLine() { + foldUnsafeBreaks() + + var start = 0 + var columnCount = segments[0].length + + for (i in 1 until segments.size) { + val segment = segments[i] + val newColumnCount = columnCount + 1 + segment.length + + // If this segment doesn't fit in the current run, print the current run and start a new one. + if (newColumnCount > columnLimit) { + emitSegmentRange(start, i) + start = i + columnCount = segment.length + indent.length * indentLevel + continue + } + + columnCount = newColumnCount + } + + // Print the last run. + emitSegmentRange(start, segments.size) + + segments.clear() + segments += "" + } + + private fun emitSegmentRange(startIndex: Int, endIndex: Int) { + // If this is a wrapped line we need a newline and an indent. + if (startIndex > 0) { + out.append("\n") + for (i in 0 until indentLevel) { + out.append(indent) + } + out.append(linePrefix) + } + + // Emit each segment separated by spaces. + out.append(segments[startIndex]) + for (i in startIndex + 1 until endIndex) { + out.append(" ") + out.append(segments[i]) + } + } + + /** + * Any segment that starts with '+' or '-' can't have a break preceding it. Combine it with the + * preceding segment. Note that this doesn't apply to the first segment. + */ + private fun foldUnsafeBreaks() { + var i = 1 + while (i < segments.size) { + val segment = segments[i] + if (UNSAFE_LINE_START.matches(segment)) { + segments[i - 1] = segments[i - 1] + " " + segments[i] + segments.removeAt(i) + if (i > 1) i-- + } else { + i++ + } + } + } + + companion object { + private val UNSAFE_LINE_START = Regex("\\s*[-+].*") + private val SPECIAL_CHARACTERS = " \n·".toCharArray() + } +} diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/MemberName.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/MemberName.kt new file mode 100644 index 00000000..c70a10ac --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/MemberName.kt @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import kotlin.reflect.KClass + +/** + * Represents the name of a member (such as a function or a property). + * + * @param packageName e.g. `kotlin.collections` + * @param enclosingClassName e.g. `Map.Entry.Companion`, if the member is declared inside the + * companion object of the Map.Entry class + * @param simpleName e.g. `isBlank`, `size` + * @param isExtension whether the member is an extension property or an extension function. Default + * is false. + * + * If there is a member with the same name as this member in a local scope, the generated code will + * include this member's fully-qualified name to avoid ambiguity, e.g.: + * + * ```kotlin + * package com.squareup.tacos + * + * import kotlin.Unit + * + * public class TacoTest { + * public fun test(): Unit { + * kotlin.error("errorText") + * } + * + * public fun error(): Unit { + * } + * } + * ``` + * + * However, since Kotlin compiler does not allow fully-qualified extension members, if [isExtension] + * is set to true for this [MemberName], the generated code will include an import for this member + * and its simple name at the call site, e.g.: + * + * ```kotlin + * package com.squareup.tacos + * + * import kotlin.Unit + * import kotlin.hashCode + * + * public class TacoTest { + * public override fun hashCode(): Unit { + * var result = super.hashCode + * if (result == 0) { + * result = result * 37 + embedded_message.hashCode() + * super.hashCode = result + * } + * return result + * } + * } + * ``` + */ +public data class MemberName internal constructor( + public val packageName: String, + public val enclosingClassName: ClassName?, + public val simpleName: String, + public val operator: KOperator? = null, + public val isExtension: Boolean = false, +) { + // TODO(egorand): Reduce the number of overloaded constructors in KotlinPoet 2.0. + + public constructor( + packageName: String, + simpleName: String, + ) : this(packageName, enclosingClassName = null, simpleName) + + public constructor( + packageName: String, + simpleName: String, + isExtension: Boolean, + ) : this(packageName, enclosingClassName = null, simpleName, operator = null, isExtension) + + public constructor( + enclosingClassName: ClassName, + simpleName: String, + ) : this(enclosingClassName.packageName, enclosingClassName, simpleName) + + public constructor( + enclosingClassName: ClassName, + simpleName: String, + isExtension: Boolean, + ) : this(enclosingClassName.packageName, enclosingClassName, simpleName, operator = null, isExtension) + + public constructor( + packageName: String, + operator: KOperator, + ) : this(packageName, enclosingClassName = null, operator.functionName, operator) + + public constructor( + enclosingClassName: ClassName, + operator: KOperator, + ) : this(enclosingClassName.packageName, enclosingClassName, operator.functionName, operator) + + /** Fully qualified name using `.` as a separator, like `kotlin.String.isBlank`. */ + public val canonicalName: String = buildString { + if (enclosingClassName != null) { + append(enclosingClassName.canonicalName) + append('.') + } else if (packageName.isNotBlank()) { + append(packageName) + append('.') + } + append(simpleName) + } + + /** + * Callable reference to this member. Emits [enclosingClassName] if it exists, followed by + * the reference operator `::`, followed by either [simpleName] or the fully-qualified + * name if this is a top-level member. + * + * Note: As `::$packageName.$simpleName` is not valid syntax, an aliased import may be + * required for a top-level member with a conflicting name. + */ + public fun reference(): CodeBlock = when (enclosingClassName) { + null -> CodeBlock.of("::%M", this) + else -> CodeBlock.of("%T::%N", enclosingClassName, simpleName) + } + + internal fun emit(out: CodeWriter) { + if (operator == null) { + out.emit(out.lookupName(this).escapeSegmentsIfNecessary()) + } else { + out.lookupName(this) + out.emit(operator.operator) + } + } + + override fun toString(): String = canonicalName + + public companion object { + @Suppress("NOTHING_TO_INLINE") + @JvmSynthetic + @JvmStatic + public inline fun ClassName.member(simpleName: String): MemberName = + MemberName(this, simpleName) + + @JvmStatic + @JvmName("get") + public fun KClass<*>.member(simpleName: String): MemberName = + asClassName().member(simpleName) + + @DelicateKotlinPoetApi( + message = "Java reflection APIs don't give complete information on Kotlin types. Consider " + + "using the kotlinpoet-metadata APIs instead.", + ) + @JvmStatic + @JvmName("get") + public fun Class<*>.member(simpleName: String): MemberName = + asClassName().member(simpleName) + } +} diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/NameAllocator.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/NameAllocator.kt new file mode 100644 index 00000000..cda10042 --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/NameAllocator.kt @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import java.util.UUID + +/** + * Assigns Kotlin identifier names to avoid collisions, keywords, and invalid characters. To use, + * first create an instance and allocate all of the names that you need. Typically this is a + * mix of user-supplied names and constants: + * + * ```kotlin + * val nameAllocator = NameAllocator() + * for (property in properties) { + * nameAllocator.newName(property.name, property) + * } + * nameAllocator.newName("sb", "string builder") + * ``` + * + * Pass a unique tag object to each allocation. The tag scopes the name, and can be used to look up + * the allocated name later. Typically the tag is the object that is being named. In the above + * example we use `property` for the user-supplied property names, and `"string builder"` for our + * constant string builder. + * + * Once we've allocated names we can use them when generating code: + * + * ```kotlin + * val builder = FunSpec.builder("toString") + * .addModifiers(KModifier.OVERRIDE) + * .returns(String::class) + * + * builder.addStatement("val %N = %T()", + * nameAllocator.get("string builder"), StringBuilder::class) + * + * for (property in properties) { + * builder.addStatement("%N.append(%N)", + * nameAllocator.get("string builder"), nameAllocator.get(property)) + * } + * builder.addStatement("return %N.toString()", nameAllocator.get("string builder")) + * return builder.build() + * ``` + * + * The above code generates unique names if presented with conflicts. Given user-supplied properties + * with names `ab` and `sb` this generates the following: + * + * ```kotlin + * override fun toString(): kotlin.String { + * val sb_ = java.lang.StringBuilder() + * sb_.append(ab) + * sb_.append(sb) + * return sb_.toString() + * } + * ``` + * + * The underscore is appended to `sb` to avoid conflicting with the user-supplied `sb` property. + * Underscores are also prefixed for names that start with a digit, and used to replace name-unsafe + * characters like space or dash. + * + * When dealing with multiple independent inner scopes, use a [copy][NameAllocator.copy] of the + * NameAllocator used for the outer scope to further refine name allocation for a specific inner + * scope. + */ +public class NameAllocator private constructor( + private val allocatedNames: MutableSet<String>, + private val tagToName: MutableMap<Any, String>, +) { + public constructor() : this(mutableSetOf(), mutableMapOf()) + + /** + * Return a new name using `suggestion` that will not be a Java identifier or clash with other + * names. The returned value can be queried multiple times by passing `tag` to + * [NameAllocator.get]. + */ + @JvmOverloads public fun newName( + suggestion: String, + tag: Any = UUID.randomUUID().toString(), + ): String { + var result = toJavaIdentifier(suggestion) + while (result.isKeyword || !allocatedNames.add(result)) { + result += "_" + } + + val replaced = tagToName.put(tag, result) + if (replaced != null) { + tagToName[tag] = replaced // Put things back as they were! + throw IllegalArgumentException("tag $tag cannot be used for both '$replaced' and '$result'") + } + + return result + } + + /** Retrieve a name created with [NameAllocator.newName]. */ + public operator fun get(tag: Any): String = requireNotNull(tagToName[tag]) { "unknown tag: $tag" } + + /** + * Create a deep copy of this NameAllocator. Useful to create multiple independent refinements + * of a NameAllocator to be used in the respective definition of multiples, independently-scoped, + * inner code blocks. + * + * @return A deep copy of this NameAllocator. + */ + public fun copy(): NameAllocator { + return NameAllocator(allocatedNames.toMutableSet(), tagToName.toMutableMap()) + } +} + +private fun toJavaIdentifier(suggestion: String) = buildString { + var i = 0 + while (i < suggestion.length) { + val codePoint = suggestion.codePointAt(i) + if (i == 0 && + !Character.isJavaIdentifierStart(codePoint) && + Character.isJavaIdentifierPart(codePoint) + ) { + append("_") + } + + val validCodePoint: Int = if (Character.isJavaIdentifierPart(codePoint)) { + codePoint + } else { + '_'.code + } + appendCodePoint(validCodePoint) + i += Character.charCount(codePoint) + } +} diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/OriginatingElementsHolder.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/OriginatingElementsHolder.kt new file mode 100644 index 00000000..27008213 --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/OriginatingElementsHolder.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import javax.lang.model.element.Element + +/** A type that can have originating [elements][Element]. */ +public interface OriginatingElementsHolder { + + /** The originating elements of this type. */ + public val originatingElements: List<Element> + + /** The builder analogue to [OriginatingElementsHolder] types. */ + public interface Builder<out T : Builder<T>> { + + /** Mutable map of the current originating elements this builder contains. */ + public val originatingElements: MutableList<Element> + + /** Adds an [originatingElement] to this type's list of originating elements. */ + @Suppress("UNCHECKED_CAST") + public fun addOriginatingElement(originatingElement: Element): T = apply { + originatingElements += originatingElement + } as T + } +} + +internal fun OriginatingElementsHolder.Builder<*>.buildOriginatingElements() = + OriginatingElements(originatingElements.toImmutableList()) + +internal fun List<Element>.buildOriginatingElements() = + OriginatingElements(toImmutableList()) + +@JvmInline +internal value class OriginatingElements( + override val originatingElements: List<Element>, +) : OriginatingElementsHolder diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/ParameterSpec.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/ParameterSpec.kt new file mode 100644 index 00000000..9c33ecd9 --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/ParameterSpec.kt @@ -0,0 +1,257 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.squareup.kotlinpoet.KModifier.CROSSINLINE +import com.squareup.kotlinpoet.KModifier.NOINLINE +import com.squareup.kotlinpoet.KModifier.VARARG +import java.lang.reflect.Type +import javax.lang.model.element.ExecutableElement +import javax.lang.model.element.Modifier +import javax.lang.model.element.VariableElement +import kotlin.DeprecationLevel.ERROR +import kotlin.reflect.KClass + +/** A generated parameter declaration. */ +public class ParameterSpec private constructor( + builder: Builder, + private val tagMap: TagMap = builder.buildTagMap(), +) : Taggable by tagMap { + public val name: String = builder.name + public val kdoc: CodeBlock = builder.kdoc.build() + public val annotations: List<AnnotationSpec> = builder.annotations.toImmutableList() + public val modifiers: Set<KModifier> = builder.modifiers + .also { + LinkedHashSet(it).apply { + removeAll(ALLOWED_PARAMETER_MODIFIERS) + if (!isEmpty()) { + throw IllegalArgumentException("Modifiers $this are not allowed on Kotlin parameters. Allowed modifiers: $ALLOWED_PARAMETER_MODIFIERS") + } + } + } + .toImmutableSet() + public val type: TypeName = builder.type + public val defaultValue: CodeBlock? = builder.defaultValue + + public constructor(name: String, type: TypeName, vararg modifiers: KModifier) : + this(builder(name, type, *modifiers)) + public constructor(name: String, type: TypeName, modifiers: Iterable<KModifier>) : + this(builder(name, type, modifiers)) + + internal fun emit( + codeWriter: CodeWriter, + includeType: Boolean = true, + emitKdoc: Boolean = false, + inlineAnnotations: Boolean = true, + ) { + if (emitKdoc) codeWriter.emitKdoc(kdoc.ensureEndsWithNewLine()) + codeWriter.emitAnnotations(annotations, inlineAnnotations) + codeWriter.emitModifiers(modifiers) + if (name.isNotEmpty()) codeWriter.emitCode("%N", this) + if (name.isNotEmpty() && includeType) codeWriter.emitCode(":·") + if (includeType) codeWriter.emitCode("%T", type) + emitDefaultValue(codeWriter) + } + + internal fun emitDefaultValue(codeWriter: CodeWriter) { + if (defaultValue != null) { + codeWriter.emitCode(if (defaultValue.hasStatements()) " = %L" else " = «%L»", defaultValue) + } + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null) return false + if (javaClass != other.javaClass) return false + return toString() == other.toString() + } + + override fun hashCode(): Int = toString().hashCode() + + override fun toString(): String = buildCodeString { emit(this) } + + public fun toBuilder(name: String = this.name, type: TypeName = this.type): Builder { + val builder = Builder(name, type) + builder.kdoc.add(kdoc) + builder.annotations += annotations + builder.modifiers += modifiers + builder.defaultValue = defaultValue + builder.tags += tagMap.tags + return builder + } + + public class Builder internal constructor( + internal val name: String, + internal val type: TypeName, + ) : Taggable.Builder<Builder> { + internal var defaultValue: CodeBlock? = null + + public val kdoc: CodeBlock.Builder = CodeBlock.builder() + public val annotations: MutableList<AnnotationSpec> = mutableListOf() + public val modifiers: MutableList<KModifier> = mutableListOf() + override val tags: MutableMap<KClass<*>, Any> = mutableMapOf() + + public fun addKdoc(format: String, vararg args: Any): Builder = apply { + kdoc.add(format, *args) + } + + public fun addKdoc(block: CodeBlock): Builder = apply { + kdoc.add(block) + } + + public fun addAnnotations(annotationSpecs: Iterable<AnnotationSpec>): Builder = apply { + annotations += annotationSpecs + } + + public fun addAnnotation(annotationSpec: AnnotationSpec): Builder = apply { + annotations += annotationSpec + } + + public fun addAnnotation(annotation: ClassName): Builder = apply { + annotations += AnnotationSpec.builder(annotation).build() + } + + public fun addAnnotation(annotation: Class<*>): Builder = + addAnnotation(annotation.asClassName()) + + public fun addAnnotation(annotation: KClass<*>): Builder = + addAnnotation(annotation.asClassName()) + + public fun addModifiers(vararg modifiers: KModifier): Builder = apply { + this.modifiers += modifiers + } + + public fun addModifiers(modifiers: Iterable<KModifier>): Builder = apply { + this.modifiers += modifiers + } + + @Deprecated( + "There are no jvm modifiers applicable to parameters in Kotlin", + ReplaceWith(""), + level = ERROR, + ) + public fun jvmModifiers(modifiers: Iterable<Modifier>): Builder = apply { + throw IllegalArgumentException("JVM modifiers are not permitted on parameters in Kotlin") + } + + public fun defaultValue(format: String, vararg args: Any?): Builder = + defaultValue(CodeBlock.of(format, *args)) + + public fun defaultValue(codeBlock: CodeBlock?): Builder = apply { + this.defaultValue = codeBlock + } + + public fun build(): ParameterSpec = ParameterSpec(this) + } + + public companion object { + @DelicateKotlinPoetApi( + message = "Element APIs don't give complete information on Kotlin types. Consider using" + + " the kotlinpoet-metadata APIs instead.", + ) + @JvmStatic + public fun get(element: VariableElement): ParameterSpec { + val name = element.simpleName.toString() + val type = element.asType().asTypeName() + return builder(name, type) + .build() + } + + @DelicateKotlinPoetApi( + message = "Element APIs don't give complete information on Kotlin types. Consider using" + + " the kotlinpoet-metadata APIs instead.", + ) + @JvmStatic + public fun parametersOf(method: ExecutableElement): List<ParameterSpec> = + method.parameters.map(::get) + + @JvmStatic public fun builder( + name: String, + type: TypeName, + vararg modifiers: KModifier, + ): Builder { + return Builder(name, type).addModifiers(*modifiers) + } + + @JvmStatic public fun builder(name: String, type: Type, vararg modifiers: KModifier): Builder = + builder(name, type.asTypeName(), *modifiers) + + @JvmStatic public fun builder( + name: String, + type: KClass<*>, + vararg modifiers: KModifier, + ): Builder = builder(name, type.asTypeName(), *modifiers) + + @JvmStatic public fun builder( + name: String, + type: TypeName, + modifiers: Iterable<KModifier>, + ): Builder { + return Builder(name, type).addModifiers(modifiers) + } + + @JvmStatic public fun builder( + name: String, + type: Type, + modifiers: Iterable<KModifier>, + ): Builder = builder(name, type.asTypeName(), modifiers) + + @JvmStatic public fun builder( + name: String, + type: KClass<*>, + modifiers: Iterable<KModifier>, + ): Builder = builder(name, type.asTypeName(), modifiers) + + @JvmStatic public fun unnamed(type: KClass<*>): ParameterSpec = unnamed(type.asTypeName()) + + @JvmStatic public fun unnamed(type: Type): ParameterSpec = unnamed(type.asTypeName()) + + @JvmStatic public fun unnamed(type: TypeName): ParameterSpec = Builder("", type).build() + } +} + +// From https://kotlinlang.org/spec/syntax-and-grammar.html#grammar-rule-parameterModifier +private val ALLOWED_PARAMETER_MODIFIERS = setOf(VARARG, NOINLINE, CROSSINLINE) + +internal fun List<ParameterSpec>.emit( + codeWriter: CodeWriter, + forceNewLines: Boolean = false, + emitBlock: (ParameterSpec) -> Unit = { it.emit(codeWriter) }, +) = with(codeWriter) { + emit("(") + + if (isNotEmpty()) { + val emitNewLines = size > 2 || forceNewLines + if (emitNewLines) { + emit("\n") + indent(1) + } + forEachIndexed { index, parameter -> + if (index > 0) { + emit(if (emitNewLines) "\n" else ", ") + } + emitBlock(parameter) + if (emitNewLines) { + emit(",") + } + } + if (emitNewLines) { + unindent(1) + emit("\n") + } + } + emit(")") +} diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/ParameterizedTypeName.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/ParameterizedTypeName.kt new file mode 100644 index 00000000..ffbcf388 --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/ParameterizedTypeName.kt @@ -0,0 +1,254 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:JvmName("ParameterizedTypeNames") + +package com.squareup.kotlinpoet + +import java.lang.reflect.Modifier +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type +import kotlin.reflect.KClass +import kotlin.reflect.KType +import kotlin.reflect.KTypeParameter +import kotlin.reflect.KTypeProjection +import kotlin.reflect.KVariance + +public class ParameterizedTypeName internal constructor( + private val enclosingType: TypeName?, + public val rawType: ClassName, + typeArguments: List<TypeName>, + nullable: Boolean = false, + annotations: List<AnnotationSpec> = emptyList(), + tags: Map<KClass<*>, Any> = emptyMap(), +) : TypeName(nullable, annotations, TagMap(tags)) { + public val typeArguments: List<TypeName> = typeArguments.toImmutableList() + + init { + require(typeArguments.isNotEmpty() || enclosingType != null) { + "no type arguments: $rawType" + } + } + + override fun copy( + nullable: Boolean, + annotations: List<AnnotationSpec>, + tags: Map<KClass<*>, Any>, + ): ParameterizedTypeName { + return ParameterizedTypeName(enclosingType, rawType, typeArguments, nullable, annotations, tags) + } + + public fun copy( + nullable: Boolean = this.isNullable, + annotations: List<AnnotationSpec> = this.annotations, + tags: Map<KClass<*>, Any> = this.tags, + typeArguments: List<TypeName> = this.typeArguments, + ): ParameterizedTypeName { + return ParameterizedTypeName(enclosingType, rawType, typeArguments, nullable, annotations, tags) + } + + public fun plusParameter(typeArgument: TypeName): ParameterizedTypeName = + ParameterizedTypeName( + enclosingType, + rawType, + typeArguments + typeArgument, + isNullable, + annotations, + ) + + public fun plusParameter(typeArgument: KClass<*>): ParameterizedTypeName = + plusParameter(typeArgument.asClassName()) + + public fun plusParameter(typeArgument: Class<*>): ParameterizedTypeName = + plusParameter(typeArgument.asClassName()) + + override fun emit(out: CodeWriter): CodeWriter { + if (enclosingType != null) { + enclosingType.emitAnnotations(out) + enclosingType.emit(out) + out.emit("." + rawType.simpleName) + } else { + rawType.emitAnnotations(out) + rawType.emit(out) + } + if (typeArguments.isNotEmpty()) { + out.emit("<") + typeArguments.forEachIndexed { index, parameter -> + if (index > 0) out.emit(",·") + parameter.emitAnnotations(out) + parameter.emit(out) + parameter.emitNullable(out) + } + out.emit(">") + } + return out + } + + /** + * Returns a new [ParameterizedTypeName] instance for the specified `name` as nested inside this + * class, with the specified `typeArguments`. + */ + public fun nestedClass(name: String, typeArguments: List<TypeName>): ParameterizedTypeName = + ParameterizedTypeName(this, rawType.nestedClass(name), typeArguments) + + public companion object { + /** Returns a parameterized type, applying `typeArguments` to `this`. */ + @JvmStatic + @JvmName("get") + public fun ClassName.parameterizedBy( + vararg typeArguments: TypeName, + ): ParameterizedTypeName = ParameterizedTypeName(null, this, typeArguments.toList()) + + /** Returns a parameterized type, applying `typeArguments` to `this`. */ + @JvmStatic + @JvmName("get") + public fun KClass<*>.parameterizedBy( + vararg typeArguments: KClass<*>, + ): ParameterizedTypeName = + ParameterizedTypeName(null, asClassName(), typeArguments.map { it.asTypeName() }) + + /** Returns a parameterized type, applying `typeArguments` to `this`. */ + @JvmStatic + @JvmName("get") + public fun Class<*>.parameterizedBy( + vararg typeArguments: Type, + ): ParameterizedTypeName = + ParameterizedTypeName(null, asClassName(), typeArguments.map { it.asTypeName() }) + + /** Returns a parameterized type, applying `typeArguments` to `this`. */ + @JvmStatic + @JvmName("get") + public fun ClassName.parameterizedBy( + typeArguments: List<TypeName>, + ): ParameterizedTypeName = ParameterizedTypeName(null, this, typeArguments) + + /** Returns a parameterized type, applying `typeArguments` to `this`. */ + @JvmStatic + @JvmName("get") + public fun KClass<*>.parameterizedBy( + typeArguments: Iterable<KClass<*>>, + ): ParameterizedTypeName = + ParameterizedTypeName(null, asClassName(), typeArguments.map { it.asTypeName() }) + + /** Returns a parameterized type, applying `typeArguments` to `this`. */ + @JvmStatic + @JvmName("get") + public fun Class<*>.parameterizedBy( + typeArguments: Iterable<Type>, + ): ParameterizedTypeName = + ParameterizedTypeName(null, asClassName(), typeArguments.map { it.asTypeName() }) + + /** Returns a parameterized type, applying `typeArgument` to `this`. */ + @JvmStatic + @JvmName("get") + public fun ClassName.plusParameter( + typeArgument: TypeName, + ): ParameterizedTypeName = parameterizedBy(typeArgument) + + /** Returns a parameterized type, applying `typeArgument` to `this`. */ + @JvmStatic + @JvmName("get") + public fun KClass<*>.plusParameter( + typeArgument: KClass<*>, + ): ParameterizedTypeName = parameterizedBy(typeArgument) + + /** Returns a parameterized type, applying `typeArgument` to `this`. */ + @JvmStatic + @JvmName("get") + public fun Class<*>.plusParameter( + typeArgument: Class<*>, + ): ParameterizedTypeName = parameterizedBy(typeArgument) + + /** Returns a parameterized type equivalent to `type`. */ + internal fun get( + type: ParameterizedType, + map: MutableMap<Type, TypeVariableName>, + ): ParameterizedTypeName { + val rawType = (type.rawType as Class<*>).asClassName() + val ownerType = if (type.ownerType is ParameterizedType && + !Modifier.isStatic((type.rawType as Class<*>).modifiers) + ) { + type.ownerType as ParameterizedType + } else { + null + } + + val typeArguments = type.actualTypeArguments.map { get(it, map = map) } + return if (ownerType != null) { + get(ownerType, map = map).nestedClass(rawType.simpleName, typeArguments) + } else { + ParameterizedTypeName(null, rawType, typeArguments) + } + } + + /** Returns a type name equivalent to type with given list of type arguments. */ + internal fun get( + type: KClass<*>, + nullable: Boolean, + typeArguments: List<KTypeProjection>, + ): TypeName { + if (typeArguments.isEmpty()) { + return type.asTypeName().run { if (nullable) copy(nullable = true) else this } + } + + val effectiveType = if (type.java.isArray) Array<Unit>::class else type + val enclosingClass = type.java.enclosingClass?.kotlin + + return ParameterizedTypeName( + enclosingClass?.let { + get(it, false, typeArguments.drop(effectiveType.typeParameters.size)) + }, + effectiveType.asTypeName(), + typeArguments.take(effectiveType.typeParameters.size).map { (paramVariance, paramType) -> + val typeName = paramType?.asTypeName() ?: return@map STAR + when (paramVariance) { + null -> STAR + KVariance.INVARIANT -> typeName + KVariance.IN -> WildcardTypeName.consumerOf(typeName) + KVariance.OUT -> WildcardTypeName.producerOf(typeName) + } + }, + nullable, + effectiveType.annotations.map { AnnotationSpec.get(it) }, + ) + } + } +} + +/** Returns a parameterized type equivalent to `type`. */ +@DelicateKotlinPoetApi( + message = "Java reflection APIs don't give complete information on Kotlin types. Consider " + + "using the kotlinpoet-metadata APIs instead.", +) +@JvmName("get") +public fun ParameterizedType.asParameterizedTypeName(): ParameterizedTypeName = + ParameterizedTypeName.get(this, mutableMapOf()) + +/** + * Returns a [TypeName] equivalent to the given Kotlin KType using reflection, maybe using kotlin-reflect + * if required. + */ +public fun KType.asTypeName(): TypeName { + val classifier = this.classifier + if (classifier is KTypeParameter) { + return classifier.asTypeVariableName().run { if (isMarkedNullable) copy(nullable = true) else this } + } + + if (classifier == null || classifier !is KClass<*>) { + throw IllegalArgumentException("Cannot build TypeName for $this") + } + + return ParameterizedTypeName.get(classifier, this.isMarkedNullable, this.arguments) +} diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/PropertySpec.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/PropertySpec.kt new file mode 100644 index 00000000..8fb58602 --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/PropertySpec.kt @@ -0,0 +1,342 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.squareup.kotlinpoet.FunSpec.Companion.GETTER +import com.squareup.kotlinpoet.FunSpec.Companion.SETTER +import com.squareup.kotlinpoet.KModifier.Target.PROPERTY +import java.lang.reflect.Type +import java.util.EnumSet +import javax.lang.model.element.Element +import kotlin.reflect.KClass + +/** A generated property declaration. */ +@OptIn(ExperimentalKotlinPoetApi::class) +public class PropertySpec private constructor( + builder: Builder, + private val tagMap: TagMap = builder.buildTagMap(), + private val delegateOriginatingElementsHolder: OriginatingElementsHolder = builder.buildOriginatingElements(), + private val contextReceivers: ContextReceivers = builder.buildContextReceivers(), +) : Taggable by tagMap, OriginatingElementsHolder by delegateOriginatingElementsHolder, ContextReceivable by contextReceivers { + public val mutable: Boolean = builder.mutable + public val name: String = builder.name + public val type: TypeName = builder.type + public val kdoc: CodeBlock = builder.kdoc.build() + public val annotations: List<AnnotationSpec> = builder.annotations.toImmutableList() + public val modifiers: Set<KModifier> = builder.modifiers.toImmutableSet() + public val typeVariables: List<TypeVariableName> = builder.typeVariables.toImmutableList() + public val initializer: CodeBlock? = builder.initializer + public val delegated: Boolean = builder.delegated + public val getter: FunSpec? = builder.getter + public val setter: FunSpec? = builder.setter + public val receiverType: TypeName? = builder.receiverType + + init { + require( + typeVariables.none { it.isReified } || + (getter != null || setter != null) && + (getter == null || KModifier.INLINE in getter.modifiers) && + (setter == null || KModifier.INLINE in setter.modifiers), + ) { + "only type parameters of properties with inline getters and/or setters can be reified!" + } + require(mutable || setter == null) { + "only a mutable property can have a setter" + } + if (contextReceiverTypes.isNotEmpty()) { + requireNotNull(getter) { "properties with context receivers require a $GETTER" } + if (mutable) { + requireNotNull(setter) { "mutable properties with context receivers require a $SETTER" } + } + } + } + + internal fun emit( + codeWriter: CodeWriter, + implicitModifiers: Set<KModifier>, + withInitializer: Boolean = true, + emitKdoc: Boolean = true, + inline: Boolean = false, + inlineAnnotations: Boolean = inline, + ) { + val isInlineProperty = getter?.modifiers?.contains(KModifier.INLINE) ?: false && + (!mutable || setter?.modifiers?.contains(KModifier.INLINE) ?: false) + val propertyModifiers = if (isInlineProperty) modifiers + KModifier.INLINE else modifiers + if (emitKdoc) { + codeWriter.emitKdoc(kdoc.ensureEndsWithNewLine()) + } + codeWriter.emitContextReceivers(contextReceiverTypes, suffix = "\n") + codeWriter.emitAnnotations(annotations, inlineAnnotations) + codeWriter.emitModifiers(propertyModifiers, implicitModifiers) + codeWriter.emitCode(if (mutable) "var·" else "val·") + if (typeVariables.isNotEmpty()) { + codeWriter.emitTypeVariables(typeVariables) + codeWriter.emit(" ") + } + if (receiverType != null) { + if (receiverType is LambdaTypeName) { + codeWriter.emitCode("(%T).", receiverType) + } else { + codeWriter.emitCode("%T.", receiverType) + } + } + codeWriter.emitCode("%N: %T", this, type) + if (withInitializer && initializer != null) { + if (delegated) { + codeWriter.emit(" by ") + } else { + codeWriter.emitCode(" = ") + } + val initializerFormat = if (initializer.hasStatements()) "%L" else "«%L»" + codeWriter.emitCode( + codeBlock = CodeBlock.of(initializerFormat, initializer), + isConstantContext = KModifier.CONST in modifiers, + ) + } + codeWriter.emitWhereBlock(typeVariables) + if (!inline) codeWriter.emit("\n") + val implicitAccessorModifiers = EnumSet.noneOf(KModifier::class.java) + for (modifier in implicitModifiers) { + // Omit visibility modifiers, accessor visibility will default to the property's visibility. + if (modifier !in VISIBILITY_MODIFIERS) { + implicitAccessorModifiers.add(modifier) + } + } + if (isInlineProperty) { + implicitAccessorModifiers.add(KModifier.INLINE) + } + if (getter != null) { + codeWriter.emitCode("⇥") + getter.emit(codeWriter, null, implicitAccessorModifiers, false) + codeWriter.emitCode("⇤") + } + if (setter != null) { + codeWriter.emitCode("⇥") + setter.emit(codeWriter, null, implicitAccessorModifiers, false) + codeWriter.emitCode("⇤") + } + } + + internal fun fromPrimaryConstructorParameter(parameter: ParameterSpec): PropertySpec { + val builder = toBuilder() + .addAnnotations(parameter.annotations) + builder.isPrimaryConstructorParameter = true + builder.modifiers += parameter.modifiers + if (builder.kdoc.isEmpty()) { + builder.addKdoc(parameter.kdoc) + } + return builder.build() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null) return false + if (javaClass != other.javaClass) return false + return toString() == other.toString() + } + + override fun hashCode(): Int = toString().hashCode() + + override fun toString(): String = buildCodeString { emit(this, emptySet()) } + + @JvmOverloads + public fun toBuilder(name: String = this.name, type: TypeName = this.type): Builder { + val builder = Builder(name, type) + builder.mutable = mutable + builder.kdoc.add(kdoc) + builder.annotations += annotations + builder.modifiers += modifiers + builder.typeVariables += typeVariables + builder.initializer = initializer + builder.delegated = delegated + builder.setter = setter + builder.getter = getter + builder.receiverType = receiverType + builder.tags += tagMap.tags + builder.originatingElements += originatingElements + builder.contextReceiverTypes += contextReceiverTypes + return builder + } + + public class Builder internal constructor( + internal val name: String, + internal val type: TypeName, + ) : Taggable.Builder<Builder>, + OriginatingElementsHolder.Builder<Builder>, + ContextReceivable.Builder<Builder> { + internal var isPrimaryConstructorParameter = false + internal var mutable = false + internal val kdoc = CodeBlock.builder() + internal var initializer: CodeBlock? = null + internal var delegated = false + internal var getter: FunSpec? = null + internal var setter: FunSpec? = null + internal var receiverType: TypeName? = null + + public val annotations: MutableList<AnnotationSpec> = mutableListOf() + public val modifiers: MutableList<KModifier> = mutableListOf() + public val typeVariables: MutableList<TypeVariableName> = mutableListOf() + override val tags: MutableMap<KClass<*>, Any> = mutableMapOf() + override val originatingElements: MutableList<Element> = mutableListOf() + override val contextReceiverTypes: MutableList<TypeName> = mutableListOf() + + /** True to create a `var` instead of a `val`. */ + public fun mutable(mutable: Boolean = true): Builder = apply { + this.mutable = mutable + } + + public fun addKdoc(format: String, vararg args: Any): Builder = apply { + kdoc.add(format, *args) + } + + public fun addKdoc(block: CodeBlock): Builder = apply { + kdoc.add(block) + } + + public fun addAnnotations(annotationSpecs: Iterable<AnnotationSpec>): Builder = apply { + annotations += annotationSpecs + } + + public fun addAnnotation(annotationSpec: AnnotationSpec): Builder = apply { + annotations += annotationSpec + } + + public fun addAnnotation(annotation: ClassName): Builder = apply { + annotations += AnnotationSpec.builder(annotation).build() + } + + @DelicateKotlinPoetApi( + message = "Java reflection APIs don't give complete information on Kotlin types. Consider " + + "using the kotlinpoet-metadata APIs instead.", + ) + public fun addAnnotation(annotation: Class<*>): Builder = + addAnnotation(annotation.asClassName()) + + public fun addAnnotation(annotation: KClass<*>): Builder = + addAnnotation(annotation.asClassName()) + + public fun addModifiers(vararg modifiers: KModifier): Builder = apply { + this.modifiers += modifiers + } + + public fun addModifiers(modifiers: Iterable<KModifier>): Builder = apply { + this.modifiers += modifiers + } + + public fun addTypeVariables(typeVariables: Iterable<TypeVariableName>): Builder = apply { + this.typeVariables += typeVariables + } + + public fun addTypeVariable(typeVariable: TypeVariableName): Builder = apply { + typeVariables += typeVariable + } + + public fun initializer(format: String, vararg args: Any?): Builder = + initializer(CodeBlock.of(format, *args)) + + public fun initializer(codeBlock: CodeBlock?): Builder = apply { + this.initializer = codeBlock + this.delegated = false + } + + public fun delegate(format: String, vararg args: Any?): Builder = + delegate(CodeBlock.of(format, *args)) + + public fun delegate(codeBlock: CodeBlock): Builder = apply { + this.initializer = codeBlock + this.delegated = true + } + + public fun getter(getter: FunSpec?): Builder = apply { + require(getter == null || getter.name == GETTER) { "${getter!!.name} is not a getter" } + this.getter = getter + } + + public fun setter(setter: FunSpec?): Builder = apply { + require(setter == null || setter.name == SETTER) { "${setter!!.name} is not a setter" } + this.setter = setter + } + + public fun receiver(receiverType: TypeName?): Builder = apply { + this.receiverType = receiverType + } + + @DelicateKotlinPoetApi( + message = "Java reflection APIs don't give complete information on Kotlin types. Consider " + + "using the kotlinpoet-metadata APIs instead.", + ) + public fun receiver(receiverType: Type): Builder = receiver(receiverType.asTypeName()) + + public fun receiver(receiverType: KClass<*>): Builder = receiver(receiverType.asTypeName()) + + public fun build(): PropertySpec { + if (KModifier.INLINE in modifiers) { + throw IllegalArgumentException( + "KotlinPoet doesn't allow setting the inline modifier on " + + "properties. You should mark either the getter, the setter, or both inline.", + ) + } + for (it in modifiers) { + if (!isPrimaryConstructorParameter) it.checkTarget(PROPERTY) + } + return PropertySpec(this) + } + } + + public companion object { + @JvmStatic public fun builder( + name: String, + type: TypeName, + vararg modifiers: KModifier, + ): Builder { + return Builder(name, type).addModifiers(*modifiers) + } + + @JvmStatic public fun builder(name: String, type: Type, vararg modifiers: KModifier): Builder = + builder(name, type.asTypeName(), *modifiers) + + @JvmStatic public fun builder( + name: String, + type: KClass<*>, + vararg modifiers: KModifier, + ): Builder = builder(name, type.asTypeName(), *modifiers) + + @JvmStatic public fun builder( + name: String, + type: TypeName, + modifiers: Iterable<KModifier>, + ): Builder { + return Builder(name, type).addModifiers(modifiers) + } + + @DelicateKotlinPoetApi( + message = "Java reflection APIs don't give complete information on Kotlin types. Consider " + + "using the kotlinpoet-metadata APIs instead.", + ) + @JvmStatic + public fun builder( + name: String, + type: Type, + modifiers: Iterable<KModifier>, + ): Builder = builder(name, type.asTypeName(), modifiers) + + @JvmStatic public fun builder( + name: String, + type: KClass<*>, + modifiers: Iterable<KModifier>, + ): Builder = builder(name, type.asTypeName(), modifiers) + } +} diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/Taggable.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/Taggable.kt new file mode 100644 index 00000000..74ff690a --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/Taggable.kt @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import kotlin.reflect.KClass + +/** A type that can be tagged with extra metadata of the user's choice. */ +public interface Taggable { + + /** Returns all tags. */ + public val tags: Map<KClass<*>, Any> get() = emptyMap() + + /** Returns the tag attached with [type] as a key, or null if no tag is attached with that key. */ + public fun <T : Any> tag(type: Class<T>): T? = tag(type.kotlin) + + /** Returns the tag attached with [type] as a key, or null if no tag is attached with that key. */ + public fun <T : Any> tag(type: KClass<T>): T? { + @Suppress("UNCHECKED_CAST") + return tags[type] as T? + } + + /** The builder analogue to [Taggable] types. */ + public interface Builder<out T : Builder<T>> { + + /** Mutable map of the current tags this builder contains. */ + public val tags: MutableMap<KClass<*>, Any> + + /** + * Attaches [tag] to the request using [type] as a key. Tags can be read from a + * request using [Taggable.tag]. Use `null` to remove any existing tag assigned for + * [type]. + * + * Use this API to attach originating elements, debugging, or other application data to a spec + * so that you may read it in other APIs or callbacks. + */ + public fun tag(type: Class<*>, tag: Any?): T = tag(type.kotlin, tag) + + /** + * Attaches [tag] to the request using [type] as a key. Tags can be read from a + * request using [Taggable.tag]. Use `null` to remove any existing tag assigned for + * [type]. + * + * Use this API to attach originating elements, debugging, or other application data to a spec + * so that you may read it in other APIs or callbacks. + */ + @Suppress("UNCHECKED_CAST") + public fun tag(type: KClass<*>, tag: Any?): T = apply { + if (tag == null) { + this.tags.remove(type) + } else { + this.tags[type] = tag + } + } as T + } +} + +/** Returns the tag attached with [T] as a key, or null if no tag is attached with that key. */ +public inline fun <reified T : Any> Taggable.tag(): T? = tag(T::class) + +/** + * Attaches [tag] to the request using [T] as a key. Tags can be read from a + * request using [Taggable.tag]. Use `null` to remove any existing tag assigned for + * [T]. + * + * Use this API to attach debugging or other application data to a spec so that you may read it in + * other APIs or callbacks. + */ + +public inline fun <reified T : Any> AnnotationSpec.Builder.tag(tag: T?): AnnotationSpec.Builder = + tag(T::class, tag) + +/** + * Attaches [tag] to the request using [T] as a key. Tags can be read from a + * request using [Taggable.tag]. Use `null` to remove any existing tag assigned for + * [T]. + * + * Use this API to attach debugging or other application data to a spec so that you may read it in + * other APIs or callbacks. + */ +public inline fun <reified T : Any> FileSpec.Builder.tag(tag: T?): FileSpec.Builder = + tag(T::class, tag) + +/** + * Attaches [tag] to the request using [T] as a key. Tags can be read from a + * request using [Taggable.tag]. Use `null` to remove any existing tag assigned for + * [T]. + * + * Use this API to attach debugging or other application data to a spec so that you may read it in + * other APIs or callbacks. + */ +public inline fun <reified T : Any> FunSpec.Builder.tag(tag: T?): FunSpec.Builder = + tag(T::class, tag) + +/** + * Attaches [tag] to the request using [T] as a key. Tags can be read from a + * request using [Taggable.tag]. Use `null` to remove any existing tag assigned for + * [T]. + * + * Use this API to attach debugging or other application data to a spec so that you may read it in + * other APIs or callbacks. + */ +public inline fun <reified T : Any> ParameterSpec.Builder.tag(tag: T?): ParameterSpec.Builder = + tag(T::class, tag) + +/** + * Attaches [tag] to the request using [T] as a key. Tags can be read from a + * request using [Taggable.tag]. Use `null` to remove any existing tag assigned for + * [T]. + * + * Use this API to attach debugging or other application data to a spec so that you may read it in + * other APIs or callbacks. + */ +public inline fun <reified T : Any> PropertySpec.Builder.tag(tag: T?): PropertySpec.Builder = + tag(T::class, tag) + +/** + * Attaches [tag] to the request using [T] as a key. Tags can be read from a + * request using [Taggable.tag]. Use `null` to remove any existing tag assigned for + * [T]. + * + * Use this API to attach debugging or other application data to a spec so that you may read it in + * other APIs or callbacks. + */ +public inline fun <reified T : Any> TypeAliasSpec.Builder.tag(tag: T?): TypeAliasSpec.Builder = + tag(T::class, tag) + +/** + * Attaches [tag] to the request using [T] as a key. Tags can be read from a + * request using [Taggable.tag]. Use `null` to remove any existing tag assigned for + * [T]. + * + * Use this API to attach debugging or other application data to a spec so that you may read it in + * other APIs or callbacks. + */ +public inline fun <reified T : Any> TypeSpec.Builder.tag(tag: T?): TypeSpec.Builder = + tag(T::class, tag) + +internal fun Taggable.Builder<*>.buildTagMap(): TagMap = TagMap(tags) + +@JvmInline +internal value class TagMap private constructor(override val tags: Map<KClass<*>, Any>) : Taggable { + companion object { + operator fun invoke(tags: Map<KClass<*>, Any>): TagMap = TagMap(tags.toImmutableMap()) + } +} diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/TypeAliasSpec.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/TypeAliasSpec.kt new file mode 100644 index 00000000..a0a90e2e --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/TypeAliasSpec.kt @@ -0,0 +1,158 @@ +/* + * Copyright (C) 2017 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.squareup.kotlinpoet.KModifier.ACTUAL +import com.squareup.kotlinpoet.KModifier.INTERNAL +import com.squareup.kotlinpoet.KModifier.PRIVATE +import com.squareup.kotlinpoet.KModifier.PUBLIC +import java.lang.reflect.Type +import kotlin.reflect.KClass + +/** A generated typealias declaration */ +public class TypeAliasSpec private constructor( + builder: Builder, + private val tagMap: TagMap = builder.buildTagMap(), +) : Taggable by tagMap { + public val name: String = builder.name + public val type: TypeName = builder.type + public val modifiers: Set<KModifier> = builder.modifiers.toImmutableSet() + public val typeVariables: List<TypeVariableName> = builder.typeVariables.toImmutableList() + public val kdoc: CodeBlock = builder.kdoc.build() + public val annotations: List<AnnotationSpec> = builder.annotations.toImmutableList() + + internal fun emit(codeWriter: CodeWriter) { + codeWriter.emitKdoc(kdoc.ensureEndsWithNewLine()) + codeWriter.emitAnnotations(annotations, false) + codeWriter.emitModifiers(modifiers, setOf(PUBLIC)) + codeWriter.emitCode("typealias %N", name) + codeWriter.emitTypeVariables(typeVariables) + codeWriter.emitCode(" = %T", type) + codeWriter.emit("\n") + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null) return false + if (javaClass != other.javaClass) return false + return toString() == other.toString() + } + + override fun hashCode(): Int = toString().hashCode() + + override fun toString(): String = buildCodeString { emit(this) } + + @JvmOverloads + public fun toBuilder(name: String = this.name, type: TypeName = this.type): Builder { + val builder = Builder(name, type) + builder.modifiers += modifiers + builder.typeVariables += typeVariables + builder.kdoc.add(kdoc) + builder.annotations += annotations + builder.tags += tagMap.tags + return builder + } + + public class Builder internal constructor( + internal val name: String, + internal val type: TypeName, + ) : Taggable.Builder<Builder> { + internal val kdoc = CodeBlock.builder() + + public val modifiers: MutableSet<KModifier> = mutableSetOf() + public val typeVariables: MutableSet<TypeVariableName> = mutableSetOf() + public val annotations: MutableList<AnnotationSpec> = mutableListOf() + override val tags: MutableMap<KClass<*>, Any> = mutableMapOf() + + public fun addModifiers(vararg modifiers: KModifier): Builder = apply { + modifiers.forEach(this::addModifier) + } + + public fun addModifiers(modifiers: Iterable<KModifier>): Builder = apply { + modifiers.forEach(this::addModifier) + } + + private fun addModifier(modifier: KModifier) { + this.modifiers.add(modifier) + } + + public fun addTypeVariables(typeVariables: Iterable<TypeVariableName>): Builder = apply { + this.typeVariables += typeVariables + } + + public fun addTypeVariable(typeVariable: TypeVariableName): Builder = apply { + typeVariables += typeVariable + } + + public fun addKdoc(format: String, vararg args: Any): Builder = apply { + kdoc.add(format, *args) + } + + public fun addKdoc(block: CodeBlock): Builder = apply { + kdoc.add(block) + } + + public fun addAnnotations(annotationSpecs: Iterable<AnnotationSpec>): Builder = apply { + this.annotations += annotationSpecs + } + + public fun addAnnotation(annotationSpec: AnnotationSpec): Builder = apply { + annotations += annotationSpec + } + + public fun addAnnotation(annotation: ClassName): Builder = apply { + annotations += AnnotationSpec.builder(annotation).build() + } + + @DelicateKotlinPoetApi( + message = "Java reflection APIs don't give complete information on Kotlin types. Consider " + + "using the kotlinpoet-metadata APIs instead.", + ) + public fun addAnnotation(annotation: Class<*>): Builder = + addAnnotation(annotation.asClassName()) + + public fun addAnnotation(annotation: KClass<*>): Builder = + addAnnotation(annotation.asClassName()) + + public fun build(): TypeAliasSpec { + for (it in modifiers) { + require(it in ALLOWABLE_MODIFIERS) { + "unexpected typealias modifier $it" + } + } + return TypeAliasSpec(this) + } + + private companion object { + private val ALLOWABLE_MODIFIERS = setOf(PUBLIC, INTERNAL, PRIVATE, ACTUAL) + } + } + + public companion object { + @JvmStatic public fun builder(name: String, type: TypeName): Builder = Builder(name, type) + + @DelicateKotlinPoetApi( + message = "Java reflection APIs don't give complete information on Kotlin types. Consider " + + "using the kotlinpoet-metadata APIs instead.", + ) + @JvmStatic + public fun builder(name: String, type: Type): Builder = + builder(name, type.asTypeName()) + + @JvmStatic public fun builder(name: String, type: KClass<*>): Builder = + builder(name, type.asTypeName()) + } +} diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/TypeName.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/TypeName.kt new file mode 100644 index 00000000..bffcf4f1 --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/TypeName.kt @@ -0,0 +1,347 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:JvmName("TypeNames") + +package com.squareup.kotlinpoet + +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import java.lang.reflect.GenericArrayType +import java.lang.reflect.ParameterizedType +import java.lang.reflect.Type +import java.lang.reflect.TypeVariable +import java.lang.reflect.WildcardType +import javax.lang.model.element.Modifier +import javax.lang.model.element.TypeElement +import javax.lang.model.element.TypeParameterElement +import javax.lang.model.type.ArrayType +import javax.lang.model.type.DeclaredType +import javax.lang.model.type.ErrorType +import javax.lang.model.type.NoType +import javax.lang.model.type.PrimitiveType +import javax.lang.model.type.TypeKind +import javax.lang.model.type.TypeMirror +import javax.lang.model.util.SimpleTypeVisitor7 +import kotlin.reflect.KClass +import kotlin.reflect.typeOf + +/** + * Any type in Kotlin's type system. This class identifies simple types like `Int` and `String`, + * nullable types like `Int?`, composite types like `Array<String>` and `Set<String>`, and + * unassignable types like `Unit`. + * + * Type names are dumb identifiers only and do not model the values they name. For example, the + * type name for `kotlin.List` doesn't know about the `size()` function, the fact that lists are + * collections, or even that it accepts a single type parameter. + * + * Instances of this class are immutable value objects that implement `equals()` and `hashCode()` + * properly. + * + * Referencing existing types + * -------------------------- + * + * In an annotation processor you can get a type name instance for a type mirror by calling + * [asTypeName]. In reflection code, you can use [asTypeName]. + + * Defining new types + * ------------------ + * + * Create new reference types like `com.example.HelloWorld` with [ClassName.bestGuess]. To build composite + * types like `Set<Long>`, use the factory methods on [ParameterizedTypeName], [TypeVariableName], + * and [WildcardTypeName]. + */ +public sealed class TypeName constructor( + public val isNullable: Boolean, + annotations: List<AnnotationSpec>, + internal val tagMap: TagMap, +) : Taggable by tagMap { + public val annotations: List<AnnotationSpec> = annotations.toImmutableList() + + /** Lazily-initialized toString of this type name. */ + private val cachedString: String by lazy { + buildCodeString { + emitAnnotations(this) + emit(this) + if (isNullable) emit("?") + } + } + + public fun copy( + nullable: Boolean = this.isNullable, + annotations: List<AnnotationSpec> = this.annotations.toList(), + ): TypeName { + return copy(nullable, annotations, this.tags) + } + + public abstract fun copy( + nullable: Boolean = this.isNullable, + annotations: List<AnnotationSpec> = this.annotations.toList(), + tags: Map<KClass<*>, Any> = this.tags, + ): TypeName + + public val isAnnotated: Boolean get() = annotations.isNotEmpty() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null) return false + if (javaClass != other.javaClass) return false + return toString() == other.toString() + } + + override fun hashCode(): Int = toString().hashCode() + + override fun toString(): String = cachedString + + internal abstract fun emit(out: CodeWriter): CodeWriter + + internal fun emitAnnotations(out: CodeWriter) { + for (annotation in annotations) { + annotation.emit(out, true) + out.emit(" ") + } + } + + internal fun emitNullable(out: CodeWriter) { + if (isNullable) { + out.emit("?") + } + } + + public companion object { + internal fun get( + mirror: TypeMirror, + typeVariables: Map<TypeParameterElement, TypeVariableName>, + ): TypeName { + return mirror.accept( + object : SimpleTypeVisitor7<TypeName, Void?>() { + override fun visitPrimitive(t: PrimitiveType, p: Void?): TypeName { + return when (t.kind) { + TypeKind.BOOLEAN -> BOOLEAN + TypeKind.BYTE -> BYTE + TypeKind.SHORT -> SHORT + TypeKind.INT -> INT + TypeKind.LONG -> LONG + TypeKind.CHAR -> CHAR + TypeKind.FLOAT -> FLOAT + TypeKind.DOUBLE -> DOUBLE + else -> throw AssertionError() + } + } + + override fun visitDeclared(t: DeclaredType, p: Void?): TypeName { + val rawType: ClassName = (t.asElement() as TypeElement).asClassName() + val enclosingType = t.enclosingType + val enclosing = if (enclosingType.kind != TypeKind.NONE && + Modifier.STATIC !in t.asElement().modifiers + ) { + enclosingType.accept(this, null) + } else { + null + } + if (t.typeArguments.isEmpty() && enclosing !is ParameterizedTypeName) { + return rawType + } + + val typeArgumentNames = mutableListOf<TypeName>() + for (typeArgument in t.typeArguments) { + typeArgumentNames += get(typeArgument, typeVariables) + } + return if (enclosing is ParameterizedTypeName) { + enclosing.nestedClass(rawType.simpleName, typeArgumentNames) + } else { + ParameterizedTypeName(null, rawType, typeArgumentNames) + } + } + + override fun visitError(t: ErrorType, p: Void?): TypeName { + return visitDeclared(t, p) + } + + override fun visitArray(t: ArrayType, p: Void?): ParameterizedTypeName { + return ARRAY.parameterizedBy(get(t.componentType, typeVariables)) + } + + override fun visitTypeVariable( + t: javax.lang.model.type.TypeVariable, + p: Void?, + ): TypeName { + return TypeVariableName.get(t, typeVariables.toMutableMap()) + } + + override fun visitWildcard(t: javax.lang.model.type.WildcardType, p: Void?): TypeName { + return WildcardTypeName.get(t, typeVariables) + } + + override fun visitNoType(t: NoType, p: Void?): TypeName { + if (t.kind == TypeKind.VOID) return UNIT + return super.visitUnknown(t, p) + } + + override fun defaultAction(e: TypeMirror?, p: Void?): TypeName { + throw IllegalArgumentException("Unexpected type mirror: " + e!!) + } + }, + null, + ) + } + + internal fun get(type: Type, map: MutableMap<Type, TypeVariableName>): TypeName { + return when (type) { + is Class<*> -> when { + type === Void.TYPE -> UNIT + type === Boolean::class.javaPrimitiveType -> BOOLEAN + type === Byte::class.javaPrimitiveType -> BYTE + type === Short::class.javaPrimitiveType -> SHORT + type === Int::class.javaPrimitiveType -> INT + type === Long::class.javaPrimitiveType -> LONG + type === Char::class.javaPrimitiveType -> CHAR + type === Float::class.javaPrimitiveType -> FLOAT + type === Double::class.javaPrimitiveType -> DOUBLE + type.isArray -> ARRAY.parameterizedBy(get(type.componentType, map)) + else -> type.asClassName() + } + is ParameterizedType -> ParameterizedTypeName.get(type, map) + is WildcardType -> WildcardTypeName.get(type, map) + is TypeVariable<*> -> TypeVariableName.get(type, map) + is GenericArrayType -> ARRAY.parameterizedBy(get(type.genericComponentType, map)) + else -> throw IllegalArgumentException("unexpected type: $type") + } + } + } +} + +@JvmField public val ANY: ClassName = ClassName("kotlin", "Any") + +@JvmField public val ARRAY: ClassName = ClassName("kotlin", "Array") + +@JvmField public val UNIT: ClassName = ClassName("kotlin", "Unit") + +@JvmField public val BOOLEAN: ClassName = ClassName("kotlin", "Boolean") + +@JvmField public val BYTE: ClassName = ClassName("kotlin", "Byte") + +@JvmField public val SHORT: ClassName = ClassName("kotlin", "Short") + +@JvmField public val INT: ClassName = ClassName("kotlin", "Int") + +@JvmField public val LONG: ClassName = ClassName("kotlin", "Long") + +@JvmField public val CHAR: ClassName = ClassName("kotlin", "Char") + +@JvmField public val FLOAT: ClassName = ClassName("kotlin", "Float") + +@JvmField public val DOUBLE: ClassName = ClassName("kotlin", "Double") + +@JvmField public val STRING: ClassName = ClassName("kotlin", "String") + +@JvmField public val CHAR_SEQUENCE: ClassName = ClassName("kotlin", "CharSequence") + +@JvmField public val COMPARABLE: ClassName = ClassName("kotlin", "Comparable") + +@JvmField public val THROWABLE: ClassName = ClassName("kotlin", "Throwable") + +@JvmField public val ANNOTATION: ClassName = ClassName("kotlin", "Annotation") + +@JvmField public val NOTHING: ClassName = ClassName("kotlin", "Nothing") + +@JvmField public val NUMBER: ClassName = ClassName("kotlin", "Number") + +@JvmField public val ITERABLE: ClassName = ClassName("kotlin.collections", "Iterable") + +@JvmField public val COLLECTION: ClassName = ClassName("kotlin.collections", "Collection") + +@JvmField public val LIST: ClassName = ClassName("kotlin.collections", "List") + +@JvmField public val SET: ClassName = ClassName("kotlin.collections", "Set") + +@JvmField public val MAP: ClassName = ClassName("kotlin.collections", "Map") + +@JvmField public val MAP_ENTRY: ClassName = MAP.nestedClass("Entry") + +@JvmField public val MUTABLE_ITERABLE: ClassName = + ClassName("kotlin.collections", "MutableIterable") + +@JvmField public val MUTABLE_COLLECTION: ClassName = + ClassName("kotlin.collections", "MutableCollection") + +@JvmField public val MUTABLE_LIST: ClassName = ClassName("kotlin.collections", "MutableList") + +@JvmField public val MUTABLE_SET: ClassName = ClassName("kotlin.collections", "MutableSet") + +@JvmField public val MUTABLE_MAP: ClassName = ClassName("kotlin.collections", "MutableMap") + +@JvmField public val MUTABLE_MAP_ENTRY: ClassName = MUTABLE_MAP.nestedClass("Entry") + +@JvmField public val BOOLEAN_ARRAY: ClassName = ClassName("kotlin", "BooleanArray") + +@JvmField public val BYTE_ARRAY: ClassName = ClassName("kotlin", "ByteArray") + +@JvmField public val CHAR_ARRAY: ClassName = ClassName("kotlin", "CharArray") + +@JvmField public val SHORT_ARRAY: ClassName = ClassName("kotlin", "ShortArray") + +@JvmField public val INT_ARRAY: ClassName = ClassName("kotlin", "IntArray") + +@JvmField public val LONG_ARRAY: ClassName = ClassName("kotlin", "LongArray") + +@JvmField public val FLOAT_ARRAY: ClassName = ClassName("kotlin", "FloatArray") + +@JvmField public val DOUBLE_ARRAY: ClassName = ClassName("kotlin", "DoubleArray") + +@JvmField public val ENUM: ClassName = ClassName("kotlin", "Enum") + +@JvmField public val U_BYTE: ClassName = ClassName("kotlin", "UByte") + +@JvmField public val U_SHORT: ClassName = ClassName("kotlin", "UShort") + +@JvmField public val U_INT: ClassName = ClassName("kotlin", "UInt") + +@JvmField public val U_LONG: ClassName = ClassName("kotlin", "ULong") + +@JvmField public val U_BYTE_ARRAY: ClassName = ClassName("kotlin", "UByteArray") + +@JvmField public val U_SHORT_ARRAY: ClassName = ClassName("kotlin", "UShortArray") + +@JvmField public val U_INT_ARRAY: ClassName = ClassName("kotlin", "UIntArray") + +@JvmField public val U_LONG_ARRAY: ClassName = ClassName("kotlin", "ULongArray") + +/** The wildcard type `*` which is shorthand for `out Any?`. */ +@JvmField public val STAR: WildcardTypeName = WildcardTypeName.producerOf(ANY.copy(nullable = true)) + +/** [Dynamic] is a singleton `object` type, so this is a shorthand for it in Java. */ +@JvmField public val DYNAMIC: Dynamic = Dynamic + +/** Returns a [TypeName] equivalent to this [TypeMirror]. */ +@DelicateKotlinPoetApi( + message = "Mirror APIs don't give complete information on Kotlin types. Consider using" + + " the kotlinpoet-metadata APIs instead.", +) +@JvmName("get") +public fun TypeMirror.asTypeName(): TypeName = TypeName.get(this, mutableMapOf()) + +/** Returns a [TypeName] equivalent to this [KClass]. */ +@JvmName("get") +public fun KClass<*>.asTypeName(): ClassName = asClassName() + +/** Returns a [TypeName] equivalent to this [Type]. */ +@JvmName("get") +public fun Type.asTypeName(): TypeName = TypeName.get(this, mutableMapOf()) + +/** + * Returns a [TypeName] equivalent of the reified type parameter [T] using reflection, maybe using kotlin-reflect + * if required. + */ +public inline fun <reified T> typeNameOf(): TypeName = typeOf<T>().asTypeName() diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/TypeSpec.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/TypeSpec.kt new file mode 100644 index 00000000..c8abca97 --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/TypeSpec.kt @@ -0,0 +1,901 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.squareup.kotlinpoet.KModifier.ABSTRACT +import com.squareup.kotlinpoet.KModifier.ANNOTATION +import com.squareup.kotlinpoet.KModifier.COMPANION +import com.squareup.kotlinpoet.KModifier.ENUM +import com.squareup.kotlinpoet.KModifier.EXPECT +import com.squareup.kotlinpoet.KModifier.EXTERNAL +import com.squareup.kotlinpoet.KModifier.FUN +import com.squareup.kotlinpoet.KModifier.INLINE +import com.squareup.kotlinpoet.KModifier.INTERNAL +import com.squareup.kotlinpoet.KModifier.PRIVATE +import com.squareup.kotlinpoet.KModifier.PROTECTED +import com.squareup.kotlinpoet.KModifier.PUBLIC +import com.squareup.kotlinpoet.KModifier.SEALED +import com.squareup.kotlinpoet.KModifier.VALUE +import java.lang.reflect.Type +import javax.lang.model.element.Element +import kotlin.reflect.KClass + +/** A generated class, interface, or enum declaration. */ +@OptIn(ExperimentalKotlinPoetApi::class) +public class TypeSpec private constructor( + builder: Builder, + private val tagMap: TagMap = builder.buildTagMap(), + private val delegateOriginatingElements: OriginatingElementsHolder = builder.originatingElements + .plus(builder.typeSpecs.flatMap(TypeSpec::originatingElements)) + .buildOriginatingElements(), + private val contextReceivers: ContextReceivers = builder.buildContextReceivers(), +) : Taggable by tagMap, OriginatingElementsHolder by delegateOriginatingElements, ContextReceivable by contextReceivers { + public val kind: Kind = builder.kind + public val name: String? = builder.name + public val kdoc: CodeBlock = builder.kdoc.build() + public val annotationSpecs: List<AnnotationSpec> = builder.annotationSpecs.toImmutableList() + public val modifiers: Set<KModifier> = builder.modifiers.toImmutableSet() + public val typeVariables: List<TypeVariableName> = builder.typeVariables.toImmutableList() + public val primaryConstructor: FunSpec? = builder.primaryConstructor + public val superclass: TypeName = builder.superclass + public val superclassConstructorParameters: List<CodeBlock> = + builder.superclassConstructorParameters.toImmutableList() + + public val isEnum: Boolean = builder.isEnum + public val isAnnotation: Boolean = builder.isAnnotation + public val isCompanion: Boolean = builder.isCompanion + public val isAnonymousClass: Boolean = builder.isAnonymousClass + public val isFunctionalInterface: Boolean = builder.isFunInterface + + /** + * Map of superinterfaces - entries with a null value represent a regular superinterface (with + * no delegation), while non-null [CodeBlock] values represent delegates + * for the corresponding [TypeSpec] interface (key) value + */ + public val superinterfaces: Map<TypeName, CodeBlock?> = builder.superinterfaces.toImmutableMap() + public val enumConstants: Map<String, TypeSpec> = builder.enumConstants.toImmutableMap() + public val propertySpecs: List<PropertySpec> = builder.propertySpecs.toImmutableList() + public val initializerBlock: CodeBlock = builder.initializerBlock.build() + public val initializerIndex: Int = builder.initializerIndex + public val funSpecs: List<FunSpec> = builder.funSpecs.toImmutableList() + public val typeSpecs: List<TypeSpec> = builder.typeSpecs.toImmutableList() + internal val nestedTypesSimpleNames = typeSpecs.map { it.name }.toImmutableSet() + + @JvmOverloads + public fun toBuilder(kind: Kind = this.kind, name: String? = this.name): Builder { + val builder = Builder(kind, name) + builder.modifiers += modifiers + builder.kdoc.add(kdoc) + builder.annotationSpecs += annotationSpecs + builder.typeVariables += typeVariables + builder.superclass = superclass + builder.superclassConstructorParameters += superclassConstructorParameters + builder.enumConstants += enumConstants + builder.propertySpecs += propertySpecs + builder.funSpecs += funSpecs + builder.typeSpecs += typeSpecs + builder.initializerBlock.add(initializerBlock) + builder.initializerIndex = initializerIndex + builder.superinterfaces.putAll(superinterfaces) + builder.primaryConstructor = primaryConstructor + builder.tags += tagMap.tags + builder.originatingElements += originatingElements + builder.contextReceiverTypes += contextReceiverTypes + return builder + } + + internal fun emit( + codeWriter: CodeWriter, + enumName: String?, + implicitModifiers: Set<KModifier> = emptySet(), + isNestedExternal: Boolean = false, + ) { + // Types. + val areNestedExternal = EXTERNAL in modifiers || isNestedExternal + + // Nested classes interrupt wrapped line indentation. Stash the current wrapping state and put + // it back afterwards when this type is complete. + val previousStatementLine = codeWriter.statementLine + codeWriter.statementLine = -1 + + val constructorProperties: Map<String, PropertySpec> = constructorProperties() + val superclassConstructorParametersBlock = superclassConstructorParameters.joinToCode() + + try { + if (enumName != null) { + codeWriter.emitKdoc(kdocWithConstructorParameters()) + codeWriter.emitAnnotations(annotationSpecs, false) + codeWriter.emitCode("%N", enumName) + if (superclassConstructorParametersBlock.isNotEmpty()) { + codeWriter.emit("(") + codeWriter.emitCode(superclassConstructorParametersBlock) + codeWriter.emit(")") + } + if (hasNoBody) { + return // Avoid unnecessary braces "{}". + } + codeWriter.emit(" {\n") + } else if (isAnonymousClass) { + codeWriter.emitCode("object") + val supertype = if (superclass != ANY) { + if (!areNestedExternal && !modifiers.contains(EXPECT)) { + listOf(CodeBlock.of(" %T(%L)", superclass, superclassConstructorParametersBlock)) + } else { + listOf(CodeBlock.of(" %T", superclass)) + } + } else { + listOf() + } + + val allSuperTypes = supertype + if (superinterfaces.isNotEmpty()) { + superinterfaces.keys.map { CodeBlock.of(" %T", it) } + } else { + emptyList() + } + + if (allSuperTypes.isNotEmpty()) { + codeWriter.emitCode(" :") + codeWriter.emitCode(allSuperTypes.joinToCode(",")) + } + if (hasNoBody) { + codeWriter.emit(" {\n}") + return + } + codeWriter.emit(" {\n") + } else { + codeWriter.emitKdoc(kdocWithConstructorParameters()) + codeWriter.emitContextReceivers(contextReceiverTypes, suffix = "\n") + codeWriter.emitAnnotations(annotationSpecs, false) + codeWriter.emitModifiers( + modifiers, + if (isNestedExternal) setOf(PUBLIC, EXTERNAL) else setOf(PUBLIC), + ) + codeWriter.emit(kind.declarationKeyword) + if (name != null) { + codeWriter.emitCode(" %N", this) + } + codeWriter.emitTypeVariables(typeVariables) + + primaryConstructor?.let { + codeWriter.pushType(this) // avoid name collisions when emitting primary constructor + val emittedAnnotations = it.annotations.isNotEmpty() + val useKeyword = it.annotations.isNotEmpty() || it.modifiers.isNotEmpty() + + if (it.annotations.isNotEmpty()) { + codeWriter.emit(" ") + codeWriter.emitAnnotations(it.annotations, true) + } + + if (it.modifiers.isNotEmpty()) { + if (!emittedAnnotations) codeWriter.emit(" ") + codeWriter.emitModifiers(it.modifiers) + } + + if (useKeyword) { + codeWriter.emit("constructor") + } + + it.parameters.emit(codeWriter, forceNewLines = true) { param -> + val property = constructorProperties[param.name] + if (property != null) { + property.emit( + codeWriter, + setOf(PUBLIC), + withInitializer = false, + inline = true, + inlineAnnotations = false, + ) + param.emitDefaultValue(codeWriter) + } else { + param.emit(codeWriter, emitKdoc = true, inlineAnnotations = false) + } + } + + codeWriter.popType() + } + + val types = listOf(superclass).filter { it != ANY }.map { + if (primaryConstructor != null || funSpecs.none(FunSpec::isConstructor)) { + if (!areNestedExternal && !modifiers.contains(EXPECT)) { + CodeBlock.of("%T(%L)", it, superclassConstructorParametersBlock) + } else { + CodeBlock.of("%T", it) + } + } else { + CodeBlock.of("%T", it) + } + } + val superTypes = types + superinterfaces.entries.map { (type, init) -> + if (init == null) CodeBlock.of("%T", type) else CodeBlock.of("%T by %L", type, init) + } + + if (superTypes.isNotEmpty()) { + codeWriter.emitCode(superTypes.joinToCode(separator = ", ", prefix = " : ")) + } + + codeWriter.emitWhereBlock(typeVariables) + + if (hasNoBody) { + codeWriter.emit("\n") + return // Avoid unnecessary braces "{}". + } + codeWriter.emit(" {\n") + } + + codeWriter.pushType(this) + codeWriter.indent() + var firstMember = true + for ((key, value) in enumConstants.entries) { + if (!firstMember) codeWriter.emit("\n") + value.emit(codeWriter, key) + codeWriter.emit(",") + firstMember = false + } + if (isEnum) { + if (!firstMember) { + codeWriter.emit("\n") + } + if (propertySpecs.isNotEmpty() || funSpecs.isNotEmpty() || typeSpecs.isNotEmpty()) { + codeWriter.emit(";\n") + } + } + + val cachedHasInitializer = hasInitializer + var initializerEmitted = false + fun possiblyEmitInitializer() { + if (initializerEmitted) return + initializerEmitted = true + if (cachedHasInitializer) { + if (!firstMember) codeWriter.emit("\n") + codeWriter.emitCode(initializerBlock) + firstMember = false + } + } + + // Properties and initializer block. + for ((index, propertySpec) in propertySpecs.withIndex()) { + // Initializer block. + if (index == initializerIndex) { + possiblyEmitInitializer() + } + if (constructorProperties.containsKey(propertySpec.name)) { + continue + } + if (!firstMember) codeWriter.emit("\n") + propertySpec.emit(codeWriter, kind.implicitPropertyModifiers(modifiers)) + firstMember = false + } + + // One last try in case the initializer index is after all properties + possiblyEmitInitializer() + + if (primaryConstructor != null && primaryConstructor.body.isNotEmpty()) { + codeWriter.emit("init {\n") + codeWriter.indent() + codeWriter.emitCode(primaryConstructor.body) + codeWriter.unindent() + codeWriter.emit("}\n") + } + + // Constructors. + for (funSpec in funSpecs) { + if (!funSpec.isConstructor) continue + if (!firstMember) codeWriter.emit("\n") + funSpec.emit(codeWriter, name, kind.implicitFunctionModifiers(modifiers + implicitModifiers), false) + firstMember = false + } + + // Functions. + for (funSpec in funSpecs) { + if (funSpec.isConstructor) continue + if (!firstMember) codeWriter.emit("\n") + funSpec.emit(codeWriter, name, kind.implicitFunctionModifiers(modifiers + implicitModifiers), true) + firstMember = false + } + + for (typeSpec in typeSpecs) { + if (!firstMember) codeWriter.emit("\n") + typeSpec.emit(codeWriter, null, kind.implicitTypeModifiers(modifiers + implicitModifiers), isNestedExternal = areNestedExternal) + firstMember = false + } + + codeWriter.unindent() + codeWriter.popType() + + codeWriter.emit("}") + if (enumName == null && !isAnonymousClass) { + codeWriter.emit("\n") // If this type isn't also a value, include a trailing newline. + } + } finally { + codeWriter.statementLine = previousStatementLine + } + } + + /** Returns the properties that can be declared inline as constructor parameters. */ + private fun constructorProperties(): Map<String, PropertySpec> { + if (primaryConstructor == null) return emptyMap() + + // Properties added after the initializer are not permitted to be inlined into the constructor + // due to ordering concerns. + val range = if (hasInitializer) { + 0 until initializerIndex + } else { + propertySpecs.indices + } + val result: MutableMap<String, PropertySpec> = LinkedHashMap() + for (propertyIndex in range) { + val property = propertySpecs[propertyIndex] + if (property.getter != null || property.setter != null) continue + val parameter = primaryConstructor.parameter(property.name) ?: continue + if (parameter.type != property.type) continue + if (!isPropertyInitializerConstructorParameter(property, parameter)) { + continue + } + + result[property.name] = property.fromPrimaryConstructorParameter(parameter) + } + return result + } + + /** + * Returns true if the property can be declared inline as a constructor parameter + */ + private fun isPropertyInitializerConstructorParameter( + property: PropertySpec, + parameter: ParameterSpec, + ): Boolean { + val parameterName = CodeBlock.of("%N", parameter).toString() + val initializerCode = property.initializer.toString().escapeIfNecessary(validate = false) + return parameterName == initializerCode + } + + /** + * Returns KDoc comments including those of primary constructor parameters. + * + * If the primary constructor parameter is not mapped to a property, or if the property doesn't + * have its own KDoc - the parameter's KDoc will be attached to the parameter. Otherwise, if both + * the parameter and the property have KDoc - the property's KDoc will be attached to the + * property/parameter, and the parameter's KDoc will be printed in the type header. + */ + private fun kdocWithConstructorParameters(): CodeBlock { + if (primaryConstructor == null || primaryConstructor.parameters.isEmpty()) { + return kdoc.ensureEndsWithNewLine() + } + val constructorProperties = constructorProperties() + val parametersWithKdoc = primaryConstructor.parameters.filter { parameter -> + val propertyKdoc = constructorProperties[parameter.name]?.kdoc ?: CodeBlock.EMPTY + return@filter parameter.kdoc.isNotEmpty() && propertyKdoc.isNotEmpty() && + parameter.kdoc != propertyKdoc + } + return with(kdoc.ensureEndsWithNewLine().toBuilder()) { + parametersWithKdoc.forEachIndexed { index, parameter -> + if (index == 0 && kdoc.isNotEmpty()) add("\n") + add("@param %L %L", parameter.name, parameter.kdoc.ensureEndsWithNewLine()) + } + build() + } + } + + private val hasInitializer: Boolean get() = initializerIndex != -1 && initializerBlock.isNotEmpty() + + private val hasNoBody: Boolean + get() { + if (propertySpecs.isNotEmpty()) { + val constructorProperties = constructorProperties() + for (propertySpec in propertySpecs) { + if (!constructorProperties.containsKey(propertySpec.name)) { + return false + } + } + } + return enumConstants.isEmpty() && + initializerBlock.isEmpty() && + (primaryConstructor?.body?.isEmpty() ?: true) && + funSpecs.isEmpty() && + typeSpecs.isEmpty() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null) return false + if (javaClass != other.javaClass) return false + return toString() == other.toString() + } + + override fun hashCode(): Int = toString().hashCode() + + override fun toString(): String = buildCodeString { emit(this, null) } + + public enum class Kind( + internal val declarationKeyword: String, + internal val defaultImplicitPropertyModifiers: Set<KModifier>, + internal val defaultImplicitFunctionModifiers: Set<KModifier>, + internal val defaultImplicitTypeModifiers: Set<KModifier>, + ) { + CLASS("class", setOf(PUBLIC), setOf(PUBLIC), setOf()), + OBJECT("object", setOf(PUBLIC), setOf(PUBLIC), setOf()), + INTERFACE("interface", setOf(PUBLIC, ABSTRACT), setOf(PUBLIC, ABSTRACT), setOf()), + ; + + internal fun implicitPropertyModifiers(modifiers: Set<KModifier>): Set<KModifier> { + return defaultImplicitPropertyModifiers + when { + ANNOTATION in modifiers -> emptySet() + EXPECT in modifiers -> setOf(EXPECT) + EXTERNAL in modifiers -> setOf(EXTERNAL) + else -> emptySet() + } + } + + internal fun implicitFunctionModifiers(modifiers: Set<KModifier> = setOf()): Set<KModifier> { + return defaultImplicitFunctionModifiers + when { + ANNOTATION in modifiers -> setOf(ABSTRACT) + EXPECT in modifiers -> setOf(EXPECT) + EXTERNAL in modifiers -> setOf(EXTERNAL) + else -> emptySet() + } + } + + internal fun implicitTypeModifiers(modifiers: Set<KModifier> = setOf()): Set<KModifier> { + return defaultImplicitTypeModifiers + when { + EXPECT in modifiers -> setOf(EXPECT) + EXTERNAL in modifiers -> setOf(EXTERNAL) + else -> emptySet() + } + } + } + + public class Builder internal constructor( + internal var kind: Kind, + internal val name: String?, + vararg modifiers: KModifier, + ) : Taggable.Builder<Builder>, OriginatingElementsHolder.Builder<Builder>, ContextReceivable.Builder<Builder> { + internal val kdoc = CodeBlock.builder() + internal var primaryConstructor: FunSpec? = null + internal var superclass: TypeName = ANY + internal val initializerBlock = CodeBlock.builder() + public var initializerIndex: Int = -1 + internal val isAnonymousClass get() = name == null && kind == Kind.CLASS + internal val isExternal get() = EXTERNAL in modifiers + internal val isEnum get() = kind == Kind.CLASS && ENUM in modifiers + internal val isAnnotation get() = kind == Kind.CLASS && ANNOTATION in modifiers + internal val isCompanion get() = kind == Kind.OBJECT && COMPANION in modifiers + internal val isInlineOrValClass get() = kind == Kind.CLASS && + (INLINE in modifiers || VALUE in modifiers) + internal val isSimpleClass get() = kind == Kind.CLASS && !isEnum && !isAnnotation + internal val isFunInterface get() = kind == Kind.INTERFACE && FUN in modifiers + + override val tags: MutableMap<KClass<*>, Any> = mutableMapOf() + override val originatingElements: MutableList<Element> = mutableListOf() + + @ExperimentalKotlinPoetApi + override val contextReceiverTypes: MutableList<TypeName> = mutableListOf() + public val modifiers: MutableSet<KModifier> = mutableSetOf(*modifiers) + public val superinterfaces: MutableMap<TypeName, CodeBlock?> = mutableMapOf() + public val enumConstants: MutableMap<String, TypeSpec> = mutableMapOf() + public val annotationSpecs: MutableList<AnnotationSpec> = mutableListOf() + public val typeVariables: MutableList<TypeVariableName> = mutableListOf() + public val superclassConstructorParameters: MutableList<CodeBlock> = mutableListOf() + public val propertySpecs: MutableList<PropertySpec> = mutableListOf() + public val funSpecs: MutableList<FunSpec> = mutableListOf() + public val typeSpecs: MutableList<TypeSpec> = mutableListOf() + + public fun addKdoc(format: String, vararg args: Any): Builder = apply { + kdoc.add(format, *args) + } + + public fun addKdoc(block: CodeBlock): Builder = apply { + kdoc.add(block) + } + + public fun addAnnotations(annotationSpecs: Iterable<AnnotationSpec>): Builder = apply { + this.annotationSpecs += annotationSpecs + } + + public fun addAnnotation(annotationSpec: AnnotationSpec): Builder = apply { + annotationSpecs += annotationSpec + } + + public fun addAnnotation(annotation: ClassName): Builder = + addAnnotation(AnnotationSpec.builder(annotation).build()) + + @DelicateKotlinPoetApi( + message = "Java reflection APIs don't give complete information on Kotlin types. Consider " + + "using the kotlinpoet-metadata APIs instead.", + ) + public fun addAnnotation(annotation: Class<*>): Builder = + addAnnotation(annotation.asClassName()) + + public fun addAnnotation(annotation: KClass<*>): Builder = + addAnnotation(annotation.asClassName()) + + public fun addModifiers(vararg modifiers: KModifier): Builder = apply { + check(!isAnonymousClass) { "forbidden on anonymous types." } + this.modifiers += modifiers + } + + public fun addModifiers(modifiers: Iterable<KModifier>): Builder = apply { + check(!isAnonymousClass) { "forbidden on anonymous types." } + this.modifiers += modifiers + } + + public fun addTypeVariables(typeVariables: Iterable<TypeVariableName>): Builder = apply { + this.typeVariables += typeVariables + } + + public fun addTypeVariable(typeVariable: TypeVariableName): Builder = apply { + typeVariables += typeVariable + } + + public fun primaryConstructor(primaryConstructor: FunSpec?): Builder = apply { + check(kind == Kind.CLASS) { + "$kind can't have a primary constructor" + } + if (primaryConstructor != null) { + require(primaryConstructor.isConstructor) { + "expected a constructor but was ${primaryConstructor.name}" + } + + if (isInlineOrValClass) { + check(primaryConstructor.parameters.size == 1) { + "value/inline classes must have 1 parameter in constructor" + } + } + } + this.primaryConstructor = primaryConstructor + } + + public fun superclass(superclass: TypeName): Builder = apply { + checkCanHaveSuperclass() + check(this.superclass === ANY) { "superclass already set to ${this.superclass}" } + this.superclass = superclass + } + + private fun checkCanHaveSuperclass() { + check(isSimpleClass || kind == Kind.OBJECT) { + "only classes can have super classes, not $kind" + } + check(!isInlineOrValClass) { + "value/inline classes cannot have super classes" + } + } + + private fun checkCanHaveInitializerBlocks() { + check(isSimpleClass || isEnum || kind == Kind.OBJECT) { + "$kind can't have initializer blocks" + } + check(EXPECT !in modifiers) { + "expect $kind can't have initializer blocks" + } + } + + @DelicateKotlinPoetApi( + message = "Java reflection APIs don't give complete information on Kotlin types. Consider " + + "using the kotlinpoet-metadata APIs instead.", + ) + public fun superclass(superclass: Type): Builder = superclass(superclass.asTypeName()) + + public fun superclass(superclass: KClass<*>): Builder = superclass(superclass.asTypeName()) + + public fun addSuperclassConstructorParameter( + format: String, + vararg args: Any, + ): Builder = apply { + addSuperclassConstructorParameter(CodeBlock.of(format, *args)) + } + + public fun addSuperclassConstructorParameter(codeBlock: CodeBlock): Builder = apply { + checkCanHaveSuperclass() + this.superclassConstructorParameters += codeBlock + } + + public fun addSuperinterfaces(superinterfaces: Iterable<TypeName>): Builder = apply { + this.superinterfaces.putAll(superinterfaces.map { it to null }) + } + + public fun addSuperinterface( + superinterface: TypeName, + delegate: CodeBlock = CodeBlock.EMPTY, + ): Builder = apply { + if (delegate.isEmpty()) { + this.superinterfaces[superinterface] = null + } else { + require(isSimpleClass || kind == Kind.OBJECT) { + "delegation only allowed for classes and objects (found $kind '$name')" + } + require(!superinterface.isNullable) { + "expected non-nullable type but was '${superinterface.copy(nullable = false)}'" + } + require(this.superinterfaces[superinterface] == null) { + "'$name' can not delegate to $superinterface by $delegate with existing declaration by " + + "${this.superinterfaces[superinterface]}" + } + this.superinterfaces[superinterface] = delegate + } + } + + @DelicateKotlinPoetApi( + message = "Java reflection APIs don't give complete information on Kotlin types. Consider " + + "using the kotlinpoet-metadata APIs instead.", + ) + public fun addSuperinterface( + superinterface: Type, + delegate: CodeBlock = CodeBlock.EMPTY, + ): Builder = addSuperinterface(superinterface.asTypeName(), delegate) + + public fun addSuperinterface( + superinterface: KClass<*>, + delegate: CodeBlock = CodeBlock.EMPTY, + ): Builder = addSuperinterface(superinterface.asTypeName(), delegate) + + public fun addSuperinterface( + superinterface: KClass<*>, + constructorParameterName: String, + ): Builder = addSuperinterface(superinterface.asTypeName(), constructorParameterName) + + public fun addSuperinterface( + superinterface: TypeName, + constructorParameter: String, + ): Builder = apply { + requireNotNull(primaryConstructor) { + "delegating to constructor parameter requires not-null constructor" + } + val parameter = primaryConstructor?.parameter(constructorParameter) + requireNotNull(parameter) { + "no such constructor parameter '$constructorParameter' to delegate to for type '$name'" + } + addSuperinterface(superinterface, CodeBlock.of(constructorParameter)) + } + + @JvmOverloads public fun addEnumConstant( + name: String, + typeSpec: TypeSpec = anonymousClassBuilder().build(), + ): Builder = apply { + require(name != "name" && name != "ordinal") { + "constant with name \"$name\" conflicts with a supertype member with the same name" + } + enumConstants[name] = typeSpec + } + + public fun addProperties(propertySpecs: Iterable<PropertySpec>): Builder = apply { + propertySpecs.map(this::addProperty) + } + + public fun addProperty(propertySpec: PropertySpec): Builder = apply { + if (EXPECT in modifiers) { + require(propertySpec.initializer == null) { + "properties in expect classes can't have initializers" + } + require(propertySpec.getter == null && propertySpec.setter == null) { + "properties in expect classes can't have getters and setters" + } + } + if (isEnum) { + require(propertySpec.name != "name" && propertySpec.name != "ordinal") { + "${propertySpec.name} is a final supertype member and can't be redeclared or overridden" + } + } + propertySpecs += propertySpec + } + + public fun addProperty(name: String, type: TypeName, vararg modifiers: KModifier): Builder = + addProperty(PropertySpec.builder(name, type, *modifiers).build()) + + @DelicateKotlinPoetApi( + message = "Java reflection APIs don't give complete information on Kotlin types. Consider " + + "using the kotlinpoet-metadata APIs instead.", + ) + public fun addProperty(name: String, type: Type, vararg modifiers: KModifier): Builder = + addProperty(name, type.asTypeName(), *modifiers) + + public fun addProperty(name: String, type: KClass<*>, vararg modifiers: KModifier): Builder = + addProperty(name, type.asTypeName(), *modifiers) + + public fun addProperty(name: String, type: TypeName, modifiers: Iterable<KModifier>): Builder = + addProperty(PropertySpec.builder(name, type, modifiers).build()) + + @DelicateKotlinPoetApi( + message = "Java reflection APIs don't give complete information on Kotlin types. Consider " + + "using the kotlinpoet-metadata APIs instead.", + ) + public fun addProperty(name: String, type: Type, modifiers: Iterable<KModifier>): Builder = + addProperty(name, type.asTypeName(), modifiers) + + public fun addProperty(name: String, type: KClass<*>, modifiers: Iterable<KModifier>): Builder = + addProperty(name, type.asTypeName(), modifiers) + + public fun addInitializerBlock(block: CodeBlock): Builder = apply { + checkCanHaveInitializerBlocks() + // Set index to however many properties we have + // All properties added after this point are declared as such, including any that initialize + // to a constructor param. + initializerIndex = propertySpecs.size + initializerBlock.add("init {\n") + .indent() + .add(block) + .unindent() + .add("}\n") + } + + public fun addFunctions(funSpecs: Iterable<FunSpec>): Builder = apply { + funSpecs.forEach { addFunction(it) } + } + + public fun addFunction(funSpec: FunSpec): Builder = apply { + funSpecs += funSpec + } + + public fun addTypes(typeSpecs: Iterable<TypeSpec>): Builder = apply { + this.typeSpecs += typeSpecs + } + + public fun addType(typeSpec: TypeSpec): Builder = apply { + typeSpecs += typeSpec + } + + @ExperimentalKotlinPoetApi + override fun contextReceivers(receiverTypes: Iterable<TypeName>): Builder = apply { + check(isSimpleClass) { "contextReceivers can only be applied on simple classes" } + contextReceiverTypes += receiverTypes + } + + public fun build(): TypeSpec { + if (enumConstants.isNotEmpty()) { + check(isEnum) { "$name is not an enum and cannot have enum constants" } + } + + if (superclassConstructorParameters.isNotEmpty()) { + checkCanHaveSuperclass() + + check(!isExternal) { + "delegated constructor call in external class is not allowed" + } + } + check(!(isExternal && funSpecs.any { it.delegateConstructor != null })) { + "delegated constructor call in external class is not allowed" + } + + check(!(isAnonymousClass && typeVariables.isNotEmpty())) { + "typevariables are forbidden on anonymous types" + } + + val isAbstract = ABSTRACT in modifiers || SEALED in modifiers || kind != Kind.CLASS || + !isSimpleClass + for (funSpec in funSpecs) { + require(isAbstract || ABSTRACT !in funSpec.modifiers) { + "non-abstract type $name cannot declare abstract function ${funSpec.name}" + } + when { + kind == Kind.INTERFACE -> { + requireNoneOf(funSpec.modifiers, INTERNAL, PROTECTED) + requireNoneOrOneOf(funSpec.modifiers, ABSTRACT, PRIVATE) + } + isAnnotation -> require(funSpec.modifiers == kind.implicitFunctionModifiers(modifiers)) { + "annotation class $name.${funSpec.name} " + + "requires modifiers ${kind.implicitFunctionModifiers(modifiers)}" + } + EXPECT in modifiers -> require(funSpec.body.isEmpty()) { + "functions in expect classes can't have bodies" + } + } + } + + if (primaryConstructor == null) { + require(funSpecs.none { it.isConstructor } || superclassConstructorParameters.isEmpty()) { + "types without a primary constructor cannot specify secondary constructors and " + + "superclass constructor parameters" + } + } + + if (isInlineOrValClass) { + primaryConstructor?.let { + check(it.parameters.size == 1) { + "value/inline classes must have 1 parameter in constructor" + } + } + + check(propertySpecs.size > 0) { + "value/inline classes must have at least 1 property" + } + + val constructorParamName = primaryConstructor?.parameters?.firstOrNull()?.name + constructorParamName?.let { paramName -> + val underlyingProperty = propertySpecs.find { it.name == paramName } + requireNotNull(underlyingProperty) { + "value/inline classes must have a single read-only (val) property parameter." + } + check(!underlyingProperty.mutable) { + "value/inline classes must have a single read-only (val) property parameter." + } + } + check(superclass == Any::class.asTypeName()) { + "value/inline classes cannot have super classes" + } + } + + if (isFunInterface) { + // Note: Functional interfaces can contain any number of non-abstract functions. + val abstractFunSpecs = funSpecs.filter { ABSTRACT in it.modifiers } + check(abstractFunSpecs.size == 1) { + "Functional interfaces must have exactly one abstract function. Contained " + + "${abstractFunSpecs.size}: ${abstractFunSpecs.map { it.name }}" + } + } + + when (typeSpecs.count { it.isCompanion }) { + 0 -> Unit + 1 -> { + require(isSimpleClass || kind == Kind.INTERFACE || isEnum || isAnnotation) { + "$kind types can't have a companion object" + } + } + else -> { + throw IllegalArgumentException("Multiple companion objects are present but only one is allowed.") + } + } + + return TypeSpec(this) + } + } + + public companion object { + @JvmStatic public fun classBuilder(name: String): Builder = Builder(Kind.CLASS, name) + + @JvmStatic public fun classBuilder(className: ClassName): Builder = + classBuilder(className.simpleName) + + @JvmStatic public fun expectClassBuilder(name: String): Builder = + Builder(Kind.CLASS, name, EXPECT) + + @JvmStatic public fun expectClassBuilder(className: ClassName): Builder = + expectClassBuilder(className.simpleName) + + @JvmStatic public fun valueClassBuilder(name: String): Builder = + Builder(Kind.CLASS, name, VALUE) + + @JvmStatic public fun objectBuilder(name: String): Builder = Builder(Kind.OBJECT, name) + + @JvmStatic public fun objectBuilder(className: ClassName): Builder = + objectBuilder(className.simpleName) + + @JvmStatic @JvmOverloads + public fun companionObjectBuilder(name: String? = null): Builder = + Builder(Kind.OBJECT, name, COMPANION) + + @JvmStatic public fun interfaceBuilder(name: String): Builder = Builder(Kind.INTERFACE, name) + + @JvmStatic public fun interfaceBuilder(className: ClassName): Builder = + interfaceBuilder(className.simpleName) + + @JvmStatic public fun funInterfaceBuilder(name: String): Builder = + Builder(Kind.INTERFACE, name, FUN) + + @JvmStatic public fun funInterfaceBuilder(className: ClassName): Builder = + funInterfaceBuilder(className.simpleName) + + @JvmStatic public fun enumBuilder(name: String): Builder = Builder(Kind.CLASS, name, ENUM) + + @JvmStatic public fun enumBuilder(className: ClassName): Builder = + enumBuilder(className.simpleName) + + @JvmStatic public fun anonymousClassBuilder(): Builder = Builder(Kind.CLASS, null) + + @JvmStatic public fun annotationBuilder(name: String): Builder = + Builder(Kind.CLASS, name, ANNOTATION) + + @JvmStatic public fun annotationBuilder(className: ClassName): Builder = + annotationBuilder(className.simpleName) + } +} diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/TypeVariableName.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/TypeVariableName.kt new file mode 100644 index 00000000..b2d7517a --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/TypeVariableName.kt @@ -0,0 +1,276 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:JvmName("TypeVariableNames") + +package com.squareup.kotlinpoet + +import java.lang.reflect.Type +import java.util.Collections +import javax.lang.model.element.TypeParameterElement +import javax.lang.model.type.TypeMirror +import javax.lang.model.type.TypeVariable +import kotlin.reflect.KClass +import kotlin.reflect.KType +import kotlin.reflect.KTypeParameter +import kotlin.reflect.KVariance + +public class TypeVariableName private constructor( + public val name: String, + public val bounds: List<TypeName>, + + /** Either [KModifier.IN], [KModifier.OUT], or null. */ + public val variance: KModifier? = null, + public val isReified: Boolean = false, + nullable: Boolean = false, + annotations: List<AnnotationSpec> = emptyList(), + tags: Map<KClass<*>, Any> = emptyMap(), +) : TypeName(nullable, annotations, TagMap(tags)) { + + override fun copy( + nullable: Boolean, + annotations: List<AnnotationSpec>, + tags: Map<KClass<*>, Any>, + ): TypeVariableName { + return copy(nullable, annotations, this.bounds, this.isReified, tags) + } + + public fun copy( + nullable: Boolean = this.isNullable, + annotations: List<AnnotationSpec> = this.annotations.toList(), + bounds: List<TypeName> = this.bounds.toList(), + reified: Boolean = this.isReified, + tags: Map<KClass<*>, Any> = this.tagMap.tags, + ): TypeVariableName { + return TypeVariableName( + name, + bounds.withoutImplicitBound(), + variance, + reified, + nullable, + annotations, + tags, + ) + } + + private fun List<TypeName>.withoutImplicitBound(): List<TypeName> { + return if (size == 1) this else filterNot { it == NULLABLE_ANY } + } + + override fun emit(out: CodeWriter) = out.emit(name) + + public companion object { + internal fun of( + name: String, + bounds: List<TypeName>, + variance: KModifier?, + ): TypeVariableName { + require(variance == null || variance.isOneOf(KModifier.IN, KModifier.OUT)) { + "$variance is an invalid variance modifier, the only allowed values are in and out!" + } + require(bounds.isNotEmpty()) { + "$name has no bounds" + } + // Strip Any? from bounds if it is present. + return TypeVariableName(name, bounds, variance) + } + + /** Returns type variable named `name` with `variance` and without bounds. */ + @JvmStatic + @JvmName("get") + @JvmOverloads + public operator fun invoke(name: String, variance: KModifier? = null): TypeVariableName = + of(name = name, bounds = NULLABLE_ANY_LIST, variance = variance) + + /** Returns type variable named `name` with `variance` and `bounds`. */ + @JvmStatic + @JvmName("get") + @JvmOverloads + public operator fun invoke( + name: String, + vararg bounds: TypeName, + variance: KModifier? = null, + ): TypeVariableName = + of( + name = name, + bounds = bounds.toList().ifEmpty(::NULLABLE_ANY_LIST), + variance = variance, + ) + + /** Returns type variable named `name` with `variance` and `bounds`. */ + @JvmStatic + @JvmName("get") + @JvmOverloads + public operator fun invoke( + name: String, + vararg bounds: KClass<*>, + variance: KModifier? = null, + ): TypeVariableName = + of( + name = name, + bounds = bounds.map(KClass<*>::asTypeName).ifEmpty(::NULLABLE_ANY_LIST), + variance = variance, + ) + + /** Returns type variable named `name` with `variance` and `bounds`. */ + @JvmStatic + @JvmName("get") + @JvmOverloads + public operator fun invoke( + name: String, + vararg bounds: Type, + variance: KModifier? = null, + ): TypeVariableName = + of( + name = name, + bounds = bounds.map(Type::asTypeName).ifEmpty(::NULLABLE_ANY_LIST), + variance = variance, + ) + + /** Returns type variable named `name` with `variance` and `bounds`. */ + @JvmStatic + @JvmName("get") + @JvmOverloads + public operator fun invoke( + name: String, + bounds: List<TypeName>, + variance: KModifier? = null, + ): TypeVariableName = of(name, bounds.ifEmpty(::NULLABLE_ANY_LIST), variance) + + /** Returns type variable named `name` with `variance` and `bounds`. */ + @JvmStatic + @JvmName("getWithClasses") + @JvmOverloads + public operator fun invoke( + name: String, + bounds: Iterable<KClass<*>>, + variance: KModifier? = null, + ): TypeVariableName = + of( + name, + bounds.map { it.asTypeName() }.ifEmpty(::NULLABLE_ANY_LIST), + variance, + ) + + /** Returns type variable named `name` with `variance` and `bounds`. */ + @JvmStatic + @JvmName("getWithTypes") + @JvmOverloads + public operator fun invoke( + name: String, + bounds: Iterable<Type>, + variance: KModifier? = null, + ): TypeVariableName = + of( + name, + bounds.map { it.asTypeName() }.ifEmpty(::NULLABLE_ANY_LIST), + variance, + ) + + /** + * Make a TypeVariableName for the given TypeMirror. This form is used internally to avoid + * infinite recursion in cases like `Enum<E extends Enum<E>>`. When we encounter such a + * thing, we will make a TypeVariableName without bounds and add that to the `typeVariables` + * map before looking up the bounds. Then if we encounter this TypeVariable again while + * constructing the bounds, we can just return it from the map. And, the code that put the entry + * in `variables` will make sure that the bounds are filled in before returning. + */ + internal fun get( + mirror: javax.lang.model.type.TypeVariable, + typeVariables: MutableMap<TypeParameterElement, TypeVariableName>, + ): TypeVariableName { + val element = mirror.asElement() as TypeParameterElement + var typeVariableName: TypeVariableName? = typeVariables[element] + if (typeVariableName == null) { + // Since the bounds field is public, we need to make it an unmodifiableList. But we control + // the List that that wraps, which means we can change it before returning. + val bounds = mutableListOf<TypeName>() + val visibleBounds = Collections.unmodifiableList(bounds) + typeVariableName = TypeVariableName(element.simpleName.toString(), visibleBounds) + typeVariables[element] = typeVariableName + for (typeMirror in element.bounds) { + bounds += get(typeMirror, typeVariables) + } + bounds.remove(ANY) + bounds.remove(JAVA_OBJECT) + if (bounds.isEmpty()) { + bounds.add(NULLABLE_ANY) + } + } + return typeVariableName + } + + /** Returns type variable equivalent to `type`. */ + internal fun get( + type: java.lang.reflect.TypeVariable<*>, + map: MutableMap<Type, TypeVariableName> = mutableMapOf(), + ): TypeVariableName { + var result: TypeVariableName? = map[type] + if (result == null) { + val bounds = mutableListOf<TypeName>() + val visibleBounds = Collections.unmodifiableList(bounds) + result = TypeVariableName(type.name, visibleBounds) + map[type] = result + for (bound in type.bounds) { + bounds += get(bound, map) + } + bounds.remove(ANY) + bounds.remove(JAVA_OBJECT) + if (bounds.isEmpty()) { + bounds.add(NULLABLE_ANY) + } + } + return result + } + + internal val NULLABLE_ANY_LIST = listOf(NULLABLE_ANY) + private val JAVA_OBJECT = ClassName("java.lang", "Object") + } +} + +/** Returns type variable equivalent to `mirror`. */ +@DelicateKotlinPoetApi( + message = "Java reflection APIs don't give complete information on Kotlin types. Consider using" + + " the kotlinpoet-metadata APIs instead.", +) +@JvmName("get") +public fun TypeVariable.asTypeVariableName(): TypeVariableName = + (asElement() as TypeParameterElement).asTypeVariableName() + +/** Returns type variable equivalent to `element`. */ +@DelicateKotlinPoetApi( + message = "Element APIs don't give complete information on Kotlin types. Consider using" + + " the kotlinpoet-metadata APIs instead.", +) +@JvmName("get") +public fun TypeParameterElement.asTypeVariableName(): TypeVariableName { + val name = simpleName.toString() + val boundsTypeNames = bounds.map(TypeMirror::asTypeName) + .ifEmpty(TypeVariableName.Companion::NULLABLE_ANY_LIST) + return TypeVariableName.of(name, boundsTypeNames, variance = null) +} + +public fun KTypeParameter.asTypeVariableName(): TypeVariableName { + return TypeVariableName.of( + name = name, + bounds = upperBounds.map(KType::asTypeName) + .ifEmpty(TypeVariableName.Companion::NULLABLE_ANY_LIST), + variance = when (variance) { + KVariance.INVARIANT -> null + KVariance.IN -> KModifier.IN + KVariance.OUT -> KModifier.OUT + }, + ) +} diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/Util.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/Util.kt new file mode 100644 index 00000000..16a443dd --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/Util.kt @@ -0,0 +1,311 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.squareup.kotlinpoet.CodeBlock.Companion.isPlaceholder +import java.util.Collections + +internal object NullAppendable : Appendable { + override fun append(charSequence: CharSequence) = this + override fun append(charSequence: CharSequence, start: Int, end: Int) = this + override fun append(c: Char) = this +} + +internal fun <K, V> Map<K, V>.toImmutableMap(): Map<K, V> = + Collections.unmodifiableMap(LinkedHashMap(this)) + +internal fun <T> Collection<T>.toImmutableList(): List<T> = + Collections.unmodifiableList(ArrayList(this)) + +internal fun <T> Collection<T>.toImmutableSet(): Set<T> = + Collections.unmodifiableSet(LinkedHashSet(this)) + +internal inline fun <reified T : Enum<T>> Collection<T>.toEnumSet(): Set<T> = + enumValues<T>().filterTo(mutableSetOf(), this::contains) + +internal fun requireNoneOrOneOf(modifiers: Set<KModifier>, vararg mutuallyExclusive: KModifier) { + val count = mutuallyExclusive.count(modifiers::contains) + require(count <= 1) { + "modifiers $modifiers must contain none or only one of ${mutuallyExclusive.contentToString()}" + } +} + +internal fun requireNoneOf(modifiers: Set<KModifier>, vararg forbidden: KModifier) { + require(forbidden.none(modifiers::contains)) { + "modifiers $modifiers must contain none of ${forbidden.contentToString()}" + } +} + +internal fun <T> T.isOneOf(t1: T, t2: T, t3: T? = null, t4: T? = null, t5: T? = null, t6: T? = null) = + this == t1 || this == t2 || this == t3 || this == t4 || this == t5 || this == t6 + +internal fun <T> Collection<T>.containsAnyOf(vararg t: T) = t.any(this::contains) + +// see https://docs.oracle.com/javase/specs/jls/se7/html/jls-3.html#jls-3.10.6 +internal fun characterLiteralWithoutSingleQuotes(c: Char) = when { + c == '\b' -> "\\b" // \u0008: backspace (BS) + c == '\t' -> "\\t" // \u0009: horizontal tab (HT) + c == '\n' -> "\\n" // \u000a: linefeed (LF) + c == '\r' -> "\\r" // \u000d: carriage return (CR) + c == '\"' -> "\"" // \u0022: double quote (") + c == '\'' -> "\\'" // \u0027: single quote (') + c == '\\' -> "\\\\" // \u005c: backslash (\) + c.isIsoControl -> String.format("\\u%04x", c.code) + else -> c.toString() +} + +internal fun escapeCharacterLiterals(s: String) = buildString { + for (c in s) append(characterLiteralWithoutSingleQuotes(c)) +} + +private val Char.isIsoControl: Boolean + get() { + return this in '\u0000'..'\u001F' || this in '\u007F'..'\u009F' + } + +/** Returns the string literal representing `value`, including wrapping double quotes. */ +internal fun stringLiteralWithQuotes( + value: String, + isInsideRawString: Boolean = false, + isConstantContext: Boolean = false, +): String { + if (!isConstantContext && '\n' in value) { + val result = StringBuilder(value.length + 32) + result.append("\"\"\"\n|") + var i = 0 + while (i < value.length) { + val c = value[i] + if (value.regionMatches(i, "\"\"\"", 0, 3)) { + // Don't inadvertently end the raw string too early + result.append("\"\"\${'\"'}") + i += 2 + } else if (c == '\n') { + // Add a '|' after newlines. This pipe will be removed by trimMargin(). + result.append("\n|") + } else if (c == '$' && !isInsideRawString) { + // Escape '$' symbols with ${'$'}. + result.append("\${\'\$\'}") + } else { + result.append(c) + } + i++ + } + // If the last-emitted character wasn't a margin '|', add a blank line. This will get removed + // by trimMargin(). + if (!value.endsWith("\n")) result.append("\n") + result.append("\"\"\".trimMargin()") + return result.toString() + } else { + val result = StringBuilder(value.length + 32) + // using pre-formatted strings allows us to get away with not escaping symbols that would + // normally require escaping, e.g. "foo ${"bar"} baz" + if (isInsideRawString) result.append("\"\"\"") else result.append('"') + for (c in value) { + // Trivial case: single quote must not be escaped. + if (c == '\'') { + result.append("'") + continue + } + // Trivial case: double quotes must be escaped. + if (c == '\"' && !isInsideRawString) { + result.append("\\\"") + continue + } + // Trivial case: $ signs must be escaped. + if (c == '$' && !isInsideRawString) { + result.append("\${\'\$\'}") + continue + } + // Default case: just let character literal do its work. + result.append(if (isInsideRawString) c else characterLiteralWithoutSingleQuotes(c)) + // Need to append indent after linefeed? + } + if (isInsideRawString) result.append("\"\"\"") else result.append('"') + return result.toString() + } +} + +internal fun CodeBlock.ensureEndsWithNewLine() = if (isEmpty()) { + this +} else { + with(toBuilder()) { + val lastFormatPart = trim().formatParts.last() + if (lastFormatPart.isPlaceholder && args.isNotEmpty()) { + val lastArg = args.last() + if (lastArg is String) { + args[args.size - 1] = lastArg.trimEnd('\n') + '\n' + } + } else { + formatParts[formatParts.lastIndexOf(lastFormatPart)] = lastFormatPart.trimEnd('\n') + formatParts += "\n" + } + return@with build() + } +} + +private val IDENTIFIER_REGEX = + ( + "((\\p{gc=Lu}+|\\p{gc=Ll}+|\\p{gc=Lt}+|\\p{gc=Lm}+|\\p{gc=Lo}+|\\p{gc=Nl}+)+" + + "\\d*" + + "\\p{gc=Lu}*\\p{gc=Ll}*\\p{gc=Lt}*\\p{gc=Lm}*\\p{gc=Lo}*\\p{gc=Nl}*)" + + "|" + + "(`[^\n\r`]+`)" + ) + .toRegex() + +internal val String.isIdentifier get() = IDENTIFIER_REGEX.matches(this) + +// https://kotlinlang.org/docs/reference/keyword-reference.html +private val KEYWORDS = setOf( + // Hard keywords + "as", + "break", + "class", + "continue", + "do", + "else", + "false", + "for", + "fun", + "if", + "in", + "interface", + "is", + "null", + "object", + "package", + "return", + "super", + "this", + "throw", + "true", + "try", + "typealias", + "typeof", + "val", + "var", + "when", + "while", + + // Soft keywords + "by", + "catch", + "constructor", + "delegate", + "dynamic", + "field", + "file", + "finally", + "get", + "import", + "init", + "param", + "property", + "receiver", + "set", + "setparam", + "where", + + // Modifier keywords + "actual", + "abstract", + "annotation", + "companion", + "const", + "crossinline", + "data", + "enum", + "expect", + "external", + "final", + "infix", + "inline", + "inner", + "internal", + "lateinit", + "noinline", + "open", + "operator", + "out", + "override", + "private", + "protected", + "public", + "reified", + "sealed", + "suspend", + "tailrec", + "value", + "vararg", + + // These aren't keywords anymore but still break some code if unescaped. https://youtrack.jetbrains.com/issue/KT-52315 + "header", + "impl", + + // Other reserved keywords + "yield", +) + +private const val ALLOWED_CHARACTER = '$' + +private const val UNDERSCORE_CHARACTER = '_' + +internal val String.isKeyword get() = this in KEYWORDS + +internal val String.hasAllowedCharacters get() = this.any { it == ALLOWED_CHARACTER } + +internal val String.allCharactersAreUnderscore get() = this.all { it == UNDERSCORE_CHARACTER } + +// https://github.com/JetBrains/kotlin/blob/master/compiler/frontend.java/src/org/jetbrains/kotlin/resolve/jvm/checkers/JvmSimpleNameBacktickChecker.kt +private val ILLEGAL_CHARACTERS_TO_ESCAPE = setOf('.', ';', '[', ']', '/', '<', '>', ':', '\\') + +private fun String.failIfEscapeInvalid() { + require(!any { it in ILLEGAL_CHARACTERS_TO_ESCAPE }) { + "Can't escape identifier $this because it contains illegal characters: " + + ILLEGAL_CHARACTERS_TO_ESCAPE.intersect(this.toSet()).joinToString("") + } +} + +internal fun String.escapeIfNecessary(validate: Boolean = true): String = escapeIfNotJavaIdentifier() + .escapeIfKeyword() + .escapeIfHasAllowedCharacters() + .escapeIfAllCharactersAreUnderscore() + .apply { if (validate) failIfEscapeInvalid() } + +private fun String.alreadyEscaped() = startsWith("`") && endsWith("`") + +private fun String.escapeIfKeyword() = if (isKeyword && !alreadyEscaped()) "`$this`" else this + +private fun String.escapeIfHasAllowedCharacters() = if (hasAllowedCharacters && !alreadyEscaped()) "`$this`" else this + +private fun String.escapeIfAllCharactersAreUnderscore() = if (allCharactersAreUnderscore && !alreadyEscaped()) "`$this`" else this + +private fun String.escapeIfNotJavaIdentifier(): String { + return if (( + !Character.isJavaIdentifierStart(first()) || + drop(1).any { !Character.isJavaIdentifierPart(it) } + ) && + !alreadyEscaped() + ) { + "`$this`".replace(' ', '·') + } else { + this + } +} + +internal fun String.escapeSegmentsIfNecessary(delimiter: Char = '.') = split(delimiter) + .filter { it.isNotEmpty() } + .joinToString(delimiter.toString()) { it.escapeIfNecessary() } diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/WildcardTypeName.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/WildcardTypeName.kt new file mode 100644 index 00000000..f7c4dc0c --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/WildcardTypeName.kt @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:JvmName("WildcardTypeNames") + +package com.squareup.kotlinpoet + +import java.lang.reflect.Type +import java.lang.reflect.WildcardType +import javax.lang.model.element.TypeParameterElement +import kotlin.reflect.KClass + +public class WildcardTypeName private constructor( + outTypes: List<TypeName>, + inTypes: List<TypeName>, + nullable: Boolean = false, + annotations: List<AnnotationSpec> = emptyList(), + tags: Map<KClass<*>, Any> = emptyMap(), +) : TypeName(nullable, annotations, TagMap(tags)) { + public val outTypes: List<TypeName> = outTypes.toImmutableList() + public val inTypes: List<TypeName> = inTypes.toImmutableList() + + init { + require(this.outTypes.size == 1) { "unexpected out types: $outTypes" } + } + + override fun copy( + nullable: Boolean, + annotations: List<AnnotationSpec>, + tags: Map<KClass<*>, Any>, + ): WildcardTypeName { + return WildcardTypeName(outTypes, inTypes, nullable, annotations, tags) + } + + override fun emit(out: CodeWriter): CodeWriter { + return when { + inTypes.size == 1 -> out.emitCode("in·%T", inTypes[0]) + outTypes == STAR.outTypes -> out.emit("*") + else -> out.emitCode("out·%T", outTypes[0]) + } + } + + public companion object { + /** + * Returns a type that represents an unknown type that produces `outType`. For example, if + * `outType` is `CharSequence`, this returns `out CharSequence`. If `outType` is `Any?`, this + * returns `*`, which is shorthand for `out Any?`. + */ + @JvmStatic public fun producerOf(outType: TypeName): WildcardTypeName = + WildcardTypeName(listOf(outType), emptyList()) + + @DelicateKotlinPoetApi( + message = "Java reflection APIs don't give complete information on Kotlin types. Consider " + + "using the kotlinpoet-metadata APIs instead.", + ) + @JvmStatic + public fun producerOf(outType: Type): WildcardTypeName = + producerOf(outType.asTypeName()) + + @JvmStatic public fun producerOf(outType: KClass<*>): WildcardTypeName = + producerOf(outType.asTypeName()) + + /** + * Returns a type that represents an unknown type that consumes `inType`. For example, if + * `inType` is `String`, this returns `in String`. + */ + @JvmStatic public fun consumerOf(inType: TypeName): WildcardTypeName = + WildcardTypeName(listOf(ANY), listOf(inType)) + + @DelicateKotlinPoetApi( + message = "Java reflection APIs don't give complete information on Kotlin types. Consider " + + "using the kotlinpoet-metadata APIs instead.", + ) + @JvmStatic + public fun consumerOf(inType: Type): WildcardTypeName = + consumerOf(inType.asTypeName()) + + @JvmStatic public fun consumerOf(inType: KClass<*>): WildcardTypeName = + consumerOf(inType.asTypeName()) + + internal fun get( + mirror: javax.lang.model.type.WildcardType, + typeVariables: Map<TypeParameterElement, TypeVariableName>, + ): TypeName { + val outType = mirror.extendsBound + return if (outType == null) { + val inType = mirror.superBound + if (inType == null) { + STAR + } else { + consumerOf(get(inType, typeVariables)) + } + } else { + producerOf(get(outType, typeVariables)) + } + } + + internal fun get( + wildcardName: WildcardType, + map: MutableMap<Type, TypeVariableName>, + ): TypeName { + return WildcardTypeName( + wildcardName.upperBounds.map { get(it, map = map) }, + wildcardName.lowerBounds.map { get(it, map = map) }, + ) + } + } +} + +@DelicateKotlinPoetApi( + message = "Mirror APIs don't give complete information on Kotlin types. Consider using" + + " the kotlinpoet-metadata APIs instead.", +) +@JvmName("get") +public fun javax.lang.model.type.WildcardType.asWildcardTypeName(): TypeName = + WildcardTypeName.get(this, mutableMapOf()) + +@DelicateKotlinPoetApi( + message = "Java reflection APIs don't give complete information on Kotlin types. Consider using" + + " the kotlinpoet-metadata APIs instead.", +) +@JvmName("get") +public fun WildcardType.asWildcardTypeName(): TypeName = + WildcardTypeName.get(this, mutableMapOf()) diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/jvm/JvmAnnotations.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/jvm/JvmAnnotations.kt new file mode 100644 index 00000000..6f18620e --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/jvm/JvmAnnotations.kt @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +@file:JvmName("JvmAnnotations") + +package com.squareup.kotlinpoet.jvm + +import com.squareup.kotlinpoet.AnnotationSpec +import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget.FILE +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.FunSpec.Companion.isAccessor +import com.squareup.kotlinpoet.FunSpec.Companion.isConstructor +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.TypeName +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.asTypeName +import java.lang.reflect.Type +import kotlin.reflect.KClass + +public fun FileSpec.Builder.jvmName(name: String): FileSpec.Builder = addAnnotation( + AnnotationSpec.builder(JvmName::class) + .useSiteTarget(FILE) + .addMember("%S", name) + .build(), +) + +public fun FileSpec.Builder.jvmMultifileClass(): FileSpec.Builder = addAnnotation( + AnnotationSpec.builder(JvmMultifileClass::class) + .useSiteTarget(FILE) + .build(), +) + +public fun TypeSpec.Builder.jvmSuppressWildcards(suppress: Boolean = true): TypeSpec.Builder = + addAnnotation(jvmSuppressWildcardsAnnotation(suppress)) + +private fun jvmSuppressWildcardsAnnotation(suppress: Boolean = true) = + AnnotationSpec.builder(JvmSuppressWildcards::class) + .apply { if (!suppress) addMember("suppress = false") } + .build() + +public fun TypeSpec.Builder.jvmInline(): TypeSpec.Builder = addAnnotation(JvmInline::class) + +public fun TypeSpec.Builder.jvmRecord(): TypeSpec.Builder = addAnnotation(JvmRecord::class) + +public fun FunSpec.Builder.jvmStatic(): FunSpec.Builder = apply { + check(!name.isConstructor) { "Can't apply @JvmStatic to a constructor!" } + addAnnotation(JvmStatic::class) +} + +public fun FunSpec.Builder.jvmOverloads(): FunSpec.Builder = apply { + check(!name.isAccessor) { + "Can't apply @JvmOverloads to a " + if (name == FunSpec.GETTER) "getter!" else "setter!" + } + addAnnotation(JvmOverloads::class) +} + +public fun FunSpec.Builder.jvmName(name: String): FunSpec.Builder = apply { + check(!this.name.isConstructor) { "Can't apply @JvmName to a constructor!" } + addAnnotation( + AnnotationSpec.builder(JvmName::class) + .addMember("%S", name) + .build(), + ) +} + +public fun FunSpec.Builder.throws(vararg exceptionClasses: KClass<out Throwable>): FunSpec.Builder = + throws(exceptionClasses.map(KClass<*>::asTypeName)) + +public fun FunSpec.Builder.throws(vararg exceptionClasses: Type): FunSpec.Builder = + throws(exceptionClasses.map(Type::asTypeName)) + +public fun FunSpec.Builder.throws(vararg exceptionClasses: TypeName): FunSpec.Builder = + throws(exceptionClasses.toList()) + +public fun FunSpec.Builder.throws(exceptionClasses: Iterable<TypeName>): FunSpec.Builder = + addAnnotation( + AnnotationSpec.builder(Throws::class) + .apply { exceptionClasses.forEach { addMember("%T::class", it) } } + .build(), + ) + +public fun FunSpec.Builder.jvmSuppressWildcards(suppress: Boolean = true): FunSpec.Builder = apply { + check(!name.isConstructor) { "Can't apply @JvmSuppressWildcards to a constructor!" } + check(!name.isAccessor) { + "Can't apply @JvmSuppressWildcards to a " + if (name == FunSpec.GETTER) "getter!" else "setter!" + } + addAnnotation(jvmSuppressWildcardsAnnotation(suppress)) +} + +public fun FunSpec.Builder.synchronized(): FunSpec.Builder = apply { + check(!name.isConstructor) { "Can't apply @Synchronized to a constructor!" } + addAnnotation(Synchronized::class) +} + +public fun FunSpec.Builder.strictfp(): FunSpec.Builder = addAnnotation(Strictfp::class) + +public fun PropertySpec.Builder.jvmField(): PropertySpec.Builder = addAnnotation(JvmField::class) + +public fun PropertySpec.Builder.jvmStatic(): PropertySpec.Builder = addAnnotation(JvmStatic::class) + +public fun PropertySpec.Builder.jvmSuppressWildcards( + suppress: Boolean = true, +): PropertySpec.Builder = addAnnotation(jvmSuppressWildcardsAnnotation(suppress)) + +public fun PropertySpec.Builder.transient(): PropertySpec.Builder = addAnnotation(Transient::class) + +public fun PropertySpec.Builder.volatile(): PropertySpec.Builder = addAnnotation(Volatile::class) + +public fun TypeName.jvmSuppressWildcards(suppress: Boolean = true): TypeName = + copy(annotations = this.annotations + jvmSuppressWildcardsAnnotation(suppress)) + +public fun TypeName.jvmWildcard(): TypeName = + copy(annotations = this.annotations + AnnotationSpec.builder(JvmWildcard::class).build()) + +@Suppress("DEPRECATION") +@Deprecated("'JvmDefault' is deprecated. Switch to new -Xjvm-default modes: `all` or `all-compatibility`") +public fun PropertySpec.Builder.jvmDefault(): PropertySpec.Builder = + addAnnotation(JvmDefault::class) + +@Suppress("DEPRECATION") +@Deprecated("'JvmDefault' is deprecated. Switch to new -Xjvm-default modes: `all` or `all-compatibility`") +public fun FunSpec.Builder.jvmDefault(): FunSpec.Builder = addAnnotation(JvmDefault::class) diff --git a/kotlinpoet/src/main/java/com/squareup/kotlinpoet/tags/TypeAliasTag.kt b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/tags/TypeAliasTag.kt new file mode 100644 index 00000000..22be037b --- /dev/null +++ b/kotlinpoet/src/main/java/com/squareup/kotlinpoet/tags/TypeAliasTag.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.tags + +import com.squareup.kotlinpoet.TypeName + +/** + * This tag indicates that this [TypeName] represents a `typealias` type. + * + * @property [abbreviatedType] the underlying type for this alias. + */ +public class TypeAliasTag(public val abbreviatedType: TypeName) diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/AbstractTypesTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/AbstractTypesTest.kt new file mode 100644 index 00000000..b78a96e8 --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/AbstractTypesTest.kt @@ -0,0 +1,226 @@ +/* + * Copyright (C) 2014 Google, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.truth.Truth.assertThat +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import java.io.Serializable +import java.nio.charset.Charset +import javax.lang.model.element.Element +import javax.lang.model.type.DeclaredType +import javax.lang.model.type.ErrorType +import javax.lang.model.type.TypeKind +import javax.lang.model.type.TypeMirror +import javax.lang.model.type.TypeVisitor +import javax.lang.model.util.Elements +import javax.lang.model.util.Types +import kotlin.reflect.KTypeProjection +import kotlin.reflect.KVariance +import kotlin.reflect.full.createType +import kotlin.reflect.full.declaredFunctions +import kotlin.reflect.full.starProjectedType +import kotlin.reflect.full.withNullability +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.fail + +abstract class AbstractTypesTest { + protected abstract val elements: Elements + protected abstract val types: Types + + private fun getElement(`class`: Class<*>) = elements.getTypeElement(`class`.canonicalName) + + private fun getMirror(`class`: Class<*>) = getElement(`class`).asType() + + @Test fun getBasicTypeMirror() { + assertThat(getMirror(Any::class.java).asTypeName()) + .isEqualTo(Any::class.java.asClassName()) + assertThat(getMirror(Charset::class.java).asTypeName()) + .isEqualTo(Charset::class.asClassName()) + assertThat(getMirror(AbstractTypesTest::class.java).asTypeName()) + .isEqualTo(AbstractTypesTest::class.asClassName()) + } + + @Test fun getParameterizedTypeMirror() { + val setType = types.getDeclaredType(getElement(Set::class.java), getMirror(String::class.java)) + assertThat(setType.asTypeName()) + .isEqualTo(Set::class.asClassName().parameterizedBy(String::class.asClassName())) + } + + @Test fun getErrorType() { + val errorType = DeclaredTypeAsErrorType(types.getDeclaredType(getElement(Set::class.java))) + assertThat(errorType.asTypeName()).isEqualTo(Set::class.asClassName()) + } + + internal class Parameterized< + Simple, + ExtendsClass : Number, + ExtendsInterface : Runnable, + ExtendsTypeVariable : Simple, + Intersection, + IntersectionOfInterfaces> + where IntersectionOfInterfaces : Runnable, Intersection : Number, Intersection : Runnable, + IntersectionOfInterfaces : Serializable + + @Test fun getTypeVariableTypeMirror() { + val typeVariables = getElement(Parameterized::class.java).typeParameters + + // Members of converted types use ClassName and not Class<?>. + val number = Number::class.asClassName() + val runnable = Runnable::class.asClassName() + val serializable = Serializable::class.asClassName() + + assertThat(typeVariables[0].asType().asTypeName()) + .isEqualTo(TypeVariableName("Simple")) + assertThat(typeVariables[1].asType().asTypeName()) + .isEqualTo(TypeVariableName("ExtendsClass", number)) + assertThat(typeVariables[2].asType().asTypeName()) + .isEqualTo(TypeVariableName("ExtendsInterface", runnable)) + assertThat(typeVariables[3].asType().asTypeName()) + .isEqualTo(TypeVariableName("ExtendsTypeVariable", TypeVariableName("Simple"))) + assertThat(typeVariables[4].asType().asTypeName()) + .isEqualTo(TypeVariableName("Intersection", number, runnable)) + assertThat(typeVariables[5].asType().asTypeName()) + .isEqualTo(TypeVariableName("IntersectionOfInterfaces", runnable, serializable)) + assertThat((typeVariables[4].asType().asTypeName() as TypeVariableName).bounds) + .containsExactly(number, runnable) + } + + internal class Recursive<T : Map<List<T>, Set<Array<T>>>> + + @Test fun getTypeVariableTypeMirrorRecursive() { + val typeMirror = getElement(Recursive::class.java).asType() + val typeName = typeMirror.asTypeName() as ParameterizedTypeName + val className = Recursive::class.java.canonicalName + assertThat(typeName.toString()).isEqualTo("$className<T>") + + val typeVariableName = typeName.typeArguments[0] as TypeVariableName + assertThat(typeVariableName.toString()).isEqualTo("T") + assertThat(typeVariableName.bounds.toString()) + .isEqualTo("[kotlin.collections.Map<kotlin.collections.List<out T>, out kotlin.collections.Set<out kotlin.Array<T>>>]") + } + + @Test fun getPrimitiveTypeMirror() { + assertThat(types.getPrimitiveType(TypeKind.BOOLEAN).asTypeName()).isEqualTo(BOOLEAN) + assertThat(types.getPrimitiveType(TypeKind.BYTE).asTypeName()).isEqualTo(BYTE) + assertThat(types.getPrimitiveType(TypeKind.SHORT).asTypeName()).isEqualTo(SHORT) + assertThat(types.getPrimitiveType(TypeKind.INT).asTypeName()).isEqualTo(INT) + assertThat(types.getPrimitiveType(TypeKind.LONG).asTypeName()).isEqualTo(LONG) + assertThat(types.getPrimitiveType(TypeKind.CHAR).asTypeName()).isEqualTo(CHAR) + assertThat(types.getPrimitiveType(TypeKind.FLOAT).asTypeName()).isEqualTo(FLOAT) + assertThat(types.getPrimitiveType(TypeKind.DOUBLE).asTypeName()).isEqualTo(DOUBLE) + } + + @Test fun getArrayTypeMirror() { + assertThat(types.getArrayType(getMirror(String::class.java)).asTypeName()) + .isEqualTo(ARRAY.parameterizedBy(String::class.asClassName())) + } + + @Test fun getVoidTypeMirror() { + assertThat(types.getNoType(TypeKind.VOID).asTypeName()).isEqualTo(UNIT) + } + + @Test fun getNullTypeMirror() { + try { + types.nullType.asTypeName() + fail() + } catch (expected: IllegalArgumentException) { + } + } + + @Test fun parameterizedType() { + val type = Map::class.parameterizedBy(String::class, Long::class) + assertThat(type.toString()).isEqualTo("kotlin.collections.Map<kotlin.String, kotlin.Long>") + } + + @Test fun starProjection() { + assertThat(STAR.toString()).isEqualTo("*") + } + + @Ignore("Figure out what this maps to in Kotlin.") + @Test fun starProjectionFromMirror() { + val wildcard = types.getWildcardType(null, null) + val type = wildcard.asTypeName() + assertThat(type.toString()).isEqualTo("*") + } + + @Test fun varianceOutType() { + val type = WildcardTypeName.producerOf(CharSequence::class) + assertThat(type.toString()).isEqualTo("out java.lang.CharSequence") + } + + @Test fun varianceOutTypeFromMirror() { + val types = types + val elements = elements + val charSequence = elements.getTypeElement(CharSequence::class.java.name).asType() + val wildcard = types.getWildcardType(charSequence, null) + val type = wildcard.asTypeName() + assertThat(type.toString()).isEqualTo("out java.lang.CharSequence") + } + + @Test fun varianceInType() { + val type = WildcardTypeName.consumerOf(String::class) + assertThat(type.toString()).isEqualTo("in kotlin.String") + } + + @Test fun varianceInTypeFromMirror() { + val types = types + val elements = elements + val string = elements.getTypeElement(String::class.java.name).asType() + val wildcard = types.getWildcardType(null, string) + val type = wildcard.asTypeName() + assertThat(type.toString()).isEqualTo("in kotlin.String") + } + + @Test fun typeVariable() { + val type = TypeVariableName("T", CharSequence::class) + assertThat(type.toString()).isEqualTo("T") // (Bounds are only emitted in declaration.) + } + + @Test fun kType() { + assertThat(Map::class.starProjectedType.asTypeName().toString()) + .isEqualTo("kotlin.collections.Map<*, *>") + assertThat(Map::class.createType(listOf(KTypeProjection(KVariance.INVARIANT, String::class.createType(emptyList())), KTypeProjection.STAR)).asTypeName().toString()) + .isEqualTo("kotlin.collections.Map<kotlin.String, *>") + assertThat(Map.Entry::class.createType(listOf(KTypeProjection(KVariance.INVARIANT, String::class.createType(emptyList())), KTypeProjection.STAR)).asTypeName().toString()) + .isEqualTo("kotlin.collections.Map.Entry<kotlin.String, *>") + assertThat(Any::class.starProjectedType.withNullability(true).asTypeName().toString()) + .isEqualTo("kotlin.Any?") + + val treeMapClass = java.util.TreeMap::class + assertThat(treeMapClass.declaredFunctions.find { it.name == "parentOf" }!!.returnType.asTypeName().toString()) + .isEqualTo("java.util.TreeMap.Entry<K, V>") + } + + private class DeclaredTypeAsErrorType(private val declaredType: DeclaredType) : ErrorType { + override fun asElement(): Element = declaredType.asElement() + + override fun getEnclosingType(): TypeMirror = declaredType.enclosingType + + override fun getTypeArguments(): MutableList<out TypeMirror> = declaredType.typeArguments + + override fun getKind(): TypeKind = declaredType.kind + + override fun <R, P> accept(typeVisitor: TypeVisitor<R, P>, p: P): R = typeVisitor.visitError(this, p) + + override fun <A : Annotation> getAnnotationsByType(annotationType: Class<A>): Array<A> = + throw UnsupportedOperationException() + + override fun <A : Annotation> getAnnotation(annotationType: Class<A>): A = throw UnsupportedOperationException() + + override fun getAnnotationMirrors() = throw UnsupportedOperationException() + } +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/AnnotatedTypeNameTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/AnnotatedTypeNameTest.kt new file mode 100644 index 00000000..3e7b766c --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/AnnotatedTypeNameTest.kt @@ -0,0 +1,140 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFalse +import kotlin.test.assertNotEquals +import kotlin.test.assertTrue + +class AnnotatedTypeNameTest { + private val NEVER_NULL = AnnotationSpec.builder(NeverNull::class).build() + private val NN = NeverNull::class.java.canonicalName + + annotation class NeverNull + + @Test fun annotated() { + val simpleString = String::class.asTypeName() + assertFalse(simpleString.isAnnotated) + assertEquals(simpleString, String::class.asTypeName()) + val annotated = simpleString.copy(annotations = simpleString.annotations + NEVER_NULL) + assertTrue(annotated.isAnnotated) + } + + @Test fun annotatedType() { + val expected = "@$NN kotlin.String" + val type = String::class.asTypeName() + val actual = type.copy(annotations = type.annotations + NEVER_NULL).toString() + assertEquals(expected, actual) + } + + @Test fun annotatedTwice() { + val expected = "@$NN @java.lang.Override kotlin.String" + val type = String::class.asTypeName() + val actual = type + .copy( + annotations = type.annotations + NEVER_NULL + + AnnotationSpec.builder(Override::class).build(), + ) + .toString() + assertEquals(expected, actual) + } + + @Test fun annotatedParameterizedType() { + val expected = "@$NN kotlin.collections.List<kotlin.String>" + val type = List::class.parameterizedBy(String::class) + val actual = type.copy(annotations = type.annotations + NEVER_NULL).toString() + assertEquals(expected, actual) + } + + @Test fun annotatedArgumentOfParameterizedType() { + val expected = "kotlin.collections.List<@$NN kotlin.String>" + val type = String::class.asTypeName().copy(annotations = listOf(NEVER_NULL)) + val list = List::class.asClassName() + val actual = list.parameterizedBy(type).toString() + assertEquals(expected, actual) + } + + @Test fun annotatedWildcardTypeNameWithSuper() { + val expected = "in @$NN kotlin.String" + val type = String::class.asTypeName().copy(annotations = listOf(NEVER_NULL)) + val actual = WildcardTypeName.consumerOf(type).toString() + assertEquals(expected, actual) + } + + @Test fun annotatedWildcardTypeNameWithExtends() { + val expected = "out @$NN kotlin.String" + val type = String::class.asTypeName().copy(annotations = listOf(NEVER_NULL)) + val actual = WildcardTypeName.producerOf(type).toString() + assertEquals(expected, actual) + } + + @Test fun annotatedEquivalence() { + annotatedEquivalence(UNIT) + annotatedEquivalence(Any::class.asClassName()) + annotatedEquivalence(List::class.parameterizedBy(Any::class)) + annotatedEquivalence(TypeVariableName("A")) + annotatedEquivalence(WildcardTypeName.producerOf(Object::class)) + } + + private fun annotatedEquivalence(type: TypeName) { + assertFalse(type.isAnnotated) + assertEquals(type, type) + assertEquals( + type.copy(annotations = listOf(NEVER_NULL)), + type.copy(annotations = listOf(NEVER_NULL)), + ) + assertNotEquals(type, type.copy(annotations = listOf(NEVER_NULL))) + assertEquals(type.hashCode().toLong(), type.hashCode().toLong()) + assertEquals( + type.copy(annotations = listOf(NEVER_NULL)).hashCode().toLong(), + type.copy(annotations = listOf(NEVER_NULL)).hashCode().toLong(), + ) + assertNotEquals( + type.hashCode().toLong(), + type.copy(annotations = listOf(NEVER_NULL)).hashCode().toLong(), + ) + } + + // https://github.com/square/javapoet/issues/431 + // @Target(ElementType.TYPE_USE) requires Java 1.8 + annotation class TypeUseAnnotation + + // https://github.com/square/javapoet/issues/431 + @Ignore @Test + fun annotatedNestedType() { + val expected = "kotlin.collections.Map.@" + TypeUseAnnotation::class.java.canonicalName + " Entry" + val typeUseAnnotation = AnnotationSpec.builder(TypeUseAnnotation::class).build() + val type = Map.Entry::class.asTypeName().copy(annotations = listOf(typeUseAnnotation)) + val actual = type.toString() + assertEquals(expected, actual) + } + + // https://github.com/square/javapoet/issues/431 + @Ignore @Test + fun annotatedNestedParameterizedType() { + val expected = "kotlin.collections.Map.@" + TypeUseAnnotation::class.java.canonicalName + + " Entry<kotlin.Byte, kotlin.Byte>" + val typeUseAnnotation = AnnotationSpec.builder(TypeUseAnnotation::class).build() + val type = Map.Entry::class.parameterizedBy(Byte::class, Byte::class) + .copy(annotations = listOf(typeUseAnnotation)) + val actual = type.toString() + assertEquals(expected, actual) + } +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/AnnotationSpecTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/AnnotationSpecTest.kt new file mode 100644 index 00000000..4f770406 --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/AnnotationSpecTest.kt @@ -0,0 +1,601 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.truth.Truth.assertThat +import com.google.testing.compile.CompilationRule +import com.squareup.kotlinpoet.KModifier.OVERRIDE +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.plusParameter +import java.lang.annotation.Inherited +import kotlin.reflect.KClass +import kotlin.test.Test +import org.junit.Rule + +class AnnotationSpecTest { + + @Retention(AnnotationRetention.RUNTIME) + annotation class AnnotationA + + @Inherited + @Retention(AnnotationRetention.RUNTIME) + annotation class AnnotationB + + @Retention(AnnotationRetention.RUNTIME) + annotation class AnnotationC(val value: String) + + enum class Breakfast { + WAFFLES, PANCAKES; + + override fun toString(): String { + return "$name with cherries!" + } + } + + @Retention(AnnotationRetention.RUNTIME) + annotation class HasDefaultsAnnotation( + val a: Byte = 5, + val b: Short = 6, + val c: Int = 7, + val d: Long = 8, + val e: Float = 9.0f, + val f: Double = 10.0, + val g: CharArray = ['\u0000', '\uCAFE', 'z', '€', 'ℕ', '"', '\'', '\t', '\n'], + val h: Boolean = true, + val i: Breakfast = Breakfast.WAFFLES, + val j: AnnotationA = AnnotationA(), + val k: String = "maple", + val l: KClass<out Annotation> = AnnotationB::class, + val m: IntArray = [1, 2, 3], + val n: Array<Breakfast> = [Breakfast.WAFFLES, Breakfast.PANCAKES], + val o: Breakfast, + val p: Int, + val q: AnnotationC = AnnotationC("foo"), + val r: Array<KClass<out Number>> = [Byte::class, Short::class, Int::class, Long::class], + ) + + @HasDefaultsAnnotation( + o = Breakfast.PANCAKES, + p = 1701, + f = 11.1, + m = [9, 8, 1], + l = Override::class, + j = AnnotationA(), + q = AnnotationC("bar"), + r = [Float::class, Double::class], + ) + inner class IsAnnotated + + @Rule @JvmField + val compilation = CompilationRule() + + @Test fun equalsAndHashCode() { + var a = AnnotationSpec.builder(AnnotationC::class.java).build() + var b = AnnotationSpec.builder(AnnotationC::class.java).build() + assertThat(a == b).isTrue() + assertThat(a.hashCode()).isEqualTo(b.hashCode()) + a = AnnotationSpec.builder(AnnotationC::class.java).addMember("value", "%S", "123").build() + b = AnnotationSpec.builder(AnnotationC::class.java).addMember("value", "%S", "123").build() + assertThat(a == b).isTrue() + assertThat(a.hashCode()).isEqualTo(b.hashCode()) + } + + @Test fun defaultAnnotation() { + val name = IsAnnotated::class.java.canonicalName + val element = compilation.elements.getTypeElement(name) + val annotation = AnnotationSpec.get(element.annotationMirrors[0]) + + assertThat(toString(annotation)).isEqualTo( + """ + |package com.squareup.tacos + | + |import com.squareup.kotlinpoet.AnnotationSpecTest + |import java.lang.Override + |import kotlin.Double + |import kotlin.Float + | + |@AnnotationSpecTest.HasDefaultsAnnotation( + | f = 11.1, + | j = AnnotationSpecTest.AnnotationA(), + | l = Override::class, + | m = arrayOf(9, 8, 1), + | o = AnnotationSpecTest.Breakfast.PANCAKES, + | p = 1_701, + | q = AnnotationSpecTest.AnnotationC(value = "bar"), + | r = arrayOf(Float::class, Double::class), + |) + |public class Taco + | + """.trimMargin(), + ) + } + + @Test fun defaultAnnotationWithImport() { + val name = IsAnnotated::class.java.canonicalName + val element = compilation.elements.getTypeElement(name) + val annotation = AnnotationSpec.get(element.annotationMirrors[0]) + val typeBuilder = TypeSpec.classBuilder(IsAnnotated::class.java.simpleName) + typeBuilder.addAnnotation(annotation) + val file = FileSpec.get("com.squareup.kotlinpoet", typeBuilder.build()) + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.kotlinpoet + | + |import java.lang.Override + |import kotlin.Double + |import kotlin.Float + | + |@AnnotationSpecTest.HasDefaultsAnnotation( + | f = 11.1, + | j = AnnotationSpecTest.AnnotationA(), + | l = Override::class, + | m = arrayOf(9, 8, 1), + | o = AnnotationSpecTest.Breakfast.PANCAKES, + | p = 1_701, + | q = AnnotationSpecTest.AnnotationC(value = "bar"), + | r = arrayOf(Float::class, Double::class), + |) + |public class IsAnnotated + | + """.trimMargin(), + ) + } + + @Test fun emptyArray() { + val builder = AnnotationSpec.builder(HasDefaultsAnnotation::class.java) + builder.addMember("%L = %L", "n", "[]") + assertThat(builder.build().toString()).isEqualTo( + "" + + "@com.squareup.kotlinpoet.AnnotationSpecTest.HasDefaultsAnnotation(" + + "n = []" + + ")", + ) + builder.addMember("%L = %L", "m", "[]") + assertThat(builder.build().toString()).isEqualTo( + "" + + "@com.squareup.kotlinpoet.AnnotationSpecTest.HasDefaultsAnnotation(" + + "n = [], " + + "m = []" + + ")", + ) + } + + @Test fun reflectAnnotation() { + val annotation = IsAnnotated::class.java.getAnnotation(HasDefaultsAnnotation::class.java) + val spec = AnnotationSpec.get(annotation) + + assertThat(toString(spec)).isEqualTo( + """ + |package com.squareup.tacos + | + |import com.squareup.kotlinpoet.AnnotationSpecTest + |import java.lang.Override + |import kotlin.Double + |import kotlin.Float + | + |@AnnotationSpecTest.HasDefaultsAnnotation( + | f = 11.1, + | l = Override::class, + | m = arrayOf(9, 8, 1), + | o = AnnotationSpecTest.Breakfast.PANCAKES, + | p = 1_701, + | q = AnnotationSpecTest.AnnotationC(value = "bar"), + | r = arrayOf(Float::class, Double::class), + |) + |public class Taco + | + """.trimMargin(), + ) + } + + @Test fun reflectAnnotationWithDefaults() { + val annotation = IsAnnotated::class.java.getAnnotation(HasDefaultsAnnotation::class.java) + val spec = AnnotationSpec.get(annotation, true) + + assertThat(toString(spec)).isEqualTo( + """ + |package com.squareup.tacos + | + |import com.squareup.kotlinpoet.AnnotationSpecTest + |import java.lang.Override + |import kotlin.Double + |import kotlin.Float + | + |@AnnotationSpecTest.HasDefaultsAnnotation( + | a = 5, + | b = 6, + | c = 7, + | d = 8, + | e = 9.0f, + | f = 11.1, + | g = arrayOf('\u0000', '쫾', 'z', '€', 'ℕ', '"', '\'', '\t', '\n'), + | h = true, + | i = AnnotationSpecTest.Breakfast.WAFFLES, + | j = AnnotationSpecTest.AnnotationA(), + | k = "maple", + | l = Override::class, + | m = arrayOf(9, 8, 1), + | n = arrayOf(AnnotationSpecTest.Breakfast.WAFFLES, AnnotationSpecTest.Breakfast.PANCAKES), + | o = AnnotationSpecTest.Breakfast.PANCAKES, + | p = 1_701, + | q = AnnotationSpecTest.AnnotationC(value = "bar"), + | r = arrayOf(Float::class, Double::class), + |) + |public class Taco + | + """.trimMargin(), + ) + } + + @Test fun useSiteTarget() { + val builder = AnnotationSpec.builder(AnnotationA::class) + assertThat(builder.build().toString()).isEqualTo( + "" + + "@com.squareup.kotlinpoet.AnnotationSpecTest.AnnotationA", + ) + builder.useSiteTarget(AnnotationSpec.UseSiteTarget.FIELD) + assertThat(builder.build().toString()).isEqualTo( + "" + + "@field:com.squareup.kotlinpoet.AnnotationSpecTest.AnnotationA", + ) + builder.useSiteTarget(AnnotationSpec.UseSiteTarget.GET) + assertThat(builder.build().toString()).isEqualTo( + "" + + "@get:com.squareup.kotlinpoet.AnnotationSpecTest.AnnotationA", + ) + builder.useSiteTarget(null) + assertThat(builder.build().toString()).isEqualTo( + "" + + "@com.squareup.kotlinpoet.AnnotationSpecTest.AnnotationA", + ) + } + + @Test fun deprecatedTest() { + val annotation = AnnotationSpec.builder(Deprecated::class) + .addMember("%S", "Nope") + .addMember("%T(%S)", ReplaceWith::class, "Yep") + .build() + + assertThat(annotation.toString()).isEqualTo( + "" + + "@kotlin.Deprecated(\"Nope\", kotlin.ReplaceWith(\"Yep\"))", + ) + } + + @Test fun modifyMembers() { + val builder = AnnotationSpec.builder(Deprecated::class) + .addMember("%S", "Nope") + .addMember("%T(%S)", ReplaceWith::class, "Yep") + + builder.members.removeAt(1) + builder.members.add(CodeBlock.of("%T(%S)", ReplaceWith::class, "Nope")) + + assertThat(builder.build().toString()).isEqualTo( + "" + + "@kotlin.Deprecated(\"Nope\", kotlin.ReplaceWith(\"Nope\"))", + ) + } + + @Test fun annotationStringsAreConstant() { + val text = "This is a long string with a newline\nin the middle." + val builder = AnnotationSpec.builder(Deprecated::class) + .addMember("%S", text) + + assertThat(builder.build().toString()).isEqualTo( + "" + + "@kotlin.Deprecated(\"This is a long string with a newline\\nin the middle.\")", + ) + } + + @Test fun literalAnnotation() { + val annotationSpec = AnnotationSpec.builder(Suppress::class) + .addMember("%S", "Things") + .build() + + val file = FileSpec.builder("test", "Test") + .addFunction( + FunSpec.builder("test") + .addStatement("%L", annotationSpec) + .addStatement("val annotatedString = %S", "AnnotatedString") + .build(), + ) + .build() + assertThat(file.toString().trim()).isEqualTo( + """ + |package test + | + |import kotlin.Suppress + |import kotlin.Unit + | + |public fun test(): Unit { + | @Suppress("Things") + | val annotatedString = "AnnotatedString" + |} + """.trimMargin(), + ) + } + + @Test fun functionOnlyLiteralAnnotation() { + val annotation = AnnotationSpec + .builder(ClassName.bestGuess("Suppress")) + .addMember("%S", "UNCHECKED_CAST") + .build() + val funSpec = FunSpec.builder("operation") + .addStatement("%L", annotation) + .build() + + assertThat(funSpec.toString().trim()).isEqualTo( + """ + |public fun operation(): kotlin.Unit { + | @Suppress("UNCHECKED_CAST") + |} + """.trimMargin(), + ) + } + + @Test fun getOnValueArrayTypeMirrorShouldNameValueArg() { + val myClazz = compilation.elements + .getTypeElement(JavaClassWithArrayValueAnnotation::class.java.canonicalName) + val classBuilder = TypeSpec.classBuilder("Result") + + myClazz.annotationMirrors.map { AnnotationSpec.get(it) } + .forEach { + classBuilder.addAnnotation(it) + } + + assertThat(toString(classBuilder.build())).isEqualTo( + """ + |package com.squareup.tacos + | + |import com.squareup.kotlinpoet.JavaClassWithArrayValueAnnotation + |import java.lang.Boolean + |import java.lang.Object + | + |@JavaClassWithArrayValueAnnotation.AnnotationWithArrayValue(value = arrayOf(Object::class, + | Boolean::class)) + |public class Result + | + """.trimMargin(), + ) + } + + @Test fun getOnVarargMirrorShouldNameValueArg() { + val myClazz = compilation.elements + .getTypeElement(KotlinClassWithVarargAnnotation::class.java.canonicalName) + val classBuilder = TypeSpec.classBuilder("Result") + + myClazz.annotationMirrors.map { AnnotationSpec.get(it) } + .filter { + val typeName = it.typeName + return@filter typeName is ClassName && typeName.simpleName == "AnnotationWithArrayValue" + } + .forEach { + classBuilder.addAnnotation(it) + } + + assertThat(toString(classBuilder.build()).trim()).isEqualTo( + """ + |package com.squareup.tacos + | + |import com.squareup.kotlinpoet.AnnotationSpecTest + |import java.lang.Object + |import kotlin.Boolean + | + |@AnnotationSpecTest.AnnotationWithArrayValue(value = arrayOf(Object::class, Boolean::class)) + |public class Result + """.trimMargin(), + ) + } + + @Test fun getOnValueArrayTypeAnnotationShouldNameValueArg() { + val annotation = JavaClassWithArrayValueAnnotation::class.java.getAnnotation( + JavaClassWithArrayValueAnnotation.AnnotationWithArrayValue::class.java, + ) + val classBuilder = TypeSpec.classBuilder("Result") + .addAnnotation(AnnotationSpec.get(annotation)) + + assertThat(toString(classBuilder.build()).trim()).isEqualTo( + """ + |package com.squareup.tacos + | + |import com.squareup.kotlinpoet.JavaClassWithArrayValueAnnotation + |import java.lang.Boolean + |import java.lang.Object + | + |@JavaClassWithArrayValueAnnotation.AnnotationWithArrayValue(value = arrayOf(Object::class, + | Boolean::class)) + |public class Result + """.trimMargin(), + ) + } + + @Test fun getOnVarargAnnotationShouldNameValueArg() { + val annotation = KotlinClassWithVarargAnnotation::class.java + .getAnnotation(AnnotationWithArrayValue::class.java) + val classBuilder = TypeSpec.classBuilder("Result") + .addAnnotation(AnnotationSpec.get(annotation)) + + assertThat(toString(classBuilder.build()).trim()).isEqualTo( + """ + |package com.squareup.tacos + | + |import com.squareup.kotlinpoet.AnnotationSpecTest + |import java.lang.Object + |import kotlin.Boolean + | + |@AnnotationSpecTest.AnnotationWithArrayValue(value = arrayOf(Object::class, Boolean::class)) + |public class Result + """.trimMargin(), + ) + } + + @AnnotationWithArrayValue(Any::class, Boolean::class) + class KotlinClassWithVarargAnnotation + + @Retention(AnnotationRetention.RUNTIME) + internal annotation class AnnotationWithArrayValue(vararg val value: KClass<*>) + + @Test fun annotationsWithTypeParameters() { + // Example from https://kotlinlang.org/docs/tutorials/android-plugin.html + val externalClass = ClassName("com.squareup.parceler", "ExternalClass") + val externalClassSpec = TypeSpec.classBuilder(externalClass) + .addProperty( + PropertySpec.builder("value", Int::class) + .initializer("value") + .build(), + ) + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("value", Int::class) + .build(), + ) + .build() + val externalClassParceler = ClassName("com.squareup.parceler", "ExternalClassParceler") + val parcel = ClassName("com.squareup.parceler", "Parcel") + val externalClassParcelerSpec = TypeSpec.objectBuilder(externalClassParceler) + .addSuperinterface( + ClassName("com.squareup.parceler", "Parceler") + .parameterizedBy(externalClass), + ) + .addFunction( + FunSpec.builder("create") + .addModifiers(OVERRIDE) + .addParameter("parcel", parcel) + .addStatement("return %T(parcel.readInt())", externalClass) + .build(), + ) + .addFunction( + FunSpec.builder("write") + .addModifiers(OVERRIDE) + .receiver(externalClass) + .addParameter("parcel", parcel) + .addParameter("flags", Int::class) + .addStatement("parcel.writeInt(value)") + .build(), + ) + .build() + val parcelize = ClassName("com.squareup.parceler", "Parcelize") + val typeParceler = ClassName("com.squareup.parceler", "TypeParceler") + val typeParcelerAnnotation = AnnotationSpec.builder( + typeParceler + .plusParameter(externalClass) + .plusParameter(externalClassParceler), + ) + .build() + val classLocalParceler = TypeSpec.classBuilder("MyClass") + .addAnnotation(parcelize) + .addAnnotation(typeParcelerAnnotation) + .addProperty( + PropertySpec.builder("external", externalClass) + .initializer("external") + .build(), + ) + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("external", externalClass) + .build(), + ) + .build() + val propertyLocalParceler = TypeSpec.classBuilder("MyClass") + .addAnnotation(parcelize) + .addProperty( + PropertySpec.builder("external", externalClass) + .addAnnotation(typeParcelerAnnotation) + .initializer("external") + .build(), + ) + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("external", externalClass) + .build(), + ) + .build() + val writeWith = ClassName("com.squareup.parceler", "WriteWith") + val writeWithExternalClass = externalClass + .copy( + annotations = listOf( + AnnotationSpec + .builder(writeWith.plusParameter(externalClassParceler)) + .build(), + ), + ) + val typeLocalParceler = TypeSpec.classBuilder("MyClass") + .addAnnotation(parcelize) + .addProperty( + PropertySpec.builder("external", writeWithExternalClass) + .initializer("external") + .build(), + ) + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("external", writeWithExternalClass) + .build(), + ) + .build() + val file = FileSpec.builder("com.squareup.parceler", "Test") + .addType(externalClassSpec) + .addType(externalClassParcelerSpec) + .addType(classLocalParceler) + .addType(propertyLocalParceler) + .addType(typeLocalParceler) + .build() + //language=kotlin + assertThat(file.toString()).isEqualTo( + """ + package com.squareup.parceler + + import kotlin.Int + import kotlin.Unit + + public class ExternalClass( + public val `value`: Int, + ) + + public object ExternalClassParceler : Parceler<ExternalClass> { + public override fun create(parcel: Parcel) = ExternalClass(parcel.readInt()) + + public override fun ExternalClass.write(parcel: Parcel, flags: Int): Unit { + parcel.writeInt(value) + } + } + + @Parcelize + @TypeParceler<ExternalClass, ExternalClassParceler> + public class MyClass( + public val `external`: ExternalClass, + ) + + @Parcelize + public class MyClass( + @TypeParceler<ExternalClass, ExternalClassParceler> + public val `external`: ExternalClass, + ) + + @Parcelize + public class MyClass( + public val `external`: @WriteWith<ExternalClassParceler> ExternalClass, + ) + + """.trimIndent(), + ) + } + + private fun toString(annotationSpec: AnnotationSpec) = + toString(TypeSpec.classBuilder("Taco").addAnnotation(annotationSpec).build()) + + private fun toString(typeSpec: TypeSpec) = + FileSpec.get("com.squareup.tacos", typeSpec).toString() +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/AssertThrows.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/AssertThrows.kt new file mode 100644 index 00000000..facd7022 --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/AssertThrows.kt @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2017 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.truth.ThrowableSubject +import com.google.common.truth.Truth.assertThat + +inline fun <reified T> assertThrows(block: () -> Unit): ThrowableSubject { + try { + block() + } catch (e: Throwable) { + if (e is T) { + return assertThat(e) + } else { + throw e + } + } + throw AssertionError("Expected ${T::class.simpleName}") +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/ClassNameTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/ClassNameTest.kt new file mode 100644 index 00000000..6d06806c --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/ClassNameTest.kt @@ -0,0 +1,223 @@ +/* + * Copyright (C) 2014 Google, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.truth.Truth.assertThat +import com.google.testing.compile.CompilationRule +import org.junit.Rule +import kotlin.test.Test +import kotlin.test.assertEquals + +class ClassNameTest { + @Rule @JvmField var compilationRule = CompilationRule() + + @Test fun bestGuessForString_simpleClass() { + assertThat(ClassName.bestGuess(String::class.java.name)) + .isEqualTo(ClassName("java.lang", "String")) + } + + @Test fun bestGuessNonAscii() { + val className = ClassName.bestGuess( + "com.\ud835\udc1andro\ud835\udc22d.\ud835\udc00ctiv\ud835\udc22ty" + ) + assertEquals("com.\ud835\udc1andro\ud835\udc22d", className.packageName) + assertEquals("\ud835\udc00ctiv\ud835\udc22ty", className.simpleName) + } + + internal class OuterClass { + internal class InnerClass + } + + @Test fun bestGuessForString_nestedClass() { + assertThat(ClassName.bestGuess(Map.Entry::class.java.canonicalName)) + .isEqualTo(ClassName("java.util", "Map", "Entry")) + assertThat(ClassName.bestGuess(OuterClass.InnerClass::class.java.canonicalName)) + .isEqualTo( + ClassName( + "com.squareup.kotlinpoet", + "ClassNameTest", "OuterClass", "InnerClass" + ) + ) + } + + @Test fun bestGuessForString_defaultPackage() { + assertThat(ClassName.bestGuess("SomeClass")) + .isEqualTo(ClassName("", "SomeClass")) + assertThat(ClassName.bestGuess("SomeClass.Nested")) + .isEqualTo(ClassName("", "SomeClass", "Nested")) + assertThat(ClassName.bestGuess("SomeClass.Nested.EvenMore")) + .isEqualTo(ClassName("", "SomeClass", "Nested", "EvenMore")) + } + + @Test fun bestGuessForString_confusingInput() { + assertBestGuessThrows("") + assertBestGuessThrows(".") + assertBestGuessThrows(".Map") + assertBestGuessThrows("java") + assertBestGuessThrows("java.util") + assertBestGuessThrows("java.util.") + assertBestGuessThrows("java..util.Map.Entry") + assertBestGuessThrows("java.util..Map.Entry") + assertBestGuessThrows("kotlin.collections.Map..Entry") + assertBestGuessThrows("com.test.$") + assertBestGuessThrows("com.test.LooksLikeAClass.pkg") + assertBestGuessThrows("!@#\$gibberish%^&*") + } + + private fun assertBestGuessThrows(s: String) { + assertThrows<IllegalArgumentException> { + ClassName.bestGuess(s) + } + } + + @Test fun createNestedClass() { + val foo = ClassName("com.example", "Foo") + val bar = foo.nestedClass("Bar") + assertThat(bar).isEqualTo(ClassName("com.example", "Foo", "Bar")) + val baz = bar.nestedClass("Baz") + assertThat(baz).isEqualTo(ClassName("com.example", "Foo", "Bar", "Baz")) + } + + @Test fun classNameFromTypeElement() { + val elements = compilationRule.elements + val element = elements.getTypeElement(Any::class.java.canonicalName) + assertThat(element.asClassName().toString()).isEqualTo("java.lang.Object") + } + + @Test fun classNameFromClass() { + assertThat(Any::class.java.asClassName().toString()) + .isEqualTo("java.lang.Object") + assertThat(OuterClass.InnerClass::class.java.asClassName().toString()) + .isEqualTo("com.squareup.kotlinpoet.ClassNameTest.OuterClass.InnerClass") + } + + @Test fun classNameFromKClass() { + assertThat(Any::class.asClassName().toString()) + .isEqualTo("kotlin.Any") + assertThat(OuterClass.InnerClass::class.asClassName().toString()) + .isEqualTo("com.squareup.kotlinpoet.ClassNameTest.OuterClass.InnerClass") + } + + @Test fun peerClass() { + assertThat(java.lang.Double::class.asClassName().peerClass("Short")) + .isEqualTo(java.lang.Short::class.asClassName()) + assertThat(ClassName("", "Double").peerClass("Short")) + .isEqualTo(ClassName("", "Short")) + assertThat(ClassName("a.b", "Combo", "Taco").peerClass("Burrito")) + .isEqualTo(ClassName("a.b", "Combo", "Burrito")) + } + + @Test fun fromClassRejectionTypes() { + assertThrows<IllegalArgumentException> { + java.lang.Integer.TYPE.asClassName() + } + + assertThrows<IllegalArgumentException> { + Void.TYPE.asClassName() + } + + assertThrows<IllegalArgumentException> { + Array<Any>::class.java.asClassName() + } + + // TODO + // assertThrows<IllegalArgumentException> { + // Array<Int>::class.asClassName() + // } + } + + @Suppress("DEPRECATION_ERROR") // Ensure still throws in case called from Java. + @Test fun fromEmptySimpleName() { + assertThrows<IllegalArgumentException> { + ClassName("foo" /* no simple name */) + } + } + + @Test fun reflectionName() { + assertThat(ANY.reflectionName()) + .isEqualTo("kotlin.Any") + assertThat(Thread.State::class.asClassName().reflectionName()) + .isEqualTo("java.lang.Thread\$State") + assertThat(Map.Entry::class.asClassName().reflectionName()) + .isEqualTo("kotlin.collections.Map\$Entry") + assertThat(ClassName("", "Foo").reflectionName()) + .isEqualTo("Foo") + assertThat(ClassName("", "Foo", "Bar", "Baz").reflectionName()) + .isEqualTo("Foo\$Bar\$Baz") + assertThat(ClassName("a.b.c", "Foo", "Bar", "Baz").reflectionName()) + .isEqualTo("a.b.c.Foo\$Bar\$Baz") + } + + @Test fun constructorReferences() { + assertThat(String::class.asClassName().constructorReference().toString()) + .isEqualTo("::kotlin.String") + assertThat(Thread.State::class.asClassName().constructorReference().toString()) + .isEqualTo("java.lang.Thread::State") + assertThat(ClassName("", "Foo").constructorReference().toString()) + .isEqualTo("::Foo") + assertThat(ClassName("", "Foo", "Bar", "Baz").constructorReference().toString()) + .isEqualTo("Foo.Bar::Baz") + assertThat(ClassName("a.b.c", "Foo", "Bar", "Baz").constructorReference().toString()) + .isEqualTo("a.b.c.Foo.Bar::Baz") + } + + @Test fun spacesEscaping() { + val tacoFactory = ClassName("com.squareup.taco factory", "Taco Factory") + val file = FileSpec.builder("com.squareup.tacos", "TacoTest") + .addFunction( + FunSpec.builder("main") + .addStatement("println(%T.produceTacos())", tacoFactory) + .build() + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import com.squareup.`taco factory`.`Taco Factory` + |import kotlin.Unit + | + |public fun main(): Unit { + | println(`Taco Factory`.produceTacos()) + |} + |""".trimMargin() + ) + } + + @Test fun emptySimpleNamesForbidden() { + assertThrows<IllegalArgumentException> { + ClassName(packageName = "", simpleNames = emptyArray()) + }.hasMessageThat().isEqualTo("simpleNames must not be empty") + + assertThrows<IllegalArgumentException> { + ClassName(packageName = "", simpleNames = arrayOf("Foo", "Bar", "")) + }.hasMessageThat().isEqualTo( + "simpleNames must not contain empty items: " + + "[Foo, Bar, ]" + ) + + assertThrows<IllegalArgumentException> { + ClassName(packageName = "", simpleNames = emptyList()) + }.hasMessageThat().isEqualTo("simpleNames must not be empty") + + assertThrows<IllegalArgumentException> { + ClassName(packageName = "", simpleNames = listOf("Foo", "Bar", "")) + }.hasMessageThat().isEqualTo( + "simpleNames must not contain empty items: " + + "[Foo, Bar, ]" + ) + } +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/CodeBlockTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/CodeBlockTest.kt new file mode 100644 index 00000000..23ef0568 --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/CodeBlockTest.kt @@ -0,0 +1,632 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.truth.Truth.assertThat +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import kotlin.test.Test + +class CodeBlockTest { + @Test fun equalsAndHashCode() { + var a = CodeBlock.builder().build() + var b = CodeBlock.builder().build() + assertThat(a == b).isTrue() + assertThat(a.hashCode()).isEqualTo(b.hashCode()) + a = CodeBlock.builder().add("%L", "taco").build() + b = CodeBlock.builder().add("%L", "taco").build() + assertThat(a == b).isTrue() + assertThat(a.hashCode()).isEqualTo(b.hashCode()) + } + + @Test fun of() { + val a = CodeBlock.of("%L taco", "delicious") + assertThat(a.toString()).isEqualTo("delicious taco") + } + + @Test fun percentEscapeCannotBeIndexed() { + assertThrows<IllegalArgumentException> { + CodeBlock.builder().add("%1%", "taco").build() + }.hasMessageThat().isEqualTo("%% may not have an index") + } + + @Test fun nameFormatCanBeIndexed() { + val block = CodeBlock.builder().add("%1N", "taco").build() + assertThat(block.toString()).isEqualTo("taco") + } + + @Test fun literalFormatCanBeIndexed() { + val block = CodeBlock.builder().add("%1L", "taco").build() + assertThat(block.toString()).isEqualTo("taco") + } + + @Test fun stringFormatCanBeIndexed() { + val block = CodeBlock.builder().add("%1S", "taco").build() + assertThat(block.toString()).isEqualTo("\"taco\"") + } + + @Test fun typeFormatCanBeIndexed() { + val block = CodeBlock.builder().add("%1T", String::class).build() + assertThat(block.toString()).isEqualTo("kotlin.String") + } + + @Test fun simpleNamedArgument() { + val map = LinkedHashMap<String, Any>() + map["text"] = "taco" + val block = CodeBlock.builder().addNamed("%text:S", map).build() + assertThat(block.toString()).isEqualTo("\"taco\"") + } + + @Test fun repeatedNamedArgument() { + val map = LinkedHashMap<String, Any>() + map["text"] = "tacos" + val block = CodeBlock.builder() + .addNamed("\"I like \" + %text:S + \". Do you like \" + %text:S + \"?\"", map) + .build() + assertThat(block.toString()).isEqualTo( + "\"I like \" + \"tacos\" + \". Do you like \" + \"tacos\" + \"?\"", + ) + } + + @Test fun namedAndNoArgFormat() { + val map = LinkedHashMap<String, Any>() + map["text"] = "tacos" + val block = CodeBlock.builder() + .addNamed("⇥\n%text:L for\n⇤%%3.50", map).build() + assertThat(block.toString()).isEqualTo("\n tacos for\n%3.50") + } + + @Test fun missingNamedArgument() { + assertThrows<IllegalArgumentException> { + val map = LinkedHashMap<String, Any>() + CodeBlock.builder().addNamed("%text:S", map).build() + }.hasMessageThat().isEqualTo("Missing named argument for %text") + } + + @Test fun lowerCaseNamed() { + assertThrows<IllegalArgumentException> { + val map = LinkedHashMap<String, Any>() + map["Text"] = "tacos" + CodeBlock.builder().addNamed("%Text:S", map).build() + }.hasMessageThat().isEqualTo("argument 'Text' must start with a lowercase character") + } + + @Test fun multipleNamedArguments() { + val map = LinkedHashMap<String, Any>() + map["pipe"] = System::class + map["text"] = "tacos" + + val block = CodeBlock.builder() + .addNamed("%pipe:T.out.println(\"Let's eat some %text:L\");", map) + .build() + + assertThat(block.toString()).isEqualTo( + "java.lang.System.out.println(\"Let's eat some tacos\");", + ) + } + + @Test fun namedNewline() { + val map = LinkedHashMap<String, Any>() + map["clazz"] = java.lang.Integer::class + val block = CodeBlock.builder().addNamed("%clazz:T\n", map).build() + assertThat(block.toString()).isEqualTo("kotlin.Int\n") + } + + @Test fun danglingNamed() { + val map = LinkedHashMap<String, Any>() + map["clazz"] = Int::class + assertThrows<IllegalArgumentException> { + CodeBlock.builder().addNamed("%clazz:T%", map).build() + }.hasMessageThat().isEqualTo("dangling % at end") + } + + @Test fun indexTooHigh() { + assertThrows<IllegalArgumentException> { + CodeBlock.builder().add("%2T", String::class).build() + }.hasMessageThat().isEqualTo("index 2 for '%2T' not in range (received 1 arguments)") + } + + @Test fun indexIsZero() { + assertThrows<IllegalArgumentException> { + CodeBlock.builder().add("%0T", String::class).build() + }.hasMessageThat().isEqualTo("index 0 for '%0T' not in range (received 1 arguments)") + } + + @Test fun indexIsNegative() { + assertThrows<IllegalArgumentException> { + CodeBlock.builder().add("%-1T", String::class).build() + }.hasMessageThat().isEqualTo("invalid format string: '%-1T'") + } + + @Test fun indexWithoutFormatType() { + assertThrows<IllegalArgumentException> { + CodeBlock.builder().add("%1", String::class).build() + }.hasMessageThat().isEqualTo("dangling format characters in '%1'") + } + + @Test fun indexWithoutFormatTypeNotAtStringEnd() { + assertThrows<IllegalArgumentException> { + CodeBlock.builder().add("%1 taco", String::class).build() + }.hasMessageThat().isEqualTo("invalid format string: '%1 taco'") + } + + @Test fun indexButNoArguments() { + assertThrows<IllegalArgumentException> { + CodeBlock.builder().add("%1T").build() + }.hasMessageThat().isEqualTo("index 1 for '%1T' not in range (received 0 arguments)") + } + + @Test fun formatIndicatorAlone() { + assertThrows<IllegalArgumentException> { + CodeBlock.builder().add("%", String::class).build() + }.hasMessageThat().isEqualTo("dangling format characters in '%'") + } + + @Test fun formatIndicatorWithoutIndexOrFormatType() { + assertThrows<IllegalArgumentException> { + CodeBlock.builder().add("% tacoString", String::class).build() + }.hasMessageThat().isEqualTo("invalid format string: '% tacoString'") + } + + @Test fun sameIndexCanBeUsedWithDifferentFormats() { + val block = CodeBlock.builder() + .add("%1T.out.println(%1S)", System::class.asClassName()) + .build() + assertThat(block.toString()).isEqualTo("java.lang.System.out.println(\"java.lang.System\")") + } + + @Test fun tooManyStatementEnters() { + val codeBlock = CodeBlock.builder() + .addStatement("print(«%L»)", "1 + 1") + .build() + assertThrows<IllegalStateException> { + // We can't report this error until rendering type because code blocks might be composed. + codeBlock.toString() + }.hasMessageThat().isEqualTo( + """ + |Can't open a new statement until the current statement is closed (opening « followed + |by another « without a closing »). + |Current code block: + |- Format parts: [«, print(, «, %L, », ), \n, »] + |- Arguments: [1 + 1] + | + """.trimMargin(), + ) + } + + @Test fun statementExitWithoutStatementEnter() { + val codeBlock = CodeBlock.builder() + .addStatement("print(%L»)", "1 + 1") + .build() + assertThrows<IllegalStateException> { + // We can't report this error until rendering type because code blocks might be composed. + codeBlock.toString() + }.hasMessageThat().isEqualTo( + """ + |Can't close a statement that hasn't been opened (closing » is not preceded by an + |opening «). + |Current code block: + |- Format parts: [«, print(, %L, », ), \n, »] + |- Arguments: [1 + 1] + | + """.trimMargin(), + ) + } + + @Test fun nullableType() { + val type = String::class.asTypeName().copy(nullable = true) + val typeBlock = CodeBlock.of("%T", type) + assertThat(typeBlock.toString()).isEqualTo("kotlin.String?") + + val list = (List::class.asClassName().copy(nullable = true) as ClassName) + .parameterizedBy(Int::class.asTypeName().copy(nullable = true)) + .copy(nullable = true) + val listBlock = CodeBlock.of("%T", list) + assertThat(listBlock.toString()).isEqualTo("kotlin.collections.List<kotlin.Int?>?") + + val map = (Map::class.asClassName().copy(nullable = true) as ClassName) + .parameterizedBy(String::class.asTypeName().copy(nullable = true), list) + .copy(nullable = true) + val mapBlock = CodeBlock.of("%T", map) + assertThat(mapBlock.toString()) + .isEqualTo("kotlin.collections.Map<kotlin.String?, kotlin.collections.List<kotlin.Int?>?>?") + + val rarr = WildcardTypeName.producerOf(String::class.asTypeName().copy(nullable = true)) + val rarrBlock = CodeBlock.of("%T", rarr) + assertThat(rarrBlock.toString()).isEqualTo("out kotlin.String?") + } + + @Test fun withoutPrefixMatching() { + assertThat( + CodeBlock.of("abcd %S efgh %S ijkl", "x", "y") + .withoutPrefix(CodeBlock.of("")), + ) + .isEqualTo(CodeBlock.of("abcd %S efgh %S ijkl", "x", "y")) + assertThat( + CodeBlock.of("abcd %S efgh %S ijkl", "x", "y") + .withoutPrefix(CodeBlock.of("ab")), + ) + .isEqualTo(CodeBlock.of("cd %S efgh %S ijkl", "x", "y")) + assertThat( + CodeBlock.of("abcd %S efgh %S ijkl", "x", "y") + .withoutPrefix(CodeBlock.of("abcd ")), + ) + .isEqualTo(CodeBlock.of("%S efgh %S ijkl", "x", "y")) + assertThat( + CodeBlock.of("abcd %S efgh %S ijkl", "x", "y") + .withoutPrefix(CodeBlock.of("abcd %S", "x")), + ) + .isEqualTo(CodeBlock.of(" efgh %S ijkl", "y")) + assertThat( + CodeBlock.of("abcd %S efgh %S ijkl", "x", "y") + .withoutPrefix(CodeBlock.of("abcd %S ef", "x")), + ) + .isEqualTo(CodeBlock.of("gh %S ijkl", "y")) + assertThat( + CodeBlock.of("abcd %S efgh %S ijkl", "x", "y") + .withoutPrefix(CodeBlock.of("abcd %S efgh ", "x")), + ) + .isEqualTo(CodeBlock.of("%S ijkl", "y")) + assertThat( + CodeBlock.of("abcd %S efgh %S ijkl", "x", "y") + .withoutPrefix(CodeBlock.of("abcd %S efgh %S", "x", "y")), + ) + .isEqualTo(CodeBlock.of(" ijkl")) + assertThat( + CodeBlock.of("abcd %S efgh %S ijkl", "x", "y") + .withoutPrefix(CodeBlock.of("abcd %S efgh %S ij", "x", "y")), + ) + .isEqualTo(CodeBlock.of("kl")) + assertThat( + CodeBlock.of("abcd %S efgh %S ijkl", "x", "y") + .withoutPrefix(CodeBlock.of("abcd %S efgh %S ijkl", "x", "y")), + ) + .isEqualTo(CodeBlock.of("")) + } + + @Test fun withoutPrefixNoArgs() { + assertThat( + CodeBlock.of("abcd %% efgh %% ijkl") + .withoutPrefix(CodeBlock.of("")), + ) + .isEqualTo(CodeBlock.of("abcd %% efgh %% ijkl")) + assertThat( + CodeBlock.of("abcd %% efgh %% ijkl") + .withoutPrefix(CodeBlock.of("ab")), + ) + .isEqualTo(CodeBlock.of("cd %% efgh %% ijkl")) + assertThat( + CodeBlock.of("abcd %% efgh %% ijkl") + .withoutPrefix(CodeBlock.of("abcd ")), + ) + .isEqualTo(CodeBlock.of("%% efgh %% ijkl")) + assertThat( + CodeBlock.of("abcd %% efgh %% ijkl") + .withoutPrefix(CodeBlock.of("abcd %%")), + ) + .isEqualTo(CodeBlock.of(" efgh %% ijkl")) + assertThat( + CodeBlock.of("abcd %% efgh %% ijkl") + .withoutPrefix(CodeBlock.of("abcd %% ef")), + ) + .isEqualTo(CodeBlock.of("gh %% ijkl")) + assertThat( + CodeBlock.of("abcd %% efgh %% ijkl") + .withoutPrefix(CodeBlock.of("abcd %% efgh ")), + ) + .isEqualTo(CodeBlock.of("%% ijkl")) + assertThat( + CodeBlock.of("abcd %% efgh %% ijkl") + .withoutPrefix(CodeBlock.of("abcd %% efgh %%")), + ) + .isEqualTo(CodeBlock.of(" ijkl")) + assertThat( + CodeBlock.of("abcd %% efgh %% ijkl") + .withoutPrefix(CodeBlock.of("abcd %% efgh %% ij")), + ) + .isEqualTo(CodeBlock.of("kl")) + assertThat( + CodeBlock.of("abcd %% efgh %% ijkl") + .withoutPrefix(CodeBlock.of("abcd %% efgh %% ijkl")), + ) + .isEqualTo(CodeBlock.of("")) + } + + @Test fun withoutPrefixArgMismatch() { + assertThat( + CodeBlock.of("abcd %S efgh %S ijkl", "x", "y") + .withoutPrefix(CodeBlock.of("abcd %S efgh %S ij", "x", "z")), + ) + .isNull() + assertThat( + CodeBlock.of("abcd %S efgh %S ijkl", "x", "y") + .withoutPrefix(CodeBlock.of("abcd %S efgh %S ij", "z", "y")), + ) + .isNull() + } + + @Test fun withoutPrefixFormatPartMismatch() { + assertThat( + CodeBlock.of("abcd %S efgh %S ijkl", "x", "y") + .withoutPrefix(CodeBlock.of("abcd %S efgx %S ij", "x", "y")), + ) + .isNull() + assertThat( + CodeBlock.of("abcd %S efgh %% ijkl", "x") + .withoutPrefix(CodeBlock.of("abcd %% efgh %S ij", "x")), + ) + .isNull() + } + + @Test fun withoutPrefixTooShort() { + assertThat( + CodeBlock.of("abcd %S efgh %S", "x", "y") + .withoutPrefix(CodeBlock.of("abcd %S efgh %S ijkl", "x", "y")), + ) + .isNull() + assertThat( + CodeBlock.of("abcd %S efgh", "x") + .withoutPrefix(CodeBlock.of("abcd %S efgh %S ijkl", "x", "y")), + ) + .isNull() + } + + @Test fun trimEmpty() { + assertThat(CodeBlock.of("").trim()) + .isEqualTo(CodeBlock.of("")) + } + + @Test fun trimNoPlaceholders() { + assertThat(CodeBlock.of("return null").trim()) + .isEqualTo(CodeBlock.of("return null")) + } + + @Test fun trimPlaceholdersWithArgs() { + assertThat(CodeBlock.of("return %S", "taco").trim()) + .isEqualTo(CodeBlock.of("return %S", "taco")) + } + + @Test fun trimNoArgPlaceholderMiddle() { + assertThat(CodeBlock.of("this.taco = %S", "taco").trim()) + .isEqualTo(CodeBlock.of("this.taco = %S", "taco")) + } + + @Test fun trimNoArgPlaceholderStart() { + assertThat(CodeBlock.of("⇥return ").trim()) + .isEqualTo(CodeBlock.of("return ")) + } + + @Test fun trimNoArgPlaceholderEnd() { + assertThat(CodeBlock.of("return ⇥").trim()) + .isEqualTo(CodeBlock.of("return ")) + } + + @Test fun trimNoArgPlaceholdersStartEnd() { + assertThat(CodeBlock.of("«return this»").trim()) + .isEqualTo(CodeBlock.of("return this")) + } + + @Test fun trimMultipleNoArgPlaceholders() { + assertThat( + CodeBlock.of("«return if (x > %L) %S else %S»", 1, "a", "b").trim(), + ) + .isEqualTo(CodeBlock.of("return if (x > %L) %S else %S", 1, "a", "b")) + } + + @Test fun trimOnlyNoArgPlaceholders() { + assertThat(CodeBlock.of("«»⇥⇤").trim()) + .isEqualTo(CodeBlock.of("")) + } + + @Test fun replaceSimple() { + assertThat(CodeBlock.of("%%⇥%%").replaceAll("%%", "")) + .isEqualTo(CodeBlock.of("⇥")) + } + + @Test fun replaceNoMatches() { + assertThat(CodeBlock.of("%%⇥%%").replaceAll("⇤", "")) + .isEqualTo(CodeBlock.of("%%⇥%%")) + } + + @Test fun replaceRegex() { + assertThat(CodeBlock.of("%%⇥%%⇤").replaceAll("[⇥|⇤]", "")) + .isEqualTo(CodeBlock.of("%%%%")) + } + + @Test fun joinToCode() { + val blocks = listOf(CodeBlock.of("%L", "taco1"), CodeBlock.of("%L", "taco2"), CodeBlock.of("%L", "taco3")) + assertThat(blocks.joinToCode(prefix = "(", suffix = ")")) + .isEqualTo(CodeBlock.of("(%L, %L, %L)", "taco1", "taco2", "taco3")) + } + + @Test fun beginControlFlowWithParams() { + val controlFlow = CodeBlock.builder() + .beginControlFlow("list.forEach { element ->") + .addStatement("println(element)") + .endControlFlow() + .build() + assertThat(controlFlow.toString()).isEqualTo( + """ + |list.forEach { element -> + | println(element) + |} + | + """.trimMargin(), + ) + } + + @Test fun beginControlFlowWithParamsAndTemplateString() { + val controlFlow = CodeBlock.builder() + .beginControlFlow("listOf(\"\${1.toString()}\").forEach { element ->") + .addStatement("println(element)") + .endControlFlow() + .build() + assertThat(controlFlow.toString()).isEqualTo( + """ + |listOf("${'$'}{1.toString()}").forEach { element -> + | println(element) + |} + | + """.trimMargin(), + ) + } + + @Test fun buildCodeBlock() { + val codeBlock = buildCodeBlock { + beginControlFlow("if (2 == 2)") + addStatement("println(%S)", "foo") + nextControlFlow("else") + addStatement("println(%S)", "bar") + endControlFlow() + } + assertThat(codeBlock.toString()).isEqualTo( + """ + |if (2 == 2) { + | println("foo") + |} else { + | println("bar") + |} + | + """.trimMargin(), + ) + } + + @Test fun nonWrappingControlFlow() { + val file = FileSpec.builder("com.squareup.tacos", "Test") + .addFunction( + FunSpec.builder("test") + .beginControlFlow("if (%1S == %1S)", "Very long string that would wrap the line ") + .nextControlFlow("else if (%1S == %1S)", "Long string that would wrap the line 2 ") + .endControlFlow() + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + | + |public fun test(): Unit { + | if ("Very long string that would wrap the line " == + | "Very long string that would wrap the line ") { + | } else if ("Long string that would wrap the line 2 " == + | "Long string that would wrap the line 2 ") { + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun ensureEndsWithNewLineWithNoArgs() { + val codeBlock = CodeBlock.builder() + .addStatement("Modeling a kdoc") + .add("\n") + .addStatement("Statement with no args") + .build() + + assertThat(codeBlock.ensureEndsWithNewLine().toString()).isEqualTo( + """ + |Modeling a kdoc + | + |Statement with no args + | + """.trimMargin(), + ) + } + + @Test fun `%N escapes keywords`() { + val funSpec = FunSpec.builder("object").build() + assertThat(CodeBlock.of("%N", funSpec).toString()).isEqualTo("`object`") + } + + @Test fun `%N escapes spaces`() { + val funSpec = FunSpec.builder("create taco").build() + assertThat(CodeBlock.of("%N", funSpec).toString()).isEqualTo("`create taco`") + } + + @Test fun clear() { + val blockBuilder = CodeBlock.builder().addStatement("%S is some existing code", "This") + + blockBuilder.clear() + + assertThat(blockBuilder.build().toString()).isEmpty() + } + + @Test fun withIndent() { + val codeBlock = CodeBlock.Builder() + .apply { + addStatement("User(") + withIndent { + addStatement("age = 42,") + addStatement("cities = listOf(") + withIndent { + addStatement("%S,", "Berlin") + addStatement("%S,", "London") + } + addStatement(")") + } + addStatement(")") + } + .build() + + assertThat(codeBlock.toString()).isEqualTo( + """ + |User( + | age = 42, + | cities = listOf( + | "Berlin", + | "London", + | ) + |) + | + """.trimMargin(), + ) + } + + // https://github.com/square/kotlinpoet/issues/1236 + @Test fun dontEscapeBackslashesInRawStrings() { + // println("ESCAPE '\\'") -> ESCAPE '\' + assertThat(CodeBlock.of("%S", "ESCAPE '\\'").toString()).isEqualTo("\"ESCAPE '\\\\'\"") + // println("""ESCAPE '\'""") -> ESCAPE '\' + assertThat(CodeBlock.of("%P", """ESCAPE '\'""").toString()).isEqualTo("\"\"\"ESCAPE '\\'\"\"\"") + } + + // https://github.com/square/kotlinpoet/issues/1381 + @Test fun useUnderscoresOnLargeDecimalLiterals() { + assertThat(CodeBlock.of("%L", 10000).toString()).isEqualTo("10_000") + assertThat(CodeBlock.of("%L", 100000L).toString()).isEqualTo("100_000") + assertThat(CodeBlock.of("%L", Int.MIN_VALUE).toString()).isEqualTo("-2_147_483_648") + assertThat(CodeBlock.of("%L", Int.MAX_VALUE).toString()).isEqualTo("2_147_483_647") + assertThat(CodeBlock.of("%L", Long.MIN_VALUE).toString()).isEqualTo("-9_223_372_036_854_775_808") + assertThat(CodeBlock.of("%L", 10000.123).toString()).isEqualTo("10_000.123") + assertThat(CodeBlock.of("%L", 3.0).toString()).isEqualTo("3.0") + assertThat(CodeBlock.of("%L", 10000.123f).toString()).isEqualTo("10_000.123") + assertThat(CodeBlock.of("%L", 10000.123456789012).toString()).isEqualTo("10_000.123456789011") + assertThat(CodeBlock.of("%L", 1281.toShort()).toString()).isEqualTo("1_281") + + assertThat(CodeBlock.of("%S", 10000).toString()).isEqualTo("\"10000\"") + assertThat(CodeBlock.of("%S", 100000L).toString()).isEqualTo("\"100000\"") + assertThat(CodeBlock.of("%S", Int.MIN_VALUE).toString()).isEqualTo("\"-2147483648\"") + assertThat(CodeBlock.of("%S", Int.MAX_VALUE).toString()).isEqualTo("\"2147483647\"") + assertThat(CodeBlock.of("%S", Long.MIN_VALUE).toString()).isEqualTo("\"-9223372036854775808\"") + assertThat(CodeBlock.of("%S", 10000.123).toString()).isEqualTo("\"10000.123\"") + assertThat(CodeBlock.of("%S", 3.0).toString()).isEqualTo("\"3.0\"") + assertThat(CodeBlock.of("%S", 10000.123f).toString()).isEqualTo("\"10000.123\"") + assertThat(CodeBlock.of("%S", 10000.12345678901).toString()).isEqualTo("\"10000.12345678901\"") + assertThat(CodeBlock.of("%S", 1281.toShort()).toString()).isEqualTo("\"1281\"") + } +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/CrossplatformTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/CrossplatformTest.kt new file mode 100644 index 00000000..5074b913 --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/CrossplatformTest.kt @@ -0,0 +1,238 @@ +/* + * Copyright (C) 2017 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.truth.Truth.assertThat +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import java.util.concurrent.atomic.AtomicReference +import kotlin.test.Test + +class CrossplatformTest { + + @Test fun crossplatform() { + val expectTypeParam = TypeVariableName("V") + val expectType = "AtomicRef" + val expectSpec = TypeSpec.expectClassBuilder(expectType) + .addTypeVariable(expectTypeParam) + .addModifiers(KModifier.INTERNAL) + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("value", expectTypeParam) + .build(), + ) + .addProperty(PropertySpec.builder("value", expectTypeParam).build()) + .addFunction( + FunSpec.builder("get") + .returns(expectTypeParam) + .build(), + ) + .addFunction( + FunSpec.builder("set") + .addParameter("value", expectTypeParam) + .build(), + ) + .addFunction( + FunSpec.builder("getAndSet") + .addParameter("value", expectTypeParam) + .returns(expectTypeParam) + .build(), + ) + .addFunction( + FunSpec.builder("compareAndSet") + .addParameter("expect", expectTypeParam) + .addParameter("update", expectTypeParam) + .returns(Boolean::class) + .build(), + ) + .build() + val actualName = AtomicReference::class.asTypeName().parameterizedBy(expectTypeParam) + val actualSpec = TypeAliasSpec.builder(expectType, actualName) + .addTypeVariable(expectTypeParam) + .addModifiers(KModifier.ACTUAL) + .build() + val fileSpec = FileSpec.builder("", "Test") + .addType(expectSpec) + .addTypeAlias(actualSpec) + .build() + + assertThat(fileSpec.toString()).isEqualTo( + """ + |import java.util.concurrent.atomic.AtomicReference + |import kotlin.Boolean + |import kotlin.Unit + | + |internal expect class AtomicRef<V>( + | `value`: V, + |) { + | public val `value`: V + | + | public fun `get`(): V + | + | public fun `set`(`value`: V): Unit + | + | public fun getAndSet(`value`: V): V + | + | public fun compareAndSet(`expect`: V, update: V): Boolean + |} + | + |public actual typealias AtomicRef<V> = AtomicReference<V> + | + """.trimMargin(), + ) + } + + @Test fun expectWithSecondaryConstructors() { + val expectSpec = TypeSpec.expectClassBuilder("IoException") + .addModifiers(KModifier.OPEN) + .superclass(Exception::class) + .addFunction(FunSpec.constructorBuilder().build()) + .addFunction( + FunSpec.constructorBuilder() + .addParameter("message", String::class) + .build(), + ) + .build() + val fileSpec = FileSpec.builder("", "Test") + .addType(expectSpec) + .build() + + assertThat(fileSpec.toString()).isEqualTo( + """ + |import java.lang.Exception + |import kotlin.String + | + |public expect open class IoException : Exception { + | public constructor() + | + | public constructor(message: String) + |} + | + """.trimMargin(), + ) + } + + @Test fun topLevelProperties() { + val fileSpec = FileSpec.builder("", "Test") + .addProperty(PropertySpec.builder("bar", String::class, KModifier.EXPECT).build()) + .addProperty( + PropertySpec.builder("bar", String::class, KModifier.ACTUAL) + .initializer(CodeBlock.of("%S", "Hello")) + .build(), + ) + .build() + + assertThat(fileSpec.toString()).isEqualTo( + """ + |import kotlin.String + | + |public expect val bar: String + | + |public actual val bar: String = "Hello" + | + """.trimMargin(), + ) + } + + @Test fun topLevelFunctions() { + val fileSpec = FileSpec.builder("", "Test") + .addFunction( + FunSpec.builder("f1") + .addModifiers(KModifier.EXPECT) + .returns(Int::class) + .build(), + ) + .addFunction( + FunSpec.builder("f1") + .addModifiers(KModifier.ACTUAL) + .addStatement("return 1") + .build(), + ) + .build() + + assertThat(fileSpec.toString()).isEqualTo( + """ + |import kotlin.Int + | + |public expect fun f1(): Int + | + |public actual fun f1() = 1 + | + """.trimMargin(), + ) + } + + @Test fun initBlockInExpectForbidden() { + assertThrows<IllegalStateException> { + TypeSpec.expectClassBuilder("AtomicRef") + .addInitializerBlock(CodeBlock.of("println()")) + }.hasMessageThat().isEqualTo("expect CLASS can't have initializer blocks") + } + + @Test fun expectFunctionBodyForbidden() { + assertThrows<IllegalArgumentException> { + TypeSpec.expectClassBuilder("AtomicRef") + .addFunction( + FunSpec.builder("print") + .addStatement("println()") + .build(), + ) + .build() + }.hasMessageThat().isEqualTo("functions in expect classes can't have bodies") + } + + @Test fun expectPropertyInitializerForbidden() { + assertThrows<IllegalArgumentException> { + TypeSpec.expectClassBuilder("AtomicRef") + .addProperty( + PropertySpec.builder("a", Boolean::class) + .initializer("true") + .build(), + ) + }.hasMessageThat().isEqualTo("properties in expect classes can't have initializers") + } + + @Test fun expectPropertyGetterForbidden() { + assertThrows<IllegalArgumentException> { + TypeSpec.expectClassBuilder("AtomicRef") + .addProperty( + PropertySpec.builder("a", Boolean::class) + .getter( + FunSpec.getterBuilder() + .addStatement("return true") + .build(), + ) + .build(), + ) + }.hasMessageThat().isEqualTo("properties in expect classes can't have getters and setters") + } + + @Test fun expectPropertySetterForbidden() { + assertThrows<IllegalArgumentException> { + TypeSpec.expectClassBuilder("AtomicRef") + .addProperty( + PropertySpec.builder("a", Boolean::class) + .mutable() + .setter( + FunSpec.setterBuilder() + .addParameter("value", Boolean::class) + .addStatement("field = true") + .build(), + ) + .build(), + ) + }.hasMessageThat().isEqualTo("properties in expect classes can't have getters and setters") + } +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/DelegatedConstructorCallTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/DelegatedConstructorCallTest.kt new file mode 100644 index 00000000..80a90358 --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/DelegatedConstructorCallTest.kt @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test + +class DelegatedConstructorCallTest { + @Test + fun defaultPresentInClass() { + val builder = TypeSpec.classBuilder("Test") + .superclass(ClassName("testpackage", "TestSuper")) + assertThat(builder.build().toString()).isEqualTo( + """ + |public class Test : testpackage.TestSuper() + | + """.trimMargin(), + ) + } + + @Test + fun defaultPresentInObject() { + val builder = TypeSpec.objectBuilder("Test") + .superclass(ClassName("testpackage", "TestSuper")) + assertThat(builder.build().toString()).isEqualTo( + """ + |public object Test : testpackage.TestSuper() + | + """.trimMargin(), + ) + } + + @Test + fun defaultNotPresentInExternalClass() { + val builder = TypeSpec.classBuilder("Test") + .addModifiers(KModifier.EXTERNAL) + .superclass(ClassName("testpackage", "TestSuper")) + assertThat(builder.build().toString()).isEqualTo( + """ + |public external class Test : testpackage.TestSuper + | + """.trimMargin(), + ) + } + + @Test + fun defaultNotPresentInExpectClass() { + val builder = TypeSpec.classBuilder("Test") + .addModifiers(KModifier.EXPECT) + .superclass(ClassName("testpackage", "TestSuper")) + assertThat(builder.build().toString()).isEqualTo( + """ + |public expect class Test : testpackage.TestSuper + | + """.trimMargin(), + ) + } + + @Test + fun defaultNotPresentInExpectObject() { + val builder = TypeSpec.objectBuilder("Test") + .addModifiers(KModifier.EXPECT) + .superclass(ClassName("testpackage", "TestSuper")) + assertThat(builder.build().toString()).isEqualTo( + """ + |public expect object Test : testpackage.TestSuper + | + """.trimMargin(), + ) + } + + @Test + fun defaultNotPresentInExternalObject() { + val builder = TypeSpec.objectBuilder("Test") + .addModifiers(KModifier.EXTERNAL) + .superclass(ClassName("testpackage", "TestSuper")) + assertThat(builder.build().toString()).isEqualTo( + """ + |public external object Test : testpackage.TestSuper + | + """.trimMargin(), + ) + } + + @Test + fun allowedInClass() { + val builder = TypeSpec.classBuilder("Test") + .superclass(ClassName("testpackage", "TestSuper")) + .addSuperclassConstructorParameter("anything") + assertThat(builder.build().toString()).isEqualTo( + """ + |public class Test : testpackage.TestSuper(anything) + | + """.trimMargin(), + ) + } + + @Test + fun allowedInObject() { + val builder = TypeSpec.objectBuilder("Test") + .superclass(ClassName("testpackage", "TestSuper")) + .addSuperclassConstructorParameter("anything") + assertThat(builder.build().toString()).isEqualTo( + """ + |public object Test : testpackage.TestSuper(anything) + | + """.trimMargin(), + ) + } + + @Test + fun allowedInClassSecondary() { + val builder = TypeSpec.classBuilder("Test") + val primaryConstructorBuilder = FunSpec.constructorBuilder() + val primaryConstructor = primaryConstructorBuilder.build() + builder.primaryConstructor(primaryConstructor) + val secondaryConstructorBuilder = FunSpec.constructorBuilder() + .addParameter(ParameterSpec("foo", ClassName("kotlin", "String"))) + .callThisConstructor() + builder.addFunction(secondaryConstructorBuilder.build()) + assertThat(builder.build().toString()).isEqualTo( + """ + |public class Test() { + | public constructor(foo: kotlin.String) : this() + |} + | + """.trimMargin(), + ) + } + + @Test + fun notAllowedInExternalClass() { + val builder = TypeSpec.classBuilder("Test") + .addModifiers(KModifier.EXTERNAL) + .superclass(ClassName("testpackage", "TestSuper")) + .addSuperclassConstructorParameter("anything") + assertThrows<IllegalStateException> { + builder.build() + } + } + + @Test + fun notAllowedInExternalObject() { + val builder = TypeSpec.objectBuilder("Test") + .addModifiers(KModifier.EXTERNAL) + .superclass(ClassName("testpackage", "TestSuper")) + .addSuperclassConstructorParameter("anything") + assertThrows<IllegalStateException> { + builder.build() + } + } + + @Test + fun notAllowedInExternalClassSecondary() { + val builder = TypeSpec.classBuilder("Test") + .addModifiers(KModifier.EXTERNAL) + val primaryConstructorBuilder = FunSpec.constructorBuilder() + builder.primaryConstructor(primaryConstructorBuilder.build()) + val secondaryConstructorBuilder = FunSpec.constructorBuilder() + .addParameter(ParameterSpec("foo", ClassName("kotlin", "String"))) + .callThisConstructor() + builder.addFunction(secondaryConstructorBuilder.build()) + assertThrows<IllegalStateException> { + builder.build() + } + } +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/ExpectDeclarationsTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/ExpectDeclarationsTest.kt new file mode 100644 index 00000000..5cb7753c --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/ExpectDeclarationsTest.kt @@ -0,0 +1,81 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test + +class ExpectDeclarationsTest { + @Test fun expectFunDeclaration() { + val methodSpec = FunSpec.builder("function") + .addModifiers(KModifier.EXPECT) + .build() + + assertThat(methodSpec.toString()).isEqualTo( + """ + |public expect fun function(): kotlin.Unit + | + """.trimMargin(), + ) + } + + @Test fun implicitExpectFunDeclaration() { + val builder = TypeSpec.classBuilder("Test") + .addModifiers(KModifier.EXPECT) + val methodSpec = FunSpec.builder("function") + .build() + builder.addFunction(methodSpec) + + assertThat(builder.build().toString()).isEqualTo( + """ + |public expect class Test { + | public fun function(): kotlin.Unit + |} + | + """.trimMargin(), + ) + } + + @Test fun expectPropertyDeclaration() { + val propertySpec = PropertySpec.builder("prop", String::class) + .addModifiers(KModifier.EXPECT) + .build() + + assertThat(propertySpec.toString()).isEqualTo( + """ + |expect val prop: kotlin.String + | + """.trimMargin(), + ) + } + + @Test fun implicitExpectPropertyDeclaration() { + val builder = TypeSpec.classBuilder("Test") + .addModifiers(KModifier.EXPECT) + val propertySpec = PropertySpec.builder("prop", String::class) + .build() + builder.addProperty(propertySpec) + + assertThat(builder.build().toString()).isEqualTo( + """ + |public expect class Test { + | public val prop: kotlin.String + |} + | + """.trimMargin(), + ) + } +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/ExternalDeclarationsTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/ExternalDeclarationsTest.kt new file mode 100644 index 00000000..42719c41 --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/ExternalDeclarationsTest.kt @@ -0,0 +1,182 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test + +class ExternalDeclarationsTest { + @Test fun externalFunDeclarationWithoutBody() { + val methodSpec = FunSpec.builder("function") + .addModifiers(KModifier.EXTERNAL) + .build() + + assertThat(methodSpec.toString()).isEqualTo( + """ + |public external fun function(): kotlin.Unit + | + """.trimMargin(), + ) + } + + @Test fun externalFunDeclarationWithDefinedExternally() { + val methodSpec = FunSpec.builder("function") + .addModifiers(KModifier.EXTERNAL) + .addCode("return kotlin.js.definedExternally") + .build() + + assertThat(methodSpec.toString()).isEqualTo( + """ + |public external fun function() = kotlin.js.definedExternally + | + """.trimMargin(), + ) + } + + @Test fun externalFunDeclarationWithDefinedExternally2() { + val methodSpec = FunSpec.builder("function") + .addModifiers(KModifier.EXTERNAL) + .addCode("kotlin.js.definedExternally") + .build() + + assertThat(methodSpec.toString()).isEqualTo( + """ + |public external fun function(): kotlin.Unit { + | kotlin.js.definedExternally + |} + | + """.trimMargin(), + ) + } + + @Test fun implicitExternalFunDeclarationWithoutBody() { + val builder = TypeSpec.classBuilder("Test") + .addModifiers(KModifier.EXTERNAL) + val methodSpec = FunSpec.builder("function") + .build() + builder.addFunction(methodSpec) + + assertThat(builder.build().toString()).isEqualTo( + """ + |public external class Test { + | public fun function(): kotlin.Unit + |} + | + """.trimMargin(), + ) + } + + @Test fun implicitExternalFunDeclarationWithDefinedExternally() { + val builder = TypeSpec.classBuilder("Test") + .addModifiers(KModifier.EXTERNAL) + val methodSpec = FunSpec.builder("function") + .addCode("return kotlin.js.definedExternally") + .build() + builder.addFunction(methodSpec) + + assertThat(builder.build().toString()).isEqualTo( + """ + |public external class Test { + | public fun function() = kotlin.js.definedExternally + |} + | + """.trimMargin(), + ) + } + + @Test fun implicitExternalFunDeclarationWithDefinedExternally2() { + val builder = TypeSpec.classBuilder("Test") + .addModifiers(KModifier.EXTERNAL) + val methodSpec = FunSpec.builder("function") + .addModifiers(KModifier.EXTERNAL) + .addCode("kotlin.js.definedExternally") + .build() + builder.addFunction(methodSpec) + + assertThat(builder.build().toString()).isEqualTo( + """ + |public external class Test { + | public fun function(): kotlin.Unit { + | kotlin.js.definedExternally + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun externalPropertyDeclarationWithoutInitializer() { + val propertySpec = PropertySpec.builder("prop", String::class) + .addModifiers(KModifier.EXTERNAL) + .build() + + assertThat(propertySpec.toString()).isEqualTo( + """ + |external val prop: kotlin.String + | + """.trimMargin(), + ) + } + + @Test fun externalPropertyDeclarationWithDefinedExternally() { + val propertySpec = PropertySpec.builder("prop", String::class) + .addModifiers(KModifier.EXTERNAL) + .initializer("kotlin.js.definedExternally") + .build() + + assertThat(propertySpec.toString()).isEqualTo( + """ + |external val prop: kotlin.String = kotlin.js.definedExternally + | + """.trimMargin(), + ) + } + + @Test fun implicitExternalPropertyDeclarationWithoutInitializer() { + val builder = TypeSpec.classBuilder("Test") + .addModifiers(KModifier.EXTERNAL) + val propertySpec = PropertySpec.builder("prop", String::class) + .build() + builder.addProperty(propertySpec) + + assertThat(builder.build().toString()).isEqualTo( + """ + |public external class Test { + | public val prop: kotlin.String + |} + | + """.trimMargin(), + ) + } + + @Test fun implicitExternalPropertyDeclarationWithDefinedExternally() { + val builder = TypeSpec.classBuilder("Test") + .addModifiers(KModifier.EXTERNAL) + val propertySpec = PropertySpec.builder("prop", String::class) + .initializer("kotlin.js.definedExternally") + .build() + builder.addProperty(propertySpec) + + assertThat(builder.build().toString()).isEqualTo( + """ + |public external class Test { + | public val prop: kotlin.String = kotlin.js.definedExternally + |} + | + """.trimMargin(), + ) + } +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/FileReadingTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/FileReadingTest.kt new file mode 100644 index 00000000..35719cf8 --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/FileReadingTest.kt @@ -0,0 +1,64 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.io.ByteStreams +import com.google.common.truth.Truth.assertThat +import java.net.URI +import java.nio.charset.StandardCharsets.UTF_8 +import javax.tools.JavaFileObject.Kind +import kotlin.test.Test + +class FileReadingTest { + @Test fun javaFileObjectUri() { + val type = TypeSpec.classBuilder("Test").build() + assertThat(FileSpec.get("", type).toJavaFileObject().toUri()) + .isEqualTo(URI.create("Test.kt")) + assertThat(FileSpec.get("foo", type).toJavaFileObject().toUri()) + .isEqualTo(URI.create("foo/Test.kt")) + assertThat(FileSpec.get("com.example", type).toJavaFileObject().toUri()) + .isEqualTo(URI.create("com/example/Test.kt")) + } + + @Test fun javaFileObjectKind() { + val source = FileSpec.get("", TypeSpec.classBuilder("Test").build()) + assertThat(source.toJavaFileObject().kind).isEqualTo(Kind.SOURCE) + } + + @Test fun javaFileObjectCharacterContent() { + val type = TypeSpec.classBuilder("Test") + .addKdoc("Pi\u00f1ata\u00a1") + .addFunction(FunSpec.builder("fooBar").build()) + .build() + val source = FileSpec.get("foo", type) + val javaFileObject = source.toJavaFileObject() + + // We can never have encoding issues (everything is in process) + assertThat(javaFileObject.getCharContent(true)).isEqualTo(source.toString()) + assertThat(javaFileObject.getCharContent(false)).isEqualTo(source.toString()) + } + + @Test fun javaFileObjectInputStreamIsUtf8() { + val source = FileSpec.builder("foo", "Test") + .addType(TypeSpec.classBuilder("Test").build()) + .addFileComment("Pi\u00f1ata\u00a1") + .build() + val bytes = ByteStreams.toByteArray(source.toJavaFileObject().openInputStream()) + + // KotlinPoet always uses UTF-8. + assertThat(bytes).isEqualTo(source.toString().toByteArray(UTF_8)) + } +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/FileSpecTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/FileSpecTest.kt new file mode 100644 index 00000000..03b3cfc0 --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/FileSpecTest.kt @@ -0,0 +1,1215 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.truth.Truth.assertThat +import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget.FILE +import com.squareup.kotlinpoet.AnnotationSpec.UseSiteTarget.SET +import com.squareup.kotlinpoet.KModifier.VARARG +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import java.util.Collections +import java.util.Date +import java.util.concurrent.Callable +import java.util.concurrent.TimeUnit +import java.util.function.Function +import kotlin.test.Ignore +import kotlin.test.Test + +class FileSpecTest { + @Test fun importStaticReadmeExample() { + val hoverboard = ClassName("com.mattel", "Hoverboard") + val namedBoards = ClassName("com.mattel", "Hoverboard", "Boards") + val list = List::class.asClassName() + val arrayList = ClassName("java.util", "ArrayList").parameterizedBy(hoverboard) + val listOfHoverboards = list.parameterizedBy(hoverboard) + val beyond = FunSpec.builder("beyond") + .returns(listOfHoverboards) + .addStatement("val result = %T()", arrayList) + .addStatement("result.add(%T.createNimbus(2000))", hoverboard) + .addStatement("result.add(%T.createNimbus(\"2001\"))", hoverboard) + .addStatement("result.add(%T.createNimbus(%T.THUNDERBOLT))", hoverboard, namedBoards) + .addStatement("%T.sort(result)", Collections::class) + .addStatement("return if (result.isEmpty()) %T.emptyList() else result", Collections::class) + .build() + val hello = TypeSpec.classBuilder("HelloWorld") + .addFunction(beyond) + .build() + val source = FileSpec.builder("com.example.helloworld", "HelloWorld") + .addType(hello) + .addImport(hoverboard, "createNimbus") + .addImport(namedBoards, "THUNDERBOLT") + .addImport(Collections::class, "sort", "emptyList") + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.example.helloworld + | + |import com.mattel.Hoverboard + |import com.mattel.Hoverboard.Boards.THUNDERBOLT + |import com.mattel.Hoverboard.createNimbus + |import java.util.ArrayList + |import java.util.Collections.emptyList + |import java.util.Collections.sort + |import kotlin.collections.List + | + |public class HelloWorld { + | public fun beyond(): List<Hoverboard> { + | val result = ArrayList<Hoverboard>() + | result.add(createNimbus(2000)) + | result.add(createNimbus("2001")) + | result.add(createNimbus(THUNDERBOLT)) + | sort(result) + | return if (result.isEmpty()) emptyList() else result + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun importStaticMixed() { + val source = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.classBuilder("Taco") + .addInitializerBlock( + CodeBlock.builder() + .addStatement("assert %1T.valueOf(\"BLOCKED\") == %1T.BLOCKED", Thread.State::class) + .addStatement("%T.gc()", System::class) + .addStatement("%1T.out.println(%1T.nanoTime())", System::class) + .build(), + ) + .addFunction( + FunSpec.constructorBuilder() + .addParameter("states", Thread.State::class.asClassName(), VARARG) + .build(), + ) + .build(), + ) + .addImport(Thread.State.BLOCKED) + .addImport(System::class, "gc", "out", "nanoTime") + .addImport(Thread.State::class, "valueOf") + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.lang.System.`out` + |import java.lang.System.gc + |import java.lang.System.nanoTime + |import java.lang.Thread + |import java.lang.Thread.State.BLOCKED + |import java.lang.Thread.State.valueOf + | + |public class Taco { + | init { + | assert valueOf("BLOCKED") == BLOCKED + | gc() + | out.println(nanoTime()) + | } + | + | public constructor(vararg states: Thread.State) + |} + | + """.trimMargin(), + ) + } + + @Test fun importTopLevel() { + val source = FileSpec.builder("com.squareup.tacos", "Taco") + .addImport("com.squareup.tacos.internal", "INGREDIENTS", "wrap") + .addFunction( + FunSpec.builder("prepareTacos") + .returns( + List::class.asClassName() + .parameterizedBy(ClassName("com.squareup.tacos", "Taco")), + ) + .addCode("return wrap(INGREDIENTS)\n") + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import com.squareup.tacos.`internal`.INGREDIENTS + |import com.squareup.tacos.`internal`.wrap + |import kotlin.collections.List + | + |public fun prepareTacos(): List<Taco> = wrap(INGREDIENTS) + | + """.trimMargin(), + ) + } + + @Ignore("addImport doesn't support members with %L") + @Test + fun importStaticDynamic() { + val source = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.classBuilder("Taco") + .addFunction( + FunSpec.builder("main") + .addStatement("%T.%L.println(%S)", System::class, "out", "hello") + .build(), + ) + .build(), + ) + .addImport(System::class, "out") + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos; + | + |import static java.lang.System.out; + | + |class Taco { + | void main() { + | out.println("hello"); + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun importStaticNone() { + val source = FileSpec.builder("readme", "Util") + .addType(importStaticTypeSpec("Util")) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package readme + | + |import java.lang.System + |import java.util.concurrent.TimeUnit + |import kotlin.Long + | + |public class Util { + | public fun minutesToSeconds(minutes: Long): Long { + | System.gc() + | return TimeUnit.SECONDS.convert(minutes, TimeUnit.MINUTES) + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun importStaticOnce() { + val source = FileSpec.builder("readme", "Util") + .addType(importStaticTypeSpec("Util")) + .addImport(TimeUnit.SECONDS).build() + assertThat(source.toString()).isEqualTo( + """ + |package readme + | + |import java.lang.System + |import java.util.concurrent.TimeUnit + |import java.util.concurrent.TimeUnit.SECONDS + |import kotlin.Long + | + |public class Util { + | public fun minutesToSeconds(minutes: Long): Long { + | System.gc() + | return SECONDS.convert(minutes, TimeUnit.MINUTES) + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun importStaticTwice() { + val source = FileSpec.builder("readme", "Util") + .addType(importStaticTypeSpec("Util")) + .addImport(TimeUnit.SECONDS) + .addImport(TimeUnit.MINUTES) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package readme + | + |import java.lang.System + |import java.util.concurrent.TimeUnit.MINUTES + |import java.util.concurrent.TimeUnit.SECONDS + |import kotlin.Long + | + |public class Util { + | public fun minutesToSeconds(minutes: Long): Long { + | System.gc() + | return SECONDS.convert(minutes, MINUTES) + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun importStaticWildcardsForbidden() { + assertThrows<IllegalArgumentException> { + FileSpec.builder("readme", "Util") + .addType(importStaticTypeSpec("Util")) + .addImport(TimeUnit::class, "*") + }.hasMessageThat().isEqualTo("Wildcard imports are not allowed") + } + + private fun importStaticTypeSpec(name: String): TypeSpec { + val funSpec = FunSpec.builder("minutesToSeconds") + .addModifiers(KModifier.PUBLIC) + .returns(Long::class) + .addParameter("minutes", Long::class) + .addStatement("%T.gc()", System::class) + .addStatement("return %1T.SECONDS.convert(minutes, %1T.MINUTES)", TimeUnit::class) + .build() + return TypeSpec.classBuilder(name).addFunction(funSpec).build() + } + + @Test fun noImports() { + val source = FileSpec.builder("com.squareup.tacos", "Taco") + .addType(TypeSpec.classBuilder("Taco").build()) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |public class Taco + | + """.trimMargin(), + ) + } + + @Test fun singleImport() { + val source = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.classBuilder("Taco") + .addProperty("madeFreshDate", Date::class) + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.util.Date + | + |public class Taco { + | public val madeFreshDate: Date + |} + | + """.trimMargin(), + ) + } + + @Test fun singleImportEscapeKeywords() { + val source = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.classBuilder("Taco") + .addProperty("madeFreshDate", ClassName("com.squareup.is.fun.in", "Date")) + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import com.squareup.`is`.`fun`.`in`.Date + | + |public class Taco { + | public val madeFreshDate: Date + |} + | + """.trimMargin(), + ) + } + + @Test fun escapeSpacesInPackageName() { + val file = FileSpec.builder("com.squareup.taco factory", "TacoFactory") + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.`taco factory` + | + | + """.trimMargin(), + ) + } + + @Test fun conflictingImports() { + val source = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.classBuilder("Taco") + .addProperty("madeFreshDate", Date::class) + .addProperty("madeFreshDatabaseDate", ClassName("java.sql", "Date")) + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.sql.Date as SqlDate + |import java.util.Date as UtilDate + | + |public class Taco { + | public val madeFreshDate: UtilDate + | + | public val madeFreshDatabaseDate: SqlDate + |} + | + """.trimMargin(), + ) + } + + @Test fun conflictingImportsEscapeKeywords() { + val source = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.classBuilder("Taco") + .addProperty("madeFreshDate1", ClassName("com.squareup.is.fun.in", "Date")) + .addProperty("madeFreshDate2", ClassName("com.squareup.do.val.var", "Date")) + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import com.squareup.`do`.`val`.`var`.Date as VarDate + |import com.squareup.`is`.`fun`.`in`.Date as InDate + | + |public class Taco { + | public val madeFreshDate1: InDate + | + | public val madeFreshDate2: VarDate + |} + | + """.trimMargin(), + ) + } + + @Test fun escapeSpacesInImports() { + val tacoFactory = ClassName("com.squareup.taco factory", "TacoFactory") + val file = FileSpec.builder("com.example", "TacoFactoryDemo") + .addFunction( + FunSpec.builder("main") + .addStatement("println(%T.produceTacos())", tacoFactory) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.example + | + |import com.squareup.`taco factory`.TacoFactory + |import kotlin.Unit + | + |public fun main(): Unit { + | println(TacoFactory.produceTacos()) + |} + | + """.trimMargin(), + ) + } + + @Test fun escapeSpacesInAliasedImports() { + val tacoFactory = ClassName("com.squareup.taco factory", "TacoFactory") + val file = FileSpec.builder("com.example", "TacoFactoryDemo") + .addAliasedImport(tacoFactory, "La Taqueria") + .addFunction( + FunSpec.builder("main") + .addStatement("println(%T.produceTacos())", tacoFactory) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.example + | + |import kotlin.Unit + |import com.squareup.`taco factory`.TacoFactory as `La Taqueria` + | + |public fun main(): Unit { + | println(`La Taqueria`.produceTacos()) + |} + | + """.trimMargin(), + ) + } + + @Test fun aliasedImports() { + val source = FileSpec.builder("com.squareup.tacos", "Taco") + .addAliasedImport(java.lang.String::class.java, "JString") + .addAliasedImport(String::class, "KString") + .addProperty( + PropertySpec.builder("a", java.lang.String::class.java) + .initializer("%T(%S)", java.lang.String::class.java, "a") + .build(), + ) + .addProperty( + PropertySpec.builder("b", String::class) + .initializer("%S", "b") + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.lang.String as JString + |import kotlin.String as KString + | + |public val a: JString = JString("a") + | + |public val b: KString = "b" + | + """.trimMargin(), + ) + } + + @Test fun enumAliasedImport() { + val minsAlias = "MINS" + val source = FileSpec.builder("com.squareup.tacos", "Taco") + .addAliasedImport(TimeUnit::class.asClassName(), "MINUTES", minsAlias) + .addFunction( + FunSpec.builder("sleepForFiveMins") + .addStatement("%T.MINUTES.sleep(5)", TimeUnit::class) + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + |import java.util.concurrent.TimeUnit.MINUTES as MINS + | + |public fun sleepForFiveMins(): Unit { + | MINS.sleep(5) + |} + | + """.trimMargin(), + ) + } + + @Test fun conflictingParentName() { + val source = FileSpec.builder("com.squareup.tacos", "A") + .addType( + TypeSpec.classBuilder("A") + .addType( + TypeSpec.classBuilder("B") + .addType(TypeSpec.classBuilder("Twin").build()) + .addType( + TypeSpec.classBuilder("C") + .addProperty("d", ClassName("com.squareup.tacos", "A", "Twin", "D")) + .build(), + ) + .build(), + ) + .addType( + TypeSpec.classBuilder("Twin") + .addType( + TypeSpec.classBuilder("D") + .build(), + ) + .build(), + ) + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |public class A { + | public class B { + | public class Twin + | + | public class C { + | public val d: A.Twin.D + | } + | } + | + | public class Twin { + | public class D + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun conflictingChildName() { + val source = FileSpec.builder("com.squareup.tacos", "A") + .addType( + TypeSpec.classBuilder("A") + .addType( + TypeSpec.classBuilder("B") + .addType( + TypeSpec.classBuilder("C") + .addProperty("d", ClassName("com.squareup.tacos", "A", "Twin", "D")) + .addType(TypeSpec.classBuilder("Twin").build()) + .build(), + ) + .build(), + ) + .addType( + TypeSpec.classBuilder("Twin") + .addType( + TypeSpec.classBuilder("D") + .build(), + ) + .build(), + ) + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |public class A { + | public class B { + | public class C { + | public val d: A.Twin.D + | + | public class Twin + | } + | } + | + | public class Twin { + | public class D + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun conflictingNameOutOfScope() { + val source = FileSpec.builder("com.squareup.tacos", "A") + .addType( + TypeSpec.classBuilder("A") + .addType( + TypeSpec.classBuilder("B") + .addType( + TypeSpec.classBuilder("C") + .addProperty("d", ClassName("com.squareup.tacos", "A", "Twin", "D")) + .addType( + TypeSpec.classBuilder("Nested") + .addType(TypeSpec.classBuilder("Twin").build()) + .build(), + ) + .build(), + ) + .build(), + ) + .addType( + TypeSpec.classBuilder("Twin") + .addType( + TypeSpec.classBuilder("D") + .build(), + ) + .build(), + ) + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |public class A { + | public class B { + | public class C { + | public val d: Twin.D + | + | public class Nested { + | public class Twin + | } + | } + | } + | + | public class Twin { + | public class D + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun nestedClassAndSuperclassShareName() { + val source = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.classBuilder("Taco") + .superclass(ClassName("com.squareup.wire", "Message")) + .addType( + TypeSpec.classBuilder("Builder") + .superclass(ClassName("com.squareup.wire", "Message", "Builder")) + .build(), + ) + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import com.squareup.wire.Message + | + |public class Taco : Message() { + | public class Builder : Message.Builder() + |} + | + """.trimMargin(), + ) + } + + /** https://github.com/square/javapoet/issues/366 */ + @Test fun annotationIsNestedClass() { + val source = FileSpec.builder("com.squareup.tacos", "TestComponent") + .addType( + TypeSpec.classBuilder("TestComponent") + .addAnnotation(ClassName("dagger", "Component")) + .addType( + TypeSpec.classBuilder("Builder") + .addAnnotation(ClassName("dagger", "Component", "Builder")) + .build(), + ) + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import dagger.Component + | + |@Component + |public class TestComponent { + | @Component.Builder + | public class Builder + |} + | + """.trimMargin(), + ) + } + + @Test fun defaultPackage() { + val source = FileSpec.builder("", "HelloWorld") + .addType( + TypeSpec.classBuilder("HelloWorld") + .addFunction( + FunSpec.builder("main") + .addModifiers(KModifier.PUBLIC) + .addParameter("args", ARRAY.parameterizedBy(String::class.asClassName())) + .addCode("%T.out.println(%S);\n", System::class, "Hello World!") + .build(), + ) + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |import java.lang.System + |import kotlin.Array + |import kotlin.String + |import kotlin.Unit + | + |public class HelloWorld { + | public fun main(args: Array<String>): Unit { + | System.out.println("Hello World!"); + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun defaultPackageTypesAreImported() { + val source = FileSpec.builder("hello", "World") + .addType( + TypeSpec.classBuilder("World") + .addSuperinterface(ClassName("", "Test")) + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package hello + | + |import Test + | + |public class World : Test + | + """.trimMargin(), + ) + } + + @Test fun topOfFileComment() { + val source = FileSpec.builder("com.squareup.tacos", "Taco") + .addType(TypeSpec.classBuilder("Taco").build()) + .addFileComment("Generated %L by KotlinPoet. DO NOT EDIT!", "2015-01-13") + .build() + assertThat(source.toString()).isEqualTo( + """ + |// Generated 2015-01-13 by KotlinPoet. DO NOT EDIT! + |package com.squareup.tacos + | + |public class Taco + | + """.trimMargin(), + ) + } + + @Test fun emptyLinesInTopOfFileComment() { + val source = FileSpec.builder("com.squareup.tacos", "Taco") + .addType(TypeSpec.classBuilder("Taco").build()) + .addFileComment("\nGENERATED FILE:\n\nDO NOT EDIT!\n") + .build() + assertThat(source.toString()).isEqualTo( + """ + |// + |// GENERATED FILE: + |// + |// DO NOT EDIT! + |// + |package com.squareup.tacos + | + |public class Taco + | + """.trimMargin(), + ) + } + + @Test fun packageClassConflictsWithNestedClass() { + val source = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.classBuilder("Taco") + .addProperty("a", ClassName("com.squareup.tacos", "A")) + .addType(TypeSpec.classBuilder("A").build()) + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |public class Taco { + | public val a: com.squareup.tacos.A + | + | public class A + |} + | + """.trimMargin(), + ) + } + + @Test fun multipleTypesInOneFile() { + val source = FileSpec.builder("com.squareup.tacos", "AB") + .addType(TypeSpec.classBuilder("A").build()) + .addType(TypeSpec.classBuilder("B").build()) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |public class A + | + |public class B + | + """.trimMargin(), + ) + } + + @Test fun simpleTypeAliases() { + val source = FileSpec.builder("com.squareup.tacos", "Taco") + .addTypeAlias(TypeAliasSpec.builder("Int8", Byte::class).build()) + .addTypeAlias( + TypeAliasSpec.builder( + "FileTable", + Map::class.parameterizedBy(String::class, Int::class), + ).build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Byte + |import kotlin.Int + |import kotlin.String + |import kotlin.collections.Map + | + |public typealias Int8 = Byte + | + |public typealias FileTable = Map<String, Int> + | + """.trimMargin(), + ) + } + + @Test fun fileAnnotations() { + val source = FileSpec.builder("com.squareup.tacos", "Taco") + .addAnnotation( + AnnotationSpec.builder(JvmName::class) + .useSiteTarget(FILE) + .addMember("%S", "TacoUtils") + .build(), + ) + .addAnnotation(JvmMultifileClass::class) + .build() + assertThat(source.toString()).isEqualTo( + """ + |@file:JvmName("TacoUtils") + |@file:JvmMultifileClass + | + |package com.squareup.tacos + | + |import kotlin.jvm.JvmMultifileClass + |import kotlin.jvm.JvmName + | + | + """.trimMargin(), + ) + } + + @Test fun fileAnnotationMustHaveCorrectUseSiteTarget() { + val builder = FileSpec.builder("com.squareup.tacos", "Taco") + val annotation = AnnotationSpec.builder(JvmName::class) + .useSiteTarget(SET) + .addMember("value", "%S", "TacoUtils") + .build() + assertThrows<IllegalStateException> { + builder.addAnnotation(annotation) + }.hasMessageThat().isEqualTo("Use-site target SET not supported for file annotations.") + } + + @Test fun escapeKeywordInPackageName() { + val source = FileSpec.builder("com.squareup.is.fun.in", "California") + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.`is`.`fun`.`in` + | + | + """.trimMargin(), + ) + } + + @Test fun generalBuilderEqualityTest() { + val source = FileSpec.builder("com.squareup.tacos", "Taco") + .addAnnotation(JvmMultifileClass::class) + .addFileComment("Generated 2015-01-13 by KotlinPoet. DO NOT EDIT!") + .addImport("com.squareup.tacos.internal", "INGREDIENTS") + .addTypeAlias(TypeAliasSpec.builder("Int8", Byte::class).build()) + .indent(" ") + .addFunction( + FunSpec.builder("defaultIngredients") + .addCode("println(INGREDIENTS)\n") + .build(), + ) + .build() + + assertThat(source.toBuilder().build()).isEqualTo(source) + } + + @Test fun modifyAnnotations() { + val builder = FileSpec.builder("com.taco", "Taco") + .addAnnotation( + AnnotationSpec.builder(JvmName::class.asClassName()) + .useSiteTarget(FILE) + .addMember("name = %S", "JvmTaco") + .build(), + ) + + val javaWord = AnnotationSpec.builder(JvmName::class.asClassName()) + .useSiteTarget(FILE) + .addMember("name = %S", "JavaTaco") + .build() + builder.annotations.clear() + builder.annotations.add(javaWord) + + assertThat(builder.build().annotations).containsExactly(javaWord) + } + + @Test fun modifyImports() { + val builder = FileSpec.builder("com.taco", "Taco") + .addImport("com.foo", "Foo") + + val currentImports = builder.imports + builder.clearImports() + builder.addImport("com.foo", "Foo2") + .apply { + for (current in currentImports) { + addImport(current) + } + } + .indent("") + + assertThat(builder.build().toString()).isEqualTo( + """ + package com.taco + + import com.foo.Foo + import com.foo.Foo2 + + + """.trimIndent(), + ) + } + + @Test fun modifyMembers() { + val builder = FileSpec.builder("com.taco", "Taco") + .addFunction(FunSpec.builder("aFunction").build()) + .addProperty(PropertySpec.builder("aProperty", INT).initializer("1").build()) + .addTypeAlias(TypeAliasSpec.builder("ATypeAlias", INT).build()) + .addType(TypeSpec.classBuilder("AClass").build()) + + builder.members.removeAll { it !is TypeSpec } + + check(builder.build().members.all { it is TypeSpec }) + } + + @Test fun clearComment() { + val builder = FileSpec.builder("com.taco", "Taco") + .addFunction(FunSpec.builder("aFunction").build()) + .addFileComment("Hello!") + + builder.clearComment() + .addFileComment("Goodbye!") + + assertThat(builder.build().comment.toString()).isEqualTo("Goodbye!") + } + + // https://github.com/square/kotlinpoet/issues/480 + @Test fun defaultPackageMemberImport() { + val bigInteger = ClassName.bestGuess("bigInt.BigInteger") + val spec = FileSpec.builder("testsrc", "Test") + .addImport("", "bigInt") + .addFunction( + FunSpec.builder("add5ToInput") + .addParameter("input", Int::class) + .returns(bigInteger) + .addCode( + """ + |val inputBigInt = bigInt(input) + |return inputBigInt.add(5) + | + """.trimMargin(), + ) + .build(), + ) + .build() + assertThat(spec.toString()).isEqualTo( + """ + |package testsrc + | + |import bigInt + |import bigInt.BigInteger + |import kotlin.Int + | + |public fun add5ToInput(input: Int): BigInteger { + | val inputBigInt = bigInt(input) + | return inputBigInt.add(5) + |} + | + """.trimMargin(), + ) + } + + @Test fun longFilePackageName() { + val spec = FileSpec.builder("com.squareup.taco.enchilada.quesadillas.tamales.burritos.super.burritos.trying.to.get.a.really.large.packagename", "Test") + .addFunction( + FunSpec.builder("foo") + .build(), + ) + .build() + assertThat(spec.toString()).isEqualTo( + """ + |package com.squareup.taco.enchilada.quesadillas.tamales.burritos.`super`.burritos.trying.to.`get`.a.really.large.packagename + | + |import kotlin.Unit + | + |public fun foo(): Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun importLongPackageName() { + val spec = FileSpec.builder("testsrc", "Test") + .addImport("a.really.veryveryveryveryveryveryvery.long.pkgname.that.will.definitely.cause.a.wrap.duetoitslength", "MyClass") + .build() + assertThat(spec.toString()).isEqualTo( + """ + |package testsrc + | + |import a.really.veryveryveryveryveryveryvery.long.pkgname.that.will.definitely.cause.a.wrap.duetoitslength.MyClass + | + | + """.trimMargin(), + ) + } + + @Test fun importAliasedLongPackageName() { + val spec = FileSpec.builder("testsrc", "Test") + .addAliasedImport(ClassName("a.really.veryveryveryveryveryveryvery.long.pkgname.that.will.definitely.cause.a.wrap.duetoitslength", "MyClass"), "MyClassAlias") + .build() + assertThat(spec.toString()).isEqualTo( + """ + |package testsrc + | + |import a.really.veryveryveryveryveryveryvery.long.pkgname.that.will.definitely.cause.a.wrap.duetoitslength.MyClass as MyClassAlias + | + | + """.trimMargin(), + ) + } + + @Test fun longComment() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addFileComment( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do " + + "eiusmod tempor incididunt ut labore et dolore magna aliqua.", + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |// Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + |package com.squareup.tacos + | + | + """.trimMargin(), + ) + } + + class WackyKey + class OhNoThisDoesNotCompile + + @Test fun longCommentWithTypes() { + val someLongParameterizedTypeName = typeNameOf<List<Map<in String, Collection<Map<WackyKey, out OhNoThisDoesNotCompile>>>>>() + val param = ParameterSpec.builder("foo", someLongParameterizedTypeName).build() + val someLongLambdaTypeName = LambdaTypeName.get(STRING, listOf(param), STRING).copy(suspending = true) + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addFunction( + FunSpec.builder("f1") + .addComment("this is a long line with a possibly long parameterized type with annotation: %T", someLongParameterizedTypeName) + .build(), + ) + .addFunction( + FunSpec.builder("f2") + .addComment("this is a very very very very very very very very very very long line with a very long lambda type: %T", someLongLambdaTypeName) + .build(), + ) + .build() + + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import com.squareup.kotlinpoet.FileSpecTest + |import kotlin.String + |import kotlin.Unit + |import kotlin.collections.Collection + |import kotlin.collections.List + |import kotlin.collections.Map + | + |public fun f1(): Unit { + | // this is a long line with a possibly long parameterized type with annotation: List<Map<in String, Collection<Map<FileSpecTest.WackyKey, out FileSpecTest.OhNoThisDoesNotCompile>>>> + |} + | + |public fun f2(): Unit { + | // this is a very very very very very very very very very very long line with a very long lambda type: suspend String.(foo: List<Map<in String, Collection<Map<FileSpecTest.WackyKey, out FileSpecTest.OhNoThisDoesNotCompile>>>>) -> String + |} + | + """.trimMargin(), + ) + } + + @Test fun simpleScriptTest() { + val spec = FileSpec.scriptBuilder("Taco") + .addProperty(PropertySpec.builder("prop", String::class).initializer("\"hi\"").build()) + .addCode("\n") + .addStatement("println(%S)", "hello!") + .addCode("\n") + .addFunction( + FunSpec.builder("localFun") + .build(), + ) + .addCode("\n") + .addType(TypeSpec.classBuilder("Yay").build()) + .addCode("\n") + .addStatement("val yayInstance = Yay()") + .build() + assertThat(spec.toString()).isEqualTo( + """ + |import kotlin.String + |import kotlin.Unit + | + |val prop: String = "hi" + | + |println("hello!") + | + |public fun localFun(): Unit { + |} + | + |public class Yay + | + |val yayInstance = Yay() + | + """.trimMargin(), + ) + } + + @Test fun defaultImports() { + val spec = FileSpec.scriptBuilder("Taco") + .addProperty(PropertySpec.builder("prop0", STRING.copy(nullable = true)).initializer("null").build()) + .addProperty(PropertySpec.builder("prop1", INT.copy(nullable = true)).initializer("null").build()) + .addProperty(PropertySpec.builder("prop2", typeNameOf<Map<String, Any>?>()).initializer("null").build()) + .addProperty(PropertySpec.builder("prop3", typeNameOf<Callable<String>?>()).initializer("null").build()) + .addProperty(PropertySpec.builder("prop4", typeNameOf<Function<Int, Int>?>()).initializer("null").build()) + .addKotlinDefaultImports() + .addDefaultPackageImport("java.util.function") + .build() + assertThat(spec.toString()).isEqualTo( + """ + |import java.util.concurrent.Callable + | + |val prop0: String? = null + |val prop1: Int? = null + |val prop2: Map<String, Any>? = null + |val prop3: @FunctionalInterface Callable<String>? = null + |val prop4: @FunctionalInterface Function<Int, Int>? = null + | + """.trimMargin(), + ) + } + + @Test fun classNameFactory() { + val className = ClassName("com.example", "Example") + val spec = FileSpec.builder(className).build() + assertThat(spec.packageName).isEqualTo(className.packageName) + assertThat(spec.name).isEqualTo(className.simpleName) + } + + @Test fun classNameFactoryIllegalArgumentExceptionOnNestedType() { + val className = ClassName("com.example", "Example", "Nested") + assertThrows<IllegalArgumentException> { + FileSpec.builder(className) + } + } +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/FileWritingTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/FileWritingTest.kt new file mode 100644 index 00000000..39807095 --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/FileWritingTest.kt @@ -0,0 +1,307 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.jimfs.Configuration +import com.google.common.jimfs.Jimfs +import com.google.common.truth.Truth.assertThat +import java.io.File +import java.nio.charset.StandardCharsets.UTF_8 +import java.nio.file.Files +import java.util.Date +import kotlin.test.Test +import org.junit.Rule +import org.junit.rules.TemporaryFolder + +class FileWritingTest { + // Used for testing java.io File behavior. + @JvmField @Rule + val tmp = TemporaryFolder() + + // Used for testing java.nio.file Path behavior. + private val fs = Jimfs.newFileSystem(Configuration.unix()) + private val fsRoot = fs.rootDirectories.iterator().next() + + // Used for testing annotation processor Filer behavior. + private val filer = TestFiler(fs, fsRoot) + + @Test fun pathNotDirectory() { + val type = TypeSpec.classBuilder("Test").build() + val source = FileSpec.get("example", type) + val path = fs.getPath("/foo/bar") + Files.createDirectories(path.parent) + Files.createFile(path) + assertThrows<IllegalArgumentException> { + source.writeTo(path) + }.hasMessageThat().isEqualTo("path /foo/bar exists but is not a directory.") + } + + @Test fun fileNotDirectory() { + val type = TypeSpec.classBuilder("Test").build() + val source = FileSpec.get("example", type) + val file = File(tmp.newFolder("foo"), "bar") + file.createNewFile() + assertThrows<IllegalArgumentException> { + source.writeTo(file) + }.hasMessageThat().isEqualTo("path ${file.path} exists but is not a directory.") + } + + @Test fun filerDefaultPackage() { + val type = TypeSpec.classBuilder("Test").build() + FileSpec.get("", type).writeTo(filer) + + val testPath = fsRoot.resolve("Test.kt") + assertThat(Files.exists(testPath)).isTrue() + } + + @Test fun pathDefaultPackage() { + val type = TypeSpec.classBuilder("Test").build() + FileSpec.get("", type).writeTo(fsRoot) + + val testPath = fsRoot.resolve("Test.kt") + assertThat(Files.exists(testPath)).isTrue() + } + + @Test fun pathDefaultPackageWithSubdirectory() { + val type = TypeSpec.classBuilder("Test").build() + FileSpec.get("", type).writeTo(fsRoot.resolve("sub")) + + val testPath = fsRoot.resolve("sub/Test.kt") + assertThat(Files.exists(testPath)).isTrue() + } + + @Test fun fileDefaultPackage() { + val type = TypeSpec.classBuilder("Test").build() + FileSpec.get("", type).writeTo(tmp.root) + + val testFile = File(tmp.root, "Test.kt") + assertThat(testFile.exists()).isTrue() + } + + @Test fun pathNestedClasses() { + val type = TypeSpec.classBuilder("Test").build() + FileSpec.get("foo", type).writeTo(fsRoot) + FileSpec.get("foo.bar", type).writeTo(fsRoot) + FileSpec.get("foo.bar.baz", type).writeTo(fsRoot) + + val fooPath = fsRoot.resolve(fs.getPath("foo", "Test.kt")) + val barPath = fsRoot.resolve(fs.getPath("foo", "bar", "Test.kt")) + val bazPath = fsRoot.resolve(fs.getPath("foo", "bar", "baz", "Test.kt")) + assertThat(Files.exists(fooPath)).isTrue() + assertThat(Files.exists(barPath)).isTrue() + assertThat(Files.exists(bazPath)).isTrue() + } + + @Test fun fileNestedClasses() { + val type = TypeSpec.classBuilder("Test").build() + FileSpec.get("foo", type).writeTo(tmp.root) + FileSpec.get("foo.bar", type).writeTo(tmp.root) + FileSpec.get("foo.bar.baz", type).writeTo(tmp.root) + + val fooDir = File(tmp.root, "foo") + val fooFile = File(fooDir, "Test.kt") + val barDir = File(fooDir, "bar") + val barFile = File(barDir, "Test.kt") + val bazDir = File(barDir, "baz") + val bazFile = File(bazDir, "Test.kt") + assertThat(fooFile.exists()).isTrue() + assertThat(barFile.exists()).isTrue() + assertThat(bazFile.exists()).isTrue() + } + + @Test fun filerNestedClasses() { + val type = TypeSpec.classBuilder("Test").build() + FileSpec.get("foo", type).writeTo(filer) + FileSpec.get("foo.bar", type).writeTo(filer) + FileSpec.get("foo.bar.baz", type).writeTo(filer) + + val fooPath = fsRoot.resolve(fs.getPath("foo", "Test.kt")) + val barPath = fsRoot.resolve(fs.getPath("foo", "bar", "Test.kt")) + val bazPath = fsRoot.resolve(fs.getPath("foo", "bar", "baz", "Test.kt")) + assertThat(Files.exists(fooPath)).isTrue() + assertThat(Files.exists(barPath)).isTrue() + assertThat(Files.exists(bazPath)).isTrue() + } + + @Suppress("LocalVariableName") + @Test + fun filerPassesOriginatingElements() { + // TypeSpecs + val element1_1 = FakeElement() + val test1 = TypeSpec.classBuilder("Test1") + .addOriginatingElement(element1_1) + .build() + + val element2_1 = FakeElement() + val element2_2 = FakeElement() + val test2 = TypeSpec.classBuilder("Test2") + .addOriginatingElement(element2_1) + .addOriginatingElement(element2_2) + .build() + + // FunSpecs + val element3_1 = FakeElement() + val element3_2 = FakeElement() + val test3 = FunSpec.builder("fun3") + .addOriginatingElement(element3_1) + .addOriginatingElement(element3_2) + .build() + + // PropertySpecs + val element4_1 = FakeElement() + val element4_2 = FakeElement() + val test4 = PropertySpec.builder("property4", String::class) + .addOriginatingElement(element4_1) + .addOriginatingElement(element4_2) + .build() + + FileSpec.get("example", test1).writeTo(filer) + FileSpec.get("example", test2).writeTo(filer) + FileSpec.builder("example", "Test3") + .addFunction(test3) + .build() + .writeTo(filer) + FileSpec.builder("example", "Test4") + .addProperty(test4) + .build() + .writeTo(filer) + + // Mixed + FileSpec.builder("example", "Mixed") + .addType(test1) + .addType(test2) + .addFunction(test3) + .addProperty(test4) + .build() + .writeTo(filer) + + val testPath1 = fsRoot.resolve(fs.getPath("example", "Test1.kt")) + assertThat(filer.getOriginatingElements(testPath1)).containsExactly(element1_1) + val testPath2 = fsRoot.resolve(fs.getPath("example", "Test2.kt")) + assertThat(filer.getOriginatingElements(testPath2)).containsExactly(element2_1, element2_2) + val testPath3 = fsRoot.resolve(fs.getPath("example", "Test3.kt")) + assertThat(filer.getOriginatingElements(testPath3)).containsExactly(element3_1, element3_2) + val testPath4 = fsRoot.resolve(fs.getPath("example", "Test4.kt")) + assertThat(filer.getOriginatingElements(testPath4)).containsExactly(element4_1, element4_2) + + val mixed = fsRoot.resolve(fs.getPath("example", "Mixed.kt")) + assertThat(filer.getOriginatingElements(mixed)).containsExactly( + element1_1, + element2_1, + element2_2, + element3_1, + element3_2, + element4_1, + element4_2, + ) + } + + @Test fun filerPassesOnlyUniqueOriginatingElements() { + val element1 = FakeElement() + val fun1 = FunSpec.builder("test1") + .addOriginatingElement(element1) + .build() + + val element2 = FakeElement() + val fun2 = FunSpec.builder("test2") + .addOriginatingElement(element1) + .addOriginatingElement(element2) + .build() + + FileSpec.builder("example", "File") + .addFunction(fun1) + .addFunction(fun2) + .build() + .writeTo(filer) + + val file = fsRoot.resolve(fs.getPath("example", "File.kt")) + assertThat(filer.getOriginatingElements(file)).containsExactly(element1, element2) + } + + @Test fun filerClassesWithTabIndent() { + val test = TypeSpec.classBuilder("Test") + .addProperty("madeFreshDate", Date::class) + .addFunction( + FunSpec.builder("main") + .addModifiers(KModifier.PUBLIC) + .addParameter("args", Array<String>::class.java) + .addCode("%T.out.println(%S);\n", System::class, "Hello World!") + .build(), + ) + .build() + FileSpec.builder("foo", "Test") + .addType(test) + .indent("\t") + .build() + .writeTo(filer) + + val fooPath = fsRoot.resolve(fs.getPath("foo", "Test.kt")) + assertThat(Files.exists(fooPath)).isTrue() + val source = String(Files.readAllBytes(fooPath)) + + assertThat(source).isEqualTo( + """ + |package foo + | + |import java.lang.String + |import java.lang.System + |import java.util.Date + |import kotlin.Array + |import kotlin.Unit + | + |public class Test { + |${"\t"}public val madeFreshDate: Date + | + |${"\t"}public fun main(args: Array<String>): Unit { + |${"\t\t"}System.out.println("Hello World!"); + |${"\t"}} + |} + | + """.trimMargin(), + ) + } + + /** + * This test confirms that KotlinPoet ignores the host charset and always uses UTF-8. The host + * charset is customized with `-Dfile.encoding=ISO-8859-1`. + */ + @Test fun fileIsUtf8() { + val source = FileSpec.builder("foo", "Taco") + .addType(TypeSpec.classBuilder("Taco").build()) + .addFileComment("Pi\u00f1ata\u00a1") + .build() + source.writeTo(fsRoot) + + val fooPath = fsRoot.resolve(fs.getPath("foo", "Taco.kt")) + assertThat(String(Files.readAllBytes(fooPath), UTF_8)).isEqualTo( + """ + |// Piñata¡ + |package foo + | + |public class Taco + | + """.trimMargin(), + ) + } + + @Test fun fileWithKeywordName() { + val type = TypeSpec.classBuilder("fun").build() + FileSpec.get("", type).writeTo(filer) + + val testPath = fsRoot.resolve("fun.kt") + assertThat(Files.exists(testPath)).isTrue() + } +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/FunSpecTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/FunSpecTest.kt new file mode 100644 index 00000000..1376cafc --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/FunSpecTest.kt @@ -0,0 +1,1219 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.collect.Iterables.getOnlyElement +import com.google.common.truth.Truth.assertThat +import com.google.testing.compile.CompilationRule +import com.squareup.kotlinpoet.FunSpec.Companion.GETTER +import com.squareup.kotlinpoet.FunSpec.Companion.SETTER +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import java.io.Closeable +import java.io.IOException +import java.util.concurrent.Callable +import javax.lang.model.element.ExecutableElement +import javax.lang.model.element.Modifier +import javax.lang.model.element.TypeElement +import javax.lang.model.type.DeclaredType +import javax.lang.model.util.ElementFilter.methodsIn +import javax.lang.model.util.Elements +import javax.lang.model.util.Types +import kotlin.test.BeforeTest +import kotlin.test.Test +import org.junit.Rule + +@OptIn(ExperimentalKotlinPoetApi::class) +class FunSpecTest { + @Rule @JvmField + val compilation = CompilationRule() + + private lateinit var elements: Elements + private lateinit var types: Types + + @BeforeTest fun setUp() { + elements = compilation.elements + types = compilation.types + } + + private fun getElement(`class`: Class<*>): TypeElement { + return elements.getTypeElement(`class`.canonicalName) + } + + private fun findFirst(elements: Collection<ExecutableElement>, name: String) = + elements.firstOrNull { it.simpleName.toString() == name } + ?: throw IllegalArgumentException("$name not found in $elements") + + @Target(AnnotationTarget.VALUE_PARAMETER) + internal annotation class Nullable + + internal abstract class Everything { + @Deprecated("") + @Throws(IOException::class, SecurityException::class) + protected abstract fun <T> everything( + @Nullable thing: String, + things: List<T>, + ): Runnable where T : Runnable, T : Closeable + } + + internal abstract class HasAnnotation { + abstract override fun toString(): String + } + + internal interface ExtendsOthers : Callable<Int>, Comparable<Long> + + annotation class TestAnnotation + + abstract class InvalidOverrideMethods { + fun finalMethod() { + } + + private fun privateMethod() { + } + + companion object { + @JvmStatic open fun staticMethod() { + } + } + } + + @Test fun overrideEverything() { + val classElement = getElement(Everything::class.java) + val methodElement = getOnlyElement(methodsIn(classElement.enclosedElements)) + val funSpec = FunSpec.overriding(methodElement).build() + assertThat(funSpec.toString()).isEqualTo( + """ + |@kotlin.jvm.Throws(java.io.IOException::class, java.lang.SecurityException::class) + |protected override fun <T> everything(arg0: java.lang.String, arg1: java.util.List<out T>): java.lang.Runnable where T : java.lang.Runnable, T : java.io.Closeable { + |} + | + """.trimMargin(), + ) + } + + @Test fun overrideDoesNotCopyOverrideAnnotation() { + val classElement = getElement(HasAnnotation::class.java) + val exec = getOnlyElement(methodsIn(classElement.enclosedElements)) + val funSpec = FunSpec.overriding(exec).build() + assertThat(funSpec.toString()).isEqualTo( + """ + |public override fun toString(): java.lang.String { + |} + | + """.trimMargin(), + ) + } + + @Test fun overrideExtendsOthersWorksWithActualTypeParameters() { + val classElement = getElement(ExtendsOthers::class.java) + val classType = classElement.asType() as DeclaredType + val methods = methodsIn(elements.getAllMembers(classElement)) + var exec = findFirst(methods, "call") + var funSpec = FunSpec.overriding(exec, classType, types).build() + assertThat(funSpec.toString()).isEqualTo( + """ + |@kotlin.jvm.Throws(java.lang.Exception::class) + |public override fun call(): java.lang.Integer { + |} + | + """.trimMargin(), + ) + exec = findFirst(methods, "compareTo") + funSpec = FunSpec.overriding(exec, classType, types).build() + assertThat(funSpec.toString()).isEqualTo( + """ + |public override fun compareTo(arg0: java.lang.Long): kotlin.Int { + |} + | + """.trimMargin(), + ) + } + + @Test fun overrideInvalidModifiers() { + val classElement = getElement(InvalidOverrideMethods::class.java) + val methods = methodsIn(elements.getAllMembers(classElement)) + + assertThrows<IllegalArgumentException> { + FunSpec.overriding(findFirst(methods, "finalMethod")) + }.hasMessageThat().isEqualTo("cannot override method with modifiers: [public, final]") + + assertThrows<IllegalArgumentException> { + FunSpec.overriding(findFirst(methods, "privateMethod")) + }.hasMessageThat().isEqualTo("cannot override method with modifiers: [private, final]") + + assertThrows<IllegalArgumentException> { + FunSpec.overriding(findFirst(methods, "staticMethod")) + }.hasMessageThat().isEqualTo("cannot override method with modifiers: [public, static]") + } + + @Test fun nullableParam() { + val funSpec = FunSpec.builder("foo") + .addParameter( + ParameterSpec + .builder("string", String::class.asTypeName().copy(nullable = true)) + .build(), + ) + .build() + assertThat(funSpec.toString()).isEqualTo( + """ + |public fun foo(string: kotlin.String?): kotlin.Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun nullableReturnType() { + val funSpec = FunSpec.builder("foo") + .returns(String::class.asTypeName().copy(nullable = true)) + .build() + assertThat(funSpec.toString()).isEqualTo( + """ + |public fun foo(): kotlin.String? { + |} + | + """.trimMargin(), + ) + } + + @Test fun returnsUnitWithoutExpressionBody() { + val funSpec = FunSpec.builder("foo") + .returns(Unit::class) + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |public fun foo(): kotlin.Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun returnsUnitWithExpressionBody() { + val funSpec = FunSpec.builder("foo") + .returns(Unit::class) + .addStatement("return bar()") + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |public fun foo(): kotlin.Unit = bar() + | + """.trimMargin(), + ) + } + + @Test fun returnsLongExpression() { + val funSpec = FunSpec.builder("foo") + .returns(String::class) + .addStatement("val placeholder = 1") + .addStatement("return %S", "Loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong") + .build() + val sb = StringBuilder() + // The FunSpec#toString columnLimit is Integer.MAX_VALUE, + // It will not cause problems with returns long expressions. + CodeWriter(sb).use { + funSpec.emit( + codeWriter = it, + enclosingName = null, + implicitModifiers = setOf(KModifier.PUBLIC), + includeKdocTags = false, + ) + } + assertThat(sb.toString()).isEqualTo( + """ + |public fun foo(): kotlin.String { + | val placeholder = 1 + | return "Loooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooooong" + |} + | + """.trimMargin(), + ) + } + + @Test fun functionParamWithKdoc() { + val funSpec = FunSpec.builder("foo") + .addParameter( + ParameterSpec.builder("string", String::class.asTypeName()) + .addKdoc("A string parameter.") + .build(), + ) + .addParameter( + ParameterSpec.builder("number", Int::class.asTypeName()) + .addKdoc("A number with a multi-line doc comment.\nYes,\nthese\nthings\nhappen.") + .build(), + ) + .addParameter(ParameterSpec.builder("nodoc", Boolean::class.asTypeName()).build()) + .build() + assertThat(funSpec.toString()).isEqualTo( + """ + |/** + | * @param string A string parameter. + | * @param number A number with a multi-line doc comment. + | * Yes, + | * these + | * things + | * happen. + | */ + |public fun foo( + | string: kotlin.String, + | number: kotlin.Int, + | nodoc: kotlin.Boolean, + |): kotlin.Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun functionParamWithKdocToBuilder() { + val funSpec = FunSpec.builder("foo") + .addParameter( + ParameterSpec.builder("string", String::class.asTypeName()) + .addKdoc("A string parameter.") + .build() + .toBuilder() + .addKdoc(" This is non null") + .build(), + ) + .build() + assertThat(funSpec.toString()).isEqualTo( + """ + |/** + | * @param string A string parameter. This is non null + | */ + |public fun foo(string: kotlin.String): kotlin.Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun originatingElementToBuilder() { + val originatingElement = FakeElement() + val funSpec = FunSpec.builder("foo") + .addOriginatingElement(originatingElement) + .build() + + val newSpec = funSpec.toBuilder().build() + assertThat(newSpec.originatingElements).containsExactly(originatingElement) + } + + @Test fun functionParamWithKdocAndReturnKdoc() { + val funSpec = FunSpec.builder("foo") + .addParameter( + ParameterSpec.builder("string", String::class) + .addKdoc("A string parameter.") + .build(), + ) + .addParameter(ParameterSpec.builder("nodoc", Boolean::class).build()) + .returns(String::class, kdoc = "the foo.") + .addStatement("return %S", "foo") + .build() + assertThat(funSpec.toString()).isEqualTo( + """ + |/** + | * @param string A string parameter. + | * @return the foo. + | */ + |public fun foo(string: kotlin.String, nodoc: kotlin.Boolean): kotlin.String = "foo" + | + """.trimMargin(), + ) + } + + @Test fun functionWithModifiedReturnKdoc() { + val funSpec = FunSpec.builder("foo") + .addParameter("nodoc", Boolean::class) + .returns(String::class, kdoc = "the foo.") + .addStatement("return %S", "foo") + .build() + .toBuilder() + .returns(String::class, kdoc = "the modified foo.") + .build() + assertThat(funSpec.toString()).isEqualTo( + """ + |/** + | * @return the modified foo. + | */ + |public fun foo(nodoc: kotlin.Boolean): kotlin.String = "foo" + | + """.trimMargin(), + ) + } + + @Test fun functionWithThrows() { + val funSpec = FunSpec.builder("foo") + .addStatement("throw %T()", AssertionError::class) + .returns(NOTHING) + .build() + assertThat(funSpec.toString()).isEqualTo( + """ + |public fun foo(): kotlin.Nothing = throw java.lang.AssertionError() + | + """.trimMargin(), + ) + } + + @Test fun functionWithWordThrowDoesntConvertToExpressionFunction() { + val throwSomethingElseFun = FunSpec.builder("throwOrDoSomethingElse") + .build() + + val funSpec = FunSpec.builder("foo") + .addStatement("%N()", throwSomethingElseFun) + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |public fun foo(): kotlin.Unit { + | throwOrDoSomethingElse() + |} + | + """.trimMargin(), + ) + } + + @Test fun expressionBodyIsDetectedReturnWithNonBreakingSpace() { + val funSpec = FunSpec.builder("foo") + .addStatement("return·1") + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |public fun foo() = 1 + | + """.trimMargin(), + ) + } + + @Test fun expressionBodyIsDetectedThrowWithNonBreakingSpace() { + val funSpec = FunSpec.builder("foo") + .addStatement("throw·%T()", AssertionError::class) + .returns(NOTHING) + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |public fun foo(): kotlin.Nothing = throw java.lang.AssertionError() + | + """.trimMargin(), + ) + } + + @Test fun functionWithReturnKDocAndMainKdoc() { + val funSpec = FunSpec.builder("foo") + .addParameter("nodoc", Boolean::class) + .returns(String::class, kdoc = "the foo.") + .addStatement("return %S", "foo") + .addKdoc("Do the foo") + .build() + assertThat(funSpec.toString()).isEqualTo( + """ + |/** + | * Do the foo + | * + | * @return the foo. + | */ + |public fun foo(nodoc: kotlin.Boolean): kotlin.String = "foo" + | + """.trimMargin(), + ) + } + + @Test fun functionParamNoLambdaParam() { + val unitType = UNIT + val funSpec = FunSpec.builder("foo") + .addParameter(ParameterSpec.builder("f", LambdaTypeName.get(returnType = unitType)).build()) + .returns(String::class) + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |public fun foo(f: () -> kotlin.Unit): kotlin.String { + |} + | + """.trimMargin(), + ) + } + + @Test fun functionWithReturnKDoc() { + val funSpec = FunSpec.builder("foo") + .addParameter(ParameterSpec.builder("f", LambdaTypeName.get(returnType = UNIT)).build()) + .returns(String::class, CodeBlock.of("the foo.")) + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |/** + | * @return the foo. + | */ + |public fun foo(f: () -> kotlin.Unit): kotlin.String { + |} + | + """.trimMargin(), + ) + } + + @Test fun functionParamNoLambdaParamWithReceiver() { + val unitType = UNIT + val lambdaTypeName = LambdaTypeName.get(receiver = INT, returnType = unitType) + val funSpec = FunSpec.builder("foo") + .addParameter(ParameterSpec.builder("f", lambdaTypeName).build()) + .returns(String::class) + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |public fun foo(f: kotlin.Int.() -> kotlin.Unit): kotlin.String { + |} + | + """.trimMargin(), + ) + } + + @Test fun functionWithContextReceiver() { + val stringType = STRING + val funSpec = FunSpec.builder("foo") + .contextReceivers(stringType) + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |context(kotlin.String) + |public fun foo(): kotlin.Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun functionWithMultipleContextReceivers() { + val stringType = STRING + val intType = INT + val booleanType = BOOLEAN + val funSpec = FunSpec.builder("foo") + .contextReceivers(stringType, intType, booleanType) + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |context(kotlin.String, kotlin.Int, kotlin.Boolean) + |public fun foo(): kotlin.Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun functionWithGenericContextReceiver() { + val genericType = TypeVariableName("T") + val funSpec = FunSpec.builder("foo") + .addTypeVariable(genericType) + .contextReceivers(genericType) + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |context(T) + |public fun <T> foo(): kotlin.Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun annotatedFunctionWithContextReceiver() { + val funSpec = FunSpec.builder("foo") + .addAnnotation(AnnotationSpec.get(TestAnnotation())) + .contextReceivers(STRING) + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |context(kotlin.String) + |@com.squareup.kotlinpoet.FunSpecTest.TestAnnotation + |public fun foo(): kotlin.Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun functionWithAnnotatedContextReceiver() { + val genericType = STRING.copy(annotations = listOf(AnnotationSpec.get(TestAnnotation()))) + val funSpec = FunSpec.builder("foo") + .contextReceivers(genericType) + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |context(@com.squareup.kotlinpoet.FunSpecTest.TestAnnotation kotlin.String) + |public fun foo(): kotlin.Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun constructorWithContextReceiver() { + assertThrows<IllegalStateException> { + FunSpec.constructorBuilder() + .contextReceivers(STRING) + }.hasMessageThat().isEqualTo("constructors cannot have context receivers") + } + + @Test fun accessorWithContextReceiver() { + assertThrows<IllegalStateException> { + FunSpec.getterBuilder() + .contextReceivers(STRING) + }.hasMessageThat().isEqualTo("$GETTER cannot have context receivers") + + assertThrows<IllegalStateException> { + FunSpec.setterBuilder() + .contextReceivers(STRING) + }.hasMessageThat().isEqualTo("$SETTER cannot have context receivers") + } + + @Test fun functionParamSingleLambdaParam() { + val unitType = UNIT + val booleanType = BOOLEAN + val funSpec = FunSpec.builder("foo") + .addParameter( + ParameterSpec.builder( + "f", + LambdaTypeName.get( + parameters = arrayOf(booleanType), + returnType = unitType, + ), + ) + .build(), + ) + .returns(String::class) + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |public fun foo(f: (kotlin.Boolean) -> kotlin.Unit): kotlin.String { + |} + | + """.trimMargin(), + ) + } + + @Test fun functionParamMultipleLambdaParam() { + val unitType = UNIT + val booleanType = BOOLEAN + val stringType = String::class.asClassName() + val lambdaType = LambdaTypeName.get(parameters = arrayOf(booleanType, stringType), returnType = unitType) + val funSpec = FunSpec.builder("foo") + .addParameter(ParameterSpec.builder("f", lambdaType).build()) + .returns(String::class) + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |public fun foo(f: (kotlin.Boolean, kotlin.String) -> kotlin.Unit): kotlin.String { + |} + | + """.trimMargin(), + ) + } + + @Test fun functionParamMultipleLambdaParamNullableLambda() { + val unitType = Unit::class.asClassName() + val booleanType = Boolean::class.asClassName() + val stringType = String::class.asClassName() + val lambdaTypeName = LambdaTypeName + .get(parameters = arrayOf(booleanType, stringType), returnType = unitType) + .copy(nullable = true) + val funSpec = FunSpec.builder("foo") + .addParameter(ParameterSpec.builder("f", lambdaTypeName).build()) + .returns(String::class) + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |public fun foo(f: ((kotlin.Boolean, kotlin.String) -> kotlin.Unit)?): kotlin.String { + |} + | + """.trimMargin(), + ) + } + + @Test fun functionParamMultipleNullableLambdaParam() { + val unitType = Unit::class.asClassName() + val booleanType = Boolean::class.asClassName() + val stringType = String::class.asClassName().copy(nullable = true) + val lambdaTypeName = LambdaTypeName + .get(parameters = arrayOf(booleanType, stringType), returnType = unitType) + .copy(nullable = true) + val funSpec = FunSpec.builder("foo") + .addParameter(ParameterSpec.builder("f", lambdaTypeName).build()) + .returns(String::class) + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |public fun foo(f: ((kotlin.Boolean, kotlin.String?) -> kotlin.Unit)?): kotlin.String { + |} + | + """.trimMargin(), + ) + } + + @Test fun setterWithPublicModifier() { + val funSpec = FunSpec.setterBuilder() + .addParameter("value", String::class.asClassName()) + .addStatement("this.value = this.value") + .addModifiers(KModifier.PUBLIC) + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |public set(`value`) { + | this.value = this.value + |} + | + """.trimMargin(), + ) + } + + @Test fun getterWithPublicModifier() { + val funSpec = FunSpec.getterBuilder() + .addStatement("return value") + .addModifiers(KModifier.PUBLIC) + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |public get() = value + | + """.trimMargin(), + ) + } + + // This does not produce correct Kotlin, but it does at least verify that we do not drop the + // explicitly specified public modifier. + @Test fun methodWithMultipleVisibilityModifiers() { + val funSpec = + FunSpec.builder("myMethod") + .addModifiers(KModifier.PUBLIC, KModifier.INTERNAL, KModifier.PRIVATE) + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |public private internal fun myMethod(): kotlin.Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun methodWithRepeatedVisibilityModifier() { + val funSpec = + FunSpec.builder("myMethod") + .addModifiers(KModifier.PUBLIC, KModifier.PUBLIC, KModifier.PUBLIC) + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |public fun myMethod(): kotlin.Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun thisConstructorDelegate() { + val funSpec = FunSpec.constructorBuilder() + .addParameter("list", List::class.parameterizedBy(Int::class)) + .callThisConstructor("list[0]", "list[1]") + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |public constructor(list: kotlin.collections.List<kotlin.Int>) : this(list[0], list[1]) + | + """.trimMargin(), + ) + } + + @Test fun superConstructorDelegate() { + val funSpec = FunSpec.constructorBuilder() + .addParameter("list", List::class.parameterizedBy(Int::class)) + .callSuperConstructor("list[0]", "list[1]") + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |public constructor(list: kotlin.collections.List<kotlin.Int>) : super(list[0], list[1]) + | + """.trimMargin(), + ) + } + + @Test fun emptyConstructorDelegate() { + val funSpec = FunSpec.constructorBuilder() + .addParameter("a", Int::class) + .callThisConstructor() + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |public constructor(a: kotlin.Int) : this() + | + """.trimMargin(), + ) + } + + @Test fun constructorDelegateWithBody() { + val funSpec = FunSpec.constructorBuilder() + .addParameter("a", Int::class) + .callThisConstructor("a") + .addStatement("println()") + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |public constructor(a: kotlin.Int) : this(a) { + | println() + |} + | + """.trimMargin(), + ) + } + + @Test fun addingDelegateParametersToNonConstructorForbidden() { + assertThrows<IllegalStateException> { + FunSpec.builder("main") + .callThisConstructor("a", "b", "c") + }.hasMessageThat().isEqualTo("only constructors can delegate to other constructors!") + } + + @Test fun emptySecondaryConstructor() { + val constructorSpec = FunSpec.constructorBuilder() + .addParameter("a", Int::class) + .build() + + assertThat(constructorSpec.toString()).isEqualTo( + """ + |public constructor(a: kotlin.Int) + | + """.trimMargin(), + ) + } + + @Test fun reifiedTypesOnNonInlineFunctionsForbidden() { + assertThrows<IllegalArgumentException> { + FunSpec.builder("foo") + .addTypeVariable(TypeVariableName("T").copy(reified = true)) + .build() + }.hasMessageThat().isEqualTo("only type parameters of inline functions can be reified!") + } + + @Test fun equalsAndHashCode() { + var a = FunSpec.constructorBuilder().build() + var b = FunSpec.constructorBuilder().build() + assertThat(a == b).isTrue() + assertThat(a.hashCode()).isEqualTo(b.hashCode()) + a = FunSpec.builder("taco").build() + b = FunSpec.builder("taco").build() + assertThat(a == b).isTrue() + assertThat(a.hashCode()).isEqualTo(b.hashCode()) + val classElement = getElement(Everything::class.java) + val methodElement = getOnlyElement(methodsIn(classElement.enclosedElements)) + a = FunSpec.overriding(methodElement).build() + b = FunSpec.overriding(methodElement).build() + assertThat(a == b).isTrue() + assertThat(a.hashCode()).isEqualTo(b.hashCode()) + } + + @Test fun escapeKeywordInFunctionName() { + val funSpec = FunSpec.builder("if") + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |public fun `if`(): kotlin.Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun escapePunctuationInFunctionName() { + val funSpec = FunSpec.builder("with-hyphen") + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |public fun `with-hyphen`(): kotlin.Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun generalBuilderEqualityTest() { + val funSpec = FunSpec.Builder("getConfig") + .addKdoc("Fix me") + .addAnnotation( + AnnotationSpec.builder(SuppressWarnings::class) + .build(), + ) + .addModifiers(KModifier.PROTECTED) + .addTypeVariable(TypeVariableName("T")) + .receiver(String::class) + .returns(String::class) + .addParameter( + ParameterSpec.builder("config", String::class) + .build(), + ) + .addParameter( + ParameterSpec.builder("override", TypeVariableName("T")) + .build(), + ) + .beginControlFlow("return when") + .addStatement(" override is String -> config + override") + .addStatement(" else -> config + %S", "{ttl:500}") + .endControlFlow() + .build() + + assertThat(funSpec.toBuilder().build()).isEqualTo(funSpec) + } + + @Test fun receiverWithKdoc() { + val funSpec = FunSpec.builder("toBar") + .receiver(String::class, kdoc = "the string to transform.") + .returns(String::class) + .addStatement("return %S", "bar") + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |/** + | * @receiver the string to transform. + | */ + |public fun kotlin.String.toBar(): kotlin.String = "bar" + | + """.trimMargin(), + ) + } + + @Test fun receiverWithKdocAndMainKDoc() { + val funSpec = FunSpec.builder("toBar") + .receiver(String::class, kdoc = "the string to transform.") + .returns(String::class) + .addKdoc("%L", "Converts to bar") + .addStatement("return %S", "bar") + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |/** + | * Converts to bar + | * + | * @receiver the string to transform. + | */ + |public fun kotlin.String.toBar(): kotlin.String = "bar" + | + """.trimMargin(), + ) + } + + @Test fun withAllKdocTags() { + val funSpec = FunSpec.builder("charAt") + .receiver(String::class, kdoc = "the string you want the char from.") + .returns(Char::class, kdoc = "The char at the given [position].") + .addParameter( + ParameterSpec.builder("position", Int::class) + .addKdoc("the index of the character that is returned.") + .build(), + ) + .addKdoc("Returns the character at the given [position].\n\n") + .addStatement("return -1") + .build() + + assertThat(funSpec.toString()).isEqualTo( + """ + |/** + | * Returns the character at the given [position]. + | * + | * @receiver the string you want the char from. + | * @param position the index of the character that is returned. + | * @return The char at the given [position]. + | */ + |public fun kotlin.String.charAt(position: kotlin.Int): kotlin.Char = -1 + | + """.trimMargin(), + ) + } + + @Test fun constructorBuilderEqualityTest() { + val funSpec = FunSpec.constructorBuilder() + .addParameter("list", List::class.parameterizedBy(Int::class)) + .callThisConstructor("list[0]", "list[1]") + .build() + + assertThat(funSpec.toBuilder().build()).isEqualTo(funSpec) + } + + // https://github.com/square/kotlinpoet/issues/398 + @Test fun changingDelegateConstructorOverridesArgs() { + val funSpec = FunSpec.constructorBuilder() + .addParameter("values", List::class.parameterizedBy(String::class)) + .callSuperConstructor("values") + .build() + val updatedFunSpec = funSpec.toBuilder() + .callSuperConstructor("values.toImmutableList()") + .build() + assertThat(updatedFunSpec.toString()).isEqualTo( + """ + |public constructor(values: kotlin.collections.List<kotlin.String>) : super(values.toImmutableList()) + | + """.trimMargin(), + ) + } + + @Test fun modifyModifiers() { + val builder = FunSpec.builder("taco") + .addModifiers(KModifier.PRIVATE) + + builder.modifiers.clear() + builder.modifiers.add(KModifier.INTERNAL) + + assertThat(builder.build().modifiers).containsExactly(KModifier.INTERNAL) + } + + @Test fun modifyAnnotations() { + val builder = FunSpec.builder("taco") + .addAnnotation( + AnnotationSpec.builder(JvmName::class.asClassName()) + .addMember("name = %S", "jvmWord") + .build(), + ) + + val javaWord = AnnotationSpec.builder(JvmName::class.asClassName()) + .addMember("name = %S", "javaWord") + .build() + builder.annotations.clear() + builder.annotations.add(javaWord) + + assertThat(builder.build().annotations).containsExactly(javaWord) + } + + @Test fun modifyTypeVariableNames() { + val builder = FunSpec.builder("taco") + .addTypeVariable(TypeVariableName("V")) + + val tVar = TypeVariableName("T") + builder.typeVariables.clear() + builder.typeVariables.add(tVar) + + assertThat(builder.build().typeVariables).containsExactly(tVar) + } + + @Test fun modifyParameters() { + val builder = FunSpec.builder("taco") + .addParameter(ParameterSpec.builder("topping", String::class.asClassName()).build()) + + val seasoning = ParameterSpec.builder("seasoning", String::class.asClassName()).build() + builder.parameters.clear() + builder.parameters.add(seasoning) + + assertThat(builder.build().parameters).containsExactly(seasoning) + } + + @Test fun jvmStaticModifier() { + val builder = FunSpec.builder("staticMethod") + builder.jvmModifiers(listOf(Modifier.STATIC)) + + assertThat(builder.build().toString()).isEqualTo( + """ + |@kotlin.jvm.JvmStatic + |internal fun staticMethod(): kotlin.Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun jvmFinalModifier() { + val builder = FunSpec.builder("finalMethod") + builder.jvmModifiers(listOf(Modifier.FINAL)) + + assertThat(builder.build().toString()).isEqualTo( + """ + |internal final fun finalMethod(): kotlin.Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun jvmSynchronizedModifier() { + val builder = FunSpec.builder("synchronizedMethod") + builder.jvmModifiers(listOf(Modifier.SYNCHRONIZED)) + + assertThat(builder.build().toString()).isEqualTo( + """ + |@kotlin.jvm.Synchronized + |internal fun synchronizedMethod(): kotlin.Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun ensureTrailingNewline() { + val methodSpec = FunSpec.builder("function") + .addCode("codeWithNoNewline()") + .build() + + assertThat(methodSpec.toString()).isEqualTo( + """ + |public fun function(): kotlin.Unit { + | codeWithNoNewline() + |} + | + """.trimMargin(), + ) + } + + /** Ensures that we don't add a duplicate newline if one is already present. */ + @Test fun ensureTrailingNewlineWithExistingNewline() { + val methodSpec = FunSpec.builder("function") + .addCode("codeWithNoNewline()\n") // Have a newline already, so ensure we're not adding one + .build() + + assertThat(methodSpec.toString()).isEqualTo( + """ + |public fun function(): kotlin.Unit { + | codeWithNoNewline() + |} + | + """.trimMargin(), + ) + } + + // https://github.com/square/kotlinpoet/issues/947 + @Test fun ensureTrailingNewlineWithExpressionBody() { + val methodSpec = FunSpec.builder("function") + .addCode("return codeWithNoNewline()") + .build() + + assertThat(methodSpec.toString()).isEqualTo( + """ + |public fun function() = codeWithNoNewline() + | + """.trimMargin(), + ) + } + + @Test fun ensureTrailingNewlineWithExpressionBodyAndExistingNewline() { + val methodSpec = FunSpec.builder("function") + .addCode("return codeWithNoNewline()\n") // Have a newline already, so ensure we're not adding one + .build() + + assertThat(methodSpec.toString()).isEqualTo( + """ + |public fun function() = codeWithNoNewline() + | + """.trimMargin(), + ) + } + + @Test fun ensureKdocTrailingNewline() { + val methodSpec = FunSpec.builder("function") + .addKdoc("This is a comment with no initial newline") + .build() + + assertThat(methodSpec.toString()).isEqualTo( + """ + |/** + | * This is a comment with no initial newline + | */ + |public fun function(): kotlin.Unit { + |} + | + """.trimMargin(), + ) + } + + /** Ensures that we don't add a duplicate newline if one is already present. */ + @Test fun ensureKdocTrailingNewlineWithExistingNewline() { + val methodSpec = FunSpec.builder("function") + .addKdoc("This is a comment with an initial newline\n") + .build() + + assertThat(methodSpec.toString()).isEqualTo( + """ + |/** + | * This is a comment with an initial newline + | */ + |public fun function(): kotlin.Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun annotatedLambdaReceiverType() { + val annotation = AnnotationSpec.builder(ClassName("com.squareup.tacos", "Annotation")).build() + val type = LambdaTypeName.get(returnType = UNIT).copy(annotations = listOf(annotation)) + val spec = FileSpec.builder("com.squareup.tacos", "Taco") + .addFunction( + FunSpec.builder("foo") + .receiver(type) + .build(), + ) + .build() + assertThat(spec.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + | + |public fun (@Annotation () -> Unit).foo(): Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun annotatedLambdaReturnType() { + val annotation = AnnotationSpec.builder(ClassName("com.squareup.tacos", "Annotation")).build() + val type = LambdaTypeName.get(returnType = UNIT).copy(annotations = listOf(annotation)) + val spec = FileSpec.builder("com.squareup.tacos", "Taco") + .addFunction( + FunSpec.builder("foo") + .returns(type) + .build(), + ) + .build() + assertThat(spec.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + | + |public fun foo(): @Annotation () -> Unit { + |} + | + """.trimMargin(), + ) + } +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/JavaClassWithArrayValueAnnotation.java b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/JavaClassWithArrayValueAnnotation.java new file mode 100644 index 00000000..c48b3a4c --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/JavaClassWithArrayValueAnnotation.java @@ -0,0 +1,18 @@ +package com.squareup.kotlinpoet; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import static com.squareup.kotlinpoet.JavaClassWithArrayValueAnnotation.AnnotationWithArrayValue; + +@AnnotationWithArrayValue({ + Object.class, Boolean.class +}) +public class JavaClassWithArrayValueAnnotation { + + @Retention(RetentionPolicy.RUNTIME) + @interface AnnotationWithArrayValue { + Class[] value(); + } + +}
\ No newline at end of file diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/KotlinPoetTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/KotlinPoetTest.kt new file mode 100644 index 00000000..9d3f1711 --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/KotlinPoetTest.kt @@ -0,0 +1,1375 @@ +/* + * Copyright (C) 2017 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.truth.Truth.assertThat +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.jvm.jvmField +import com.squareup.kotlinpoet.jvm.jvmSuppressWildcards +import java.util.concurrent.TimeUnit +import kotlin.test.Test + +class KotlinPoetTest { + private val tacosPackage = "com.squareup.tacos" + + @Test fun topLevelMembersRetainOrder() { + val source = FileSpec.builder(tacosPackage, "Taco") + .addFunction(FunSpec.builder("a").addModifiers(KModifier.PUBLIC).build()) + .addType(TypeSpec.classBuilder("B").build()) + .addProperty( + PropertySpec.builder("c", String::class, KModifier.PUBLIC) + .initializer("%S", "C") + .build(), + ) + .addFunction(FunSpec.builder("d").build()) + .addType(TypeSpec.classBuilder("E").build()) + .addProperty( + PropertySpec.builder("f", String::class, KModifier.PUBLIC) + .initializer("%S", "F") + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.Unit + | + |public fun a(): Unit { + |} + | + |public class B + | + |public val c: String = "C" + | + |public fun d(): Unit { + |} + | + |public class E + | + |public val f: String = "F" + | + """.trimMargin(), + ) + } + + @Test fun noTopLevelConstructor() { + assertThrows<IllegalArgumentException> { + FileSpec.builder(tacosPackage, "Taco") + .addFunction(FunSpec.constructorBuilder().build()) + } + } + + @Test fun primaryConstructor() { + val source = FileSpec.get( + tacosPackage, + TypeSpec.classBuilder("Taco") + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("cheese", String::class) + .beginControlFlow("require(cheese.isNotEmpty())") + .addStatement("%S", "cheese cannot be empty") + .endControlFlow() + .build(), + ) + .build(), + ) + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + | + |public class Taco( + | cheese: String, + |) { + | init { + | require(cheese.isNotEmpty()) { + | "cheese cannot be empty" + | } + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun primaryConstructorProperties() { + val source = FileSpec.get( + tacosPackage, + TypeSpec.classBuilder("Taco") + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("cheese", String::class) + .addParameter("cilantro", String::class) + .addParameter("lettuce", String::class) + .beginControlFlow("require(!cheese.isEmpty())") + .addStatement("%S", "cheese cannot be empty") + .endControlFlow() + .build(), + ) + .addProperty( + PropertySpec.builder("cheese", String::class) + .initializer("cheese") + .build(), + ) + .addProperty( + PropertySpec.builder("cilantro", String::class.asTypeName()) + .mutable() + .initializer("cilantro") + .build(), + ) + .addProperty( + PropertySpec.builder("lettuce", String::class) + .initializer("lettuce.trim()") + .build(), + ) + .addProperty( + PropertySpec.builder("onion", Boolean::class) + .initializer("true") + .build(), + ) + .build(), + ) + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Boolean + |import kotlin.String + | + |public class Taco( + | public val cheese: String, + | public var cilantro: String, + | lettuce: String, + |) { + | public val lettuce: String = lettuce.trim() + | + | public val onion: Boolean = true + | init { + | require(!cheese.isEmpty()) { + | "cheese cannot be empty" + | } + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun propertyModifiers() { + val source = FileSpec.get( + tacosPackage, + TypeSpec.classBuilder("Taco") + .addProperty( + PropertySpec.builder("CHEESE", String::class, KModifier.PRIVATE, KModifier.CONST) + .initializer("%S", "monterey jack") + .build(), + ) + .addProperty( + PropertySpec.builder("sauce", String::class.asTypeName(), KModifier.PUBLIC) + .mutable() + .initializer("%S", "chipotle mayo") + .build(), + ) + .build(), + ) + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + | + |public class Taco { + | private const val CHEESE: String = "monterey jack" + | + | public var sauce: String = "chipotle mayo" + |} + | + """.trimMargin(), + ) + } + + @Test fun mistargetedModifier() { + assertThrows<IllegalArgumentException> { + PropertySpec.builder("CHEESE", String::class, KModifier.DATA).build() + } + } + + @Test fun visibilityModifiers() { + val source = FileSpec.get( + tacosPackage, + TypeSpec.classBuilder("Taco") + .addFunction(FunSpec.builder("a").addModifiers(KModifier.PUBLIC).build()) + .addFunction(FunSpec.builder("b").addModifiers(KModifier.PROTECTED).build()) + .addFunction(FunSpec.builder("c").addModifiers(KModifier.INTERNAL).build()) + .addFunction(FunSpec.builder("d").addModifiers(KModifier.PRIVATE).build()) + .build(), + ) + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + | + |public class Taco { + | public fun a(): Unit { + | } + | + | protected fun b(): Unit { + | } + | + | internal fun c(): Unit { + | } + | + | private fun d(): Unit { + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun strings() { + val source = FileSpec.get( + tacosPackage, + TypeSpec.classBuilder("Taco") + .addFunction( + FunSpec.builder("strings") + .addStatement("val a = %S", "basic string") + .addStatement("val b = %S", "string with a \$ dollar sign") + .build(), + ) + .build(), + ) + assertThat(source.toString()).isEqualTo( + "" + + "package com.squareup.tacos\n" + + "\n" + + "import kotlin.Unit\n" + + "\n" + + "public class Taco {\n" + + " public fun strings(): Unit {\n" + + " val a = \"basic string\"\n" + + " val b = \"string with a \${\'\$\'} dollar sign\"\n" + + " }\n" + + "}\n", + ) + } + + /** When emitting a triple quote, KotlinPoet escapes the 3rd quote in the triplet. */ + @Test fun rawStrings() { + val source = FileSpec.get( + tacosPackage, + TypeSpec.classBuilder("Taco") + .addFunction( + FunSpec.builder("strings") + .addStatement("val a = %S", "\"\n") + .addStatement("val b = %S", "a\"\"\"b\"\"\"\"\"\"c\n") + .addStatement( + "val c = %S", + """ + |whoa + |"raw" + |string + """.trimMargin(), + ) + .addStatement( + "val d = %S", + """ + |"raw" + |string + |with + |${'$'}a interpolated value + """.trimMargin(), + ) + .build(), + ) + .build(), + ) + assertThat(source.toString()).isEqualTo( + "" + + "package com.squareup.tacos\n" + + "\n" + + "import kotlin.Unit\n" + + "\n" + + "public class Taco {\n" + + " public fun strings(): Unit {\n" + + " val a = \"\"\"\n" + + " |\"\n" + + " |\"\"\".trimMargin()\n" + + " val b = \"\"\"\n" + + " |a\"\"\${'\"'}b\"\"\${'\"'}\"\"\${'\"'}c\n" + + " |\"\"\".trimMargin()\n" + + " val c = \"\"\"\n" + + " |whoa\n" + + " |\"raw\"\n" + + " |string\n" + + " \"\"\".trimMargin()\n" + + " val d = \"\"\"\n" + + " |\"raw\"\n" + + " |string\n" + + " |with\n" + + " |\${\'\$\'}a interpolated value\n" + + " \"\"\".trimMargin()\n" + + " }\n" + + "}\n", + ) + } + + /** + * When a string literal ends in a newline, there's a pipe `|` immediately preceding the closing + * triple quote. Otherwise the closing triple quote has no preceding `|`. + */ + @Test fun edgeCaseStrings() { + val source = FileSpec.get( + tacosPackage, + TypeSpec.classBuilder("Taco") + .addFunction( + FunSpec.builder("strings") + .addStatement("val a = %S", "\n") + .addStatement("val b = %S", " \n ") + .build(), + ) + .build(), + ) + assertThat(source.toString()).isEqualTo( + "" + + "package com.squareup.tacos\n" + + "\n" + + "import kotlin.Unit\n" + + "\n" + + "public class Taco {\n" + + " public fun strings(): Unit {\n" + + " val a = \"\"\"\n" + + " |\n" + + " |\"\"\".trimMargin()\n" + + " val b = \"\"\"\n" + + " | \n" + + " | \n" + + " \"\"\".trimMargin()\n" + + " }\n" + + "}\n", + ) + } + + @Test fun parameterDefaultValue() { + val source = FileSpec.get( + tacosPackage, + TypeSpec.classBuilder("Taco") + .addFunction( + FunSpec.builder("addCheese") + .addParameter( + ParameterSpec.builder("kind", String::class) + .defaultValue("%S", "monterey jack") + .build(), + ) + .build(), + ) + .build(), + ) + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.Unit + | + |public class Taco { + | public fun addCheese(kind: String = "monterey jack"): Unit { + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun extensionFunction() { + val source = FileSpec.builder(tacosPackage, "Taco") + .addFunction( + FunSpec.builder("shrink") + .returns(String::class) + .receiver(String::class) + .addStatement("return substring(0, length - 1)") + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + | + |public fun String.shrink(): String = substring(0, length - 1) + | + """.trimMargin(), + ) + } + + @Test fun extensionFunctionLambda() { + val source = FileSpec.builder(tacosPackage, "Taco") + .addFunction( + FunSpec.builder("shrink") + .returns(String::class) + .receiver( + LambdaTypeName.get( + parameters = arrayOf(String::class.asClassName()), + returnType = String::class.asTypeName(), + ), + ) + .addStatement("return substring(0, length - 1)") + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + | + |public fun ((String) -> String).shrink(): String = substring(0, length - 1) + | + """.trimMargin(), + ) + } + + @Test fun extensionFunctionLambdaWithParamName() { + val source = FileSpec.builder(tacosPackage, "Taco") + .addFunction( + FunSpec.builder("whatever") + .returns(Unit::class) + .receiver( + LambdaTypeName.get( + parameters = arrayOf(ParameterSpec.builder("name", String::class).build()), + returnType = Unit::class.asClassName(), + ), + ) + .addStatement("return Unit") + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.Unit + | + |public fun ((name: String) -> Unit).whatever(): Unit = Unit + | + """.trimMargin(), + ) + } + + @Test fun extensionFunctionLambdaWithMultipleParams() { + val source = FileSpec.builder(tacosPackage, "Taco") + .addFunction( + FunSpec.builder("whatever") + .returns(Unit::class) + .receiver( + LambdaTypeName.get( + parameters = listOf( + ParameterSpec.builder("name", String::class).build(), + ParameterSpec.unnamed(Int::class), + ParameterSpec.builder("age", Long::class).build(), + ), + returnType = Unit::class.asClassName(), + ), + ) + .addStatement("return Unit") + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Int + |import kotlin.Long + |import kotlin.String + |import kotlin.Unit + | + |public fun (( + | name: String, + | Int, + | age: Long, + |) -> Unit).whatever(): Unit = Unit + | + """.trimMargin(), + ) + } + + @Test fun extensionProperty() { + val source = FileSpec.builder(tacosPackage, "Taco") + .addProperty( + PropertySpec.builder("extensionProperty", Int::class) + .receiver(String::class) + .getter( + FunSpec.getterBuilder() + .addStatement("return length") + .build(), + ) + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Int + |import kotlin.String + | + |public val String.extensionProperty: Int + | get() = length + | + """.trimMargin(), + ) + } + + @Test fun extensionPropertyLambda() { + val source = FileSpec.builder(tacosPackage, "Taco") + .addProperty( + PropertySpec.builder("extensionProperty", Int::class) + .receiver( + LambdaTypeName.get( + parameters = arrayOf(String::class.asClassName()), + returnType = String::class.asClassName(), + ), + ) + .getter( + FunSpec.getterBuilder() + .addStatement("return length") + .build(), + ) + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Int + |import kotlin.String + | + |public val ((String) -> String).extensionProperty: Int + | get() = length + | + """.trimMargin(), + ) + } + + @Test fun nullableTypes() { + val list = (List::class.asClassName().copy(nullable = true) as ClassName) + .parameterizedBy(Int::class.asClassName().copy(nullable = true)) + .copy(nullable = true) + assertThat(list.toString()).isEqualTo("kotlin.collections.List<kotlin.Int?>?") + } + + @Test fun getAndSet() { + val source = FileSpec.builder(tacosPackage, "Taco") + .addProperty( + PropertySpec.builder("propertyWithCustomAccessors", Int::class.asTypeName()) + .mutable() + .initializer("%L", 1) + .getter( + FunSpec.getterBuilder() + .addStatement("println(%S)", "getter") + .addStatement("return field") + .build(), + ) + .setter( + FunSpec.setterBuilder() + .addParameter("value", Int::class) + .addStatement("println(%S)", "setter") + .addStatement("field = value") + .build(), + ) + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Int + | + |public var propertyWithCustomAccessors: Int = 1 + | get() { + | println("getter") + | return field + | } + | set(`value`) { + | println("setter") + | field = value + | } + | + """.trimMargin(), + ) + } + + @Test fun propertyWithLongInitializerWrapping() { + val source = FileSpec.builder(tacosPackage, "Taco") + .addProperty( + PropertySpec + .builder("foo", ClassName(tacosPackage, "Foo").copy(nullable = true)) + .addModifiers(KModifier.PRIVATE) + .initializer("DefaultFooRegistry.getInstance().getDefaultFooInstanceForPropertiesFiles(file)") + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |private val foo: Foo? = + | DefaultFooRegistry.getInstance().getDefaultFooInstanceForPropertiesFiles(file) + | + """.trimMargin(), + ) + } + + @Test fun stackedPropertyModifiers() { + val source = FileSpec.builder(tacosPackage, "Taco") + .addType( + TypeSpec.classBuilder("A") + .addModifiers(KModifier.ABSTRACT) + .addProperty( + PropertySpec.builder("q", String::class.asTypeName()) + .mutable() + .addModifiers(KModifier.ABSTRACT, KModifier.PROTECTED) + .build(), + ) + .build(), + ) + .addProperty( + PropertySpec.builder("p", String::class) + .addModifiers(KModifier.CONST, KModifier.INTERNAL) + .initializer("%S", "a") + .build(), + ) + .addType( + TypeSpec.classBuilder("B") + .superclass(ClassName(tacosPackage, "A")) + .addModifiers(KModifier.ABSTRACT) + .addProperty( + PropertySpec.builder("q", String::class.asTypeName()) + .mutable() + .addModifiers( + KModifier.FINAL, + KModifier.LATEINIT, + KModifier.OVERRIDE, + KModifier.PUBLIC, + ) + .build(), + ) + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + | + |public abstract class A { + | protected abstract var q: String + |} + | + |internal const val p: String = "a" + | + |public abstract class B : A() { + | public final override lateinit var q: String + |} + | + """.trimMargin(), + ) + } + + @Test fun stackedFunModifiers() { + val source = FileSpec.get( + tacosPackage, + TypeSpec.classBuilder("A") + .addModifiers(KModifier.OPEN) + .addFunction( + FunSpec.builder("get") + .addModifiers( + KModifier.EXTERNAL, + KModifier.INFIX, + KModifier.OPEN, + KModifier.OPERATOR, + KModifier.PROTECTED, + ) + .addParameter("v", String::class) + .returns(String::class) + .build(), + ) + .addFunction( + FunSpec.builder("loop") + .addModifiers(KModifier.FINAL, KModifier.INLINE, KModifier.INTERNAL, KModifier.TAILREC) + .returns(String::class) + .addStatement("return %S", "a") + .build(), + ) + .build(), + ) + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + | + |public open class A { + | protected open external infix operator fun `get`(v: String): String + | + | internal final tailrec inline fun loop(): String = "a" + |} + | + """.trimMargin(), + ) + } + + @Test fun basicExpressionBody() { + val source = FileSpec.builder(tacosPackage, "Taco") + .addFunction( + FunSpec.builder("addA") + .addParameter("s", String::class) + .returns(String::class) + .addStatement("return s + %S", "a") + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + | + |public fun addA(s: String): String = s + "a" + | + """.trimMargin(), + ) + } + + @Test fun suspendingLambdas() { + val barType = ClassName(tacosPackage, "Bar") + val suspendingLambda = LambdaTypeName + .get(parameters = arrayOf(ClassName(tacosPackage, "Foo")), returnType = barType) + .copy(suspending = true) + val source = FileSpec.builder(tacosPackage, "Taco") + .addProperty( + PropertySpec.builder("bar", suspendingLambda) + .mutable() + .initializer("{ %T() }", barType) + .build(), + ) + .addProperty( + PropertySpec.builder("nullBar", suspendingLambda.copy(nullable = true)) + .mutable() + .initializer("null") + .build(), + ) + .addFunction( + FunSpec.builder("foo") + .addParameter("bar", suspendingLambda) + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + | + |public var bar: suspend (Foo) -> Bar = { Bar() } + | + |public var nullBar: (suspend (Foo) -> Bar)? = null + | + |public fun foo(bar: suspend (Foo) -> Bar): Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun enumAsDefaultArgument() { + val source = FileSpec.builder(tacosPackage, "Taco") + .addFunction( + FunSpec.builder("timeout") + .addParameter("duration", Long::class) + .addParameter( + ParameterSpec.builder("timeUnit", TimeUnit::class) + .defaultValue("%T.%L", TimeUnit::class, TimeUnit.MILLISECONDS.name) + .build(), + ) + .addStatement("this.timeout = timeUnit.toMillis(duration)") + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.util.concurrent.TimeUnit + |import kotlin.Long + |import kotlin.Unit + | + |public fun timeout(duration: Long, timeUnit: TimeUnit = TimeUnit.MILLISECONDS): Unit { + | this.timeout = timeUnit.toMillis(duration) + |} + | + """.trimMargin(), + ) + } + + @Test fun dynamicType() { + val source = FileSpec.builder(tacosPackage, "Taco") + .addFunction( + FunSpec.builder("dynamicTest") + .addCode( + CodeBlock.of( + "%L", + PropertySpec.builder("d1", DYNAMIC) + .initializer("%S", "Taco") + .build(), + ), + ) + .addCode( + CodeBlock.of( + "%L", + PropertySpec.builder("d2", DYNAMIC) + .initializer("1f") + .build(), + ), + ) + .addStatement("// dynamics are dangerous!") + .addStatement("println(d1 - d2)") + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + | + |public fun dynamicTest(): Unit { + | val d1: dynamic = "Taco" + | val d2: dynamic = 1f + | // dynamics are dangerous! + | println(d1 - d2) + |} + | + """.trimMargin(), + ) + } + + @Test fun primaryConstructorParameterAnnotation() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.classBuilder("Taco") + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("foo", String::class) + .build(), + ) + .addProperty( + PropertySpec.builder("foo", String::class) + .jvmField() + .initializer("foo") + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.jvm.JvmField + | + |public class Taco( + | @JvmField + | public val foo: String, + |) + | + """.trimMargin(), + ) + } + + // https://github.com/square/kotlinpoet/issues/346 + @Test fun importTypeArgumentInParameterizedTypeName() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addFunction( + FunSpec.builder("foo") + .addParameter( + "a", + List::class.asTypeName() + .parameterizedBy(Int::class.asTypeName().jvmSuppressWildcards()), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Int + |import kotlin.Unit + |import kotlin.collections.List + |import kotlin.jvm.JvmSuppressWildcards + | + |public fun foo(a: List<@JvmSuppressWildcards Int>): Unit { + |} + | + """.trimMargin(), + ) + } + + // https://github.com/square/kotlinpoet/issues/462 + @Test fun foldingPropertyWithLambdaInitializer() { + val param = ParameterSpec.builder("arg", ANY).build() + val initializer = CodeBlock.builder() + .beginControlFlow("{ %L ->", param) + .addStatement("println(\"arg=\$%N\")", param) + .endControlFlow() + .build() + val lambdaTypeName = ClassName.bestGuess("com.example.SomeTypeAlias") + val property = PropertySpec.builder("foo", lambdaTypeName) + .initializer("foo") + .build() + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.classBuilder("Taco") + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter( + ParameterSpec.builder("foo", lambdaTypeName) + .defaultValue(initializer) + .build(), + ) + .build(), + ) + .addProperty(property) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import com.example.SomeTypeAlias + | + |public class Taco( + | public val foo: SomeTypeAlias = { arg: kotlin.Any -> + | println("arg=${'$'}arg") + | } + | , + |) + | + """.trimMargin(), + ) + } + + // https://github.com/square/kotlinpoet/issues/483 + @Test fun foldingPropertyWithEscapedName() { + val file = FileSpec.builder("com.squareup.tacos", "AlarmInfo") + .addType( + TypeSpec.classBuilder("AlarmInfo") + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("when", Float::class) + .build(), + ) + .addProperty( + PropertySpec.builder("when", Float::class) + .initializer("when") + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Float + | + |public class AlarmInfo( + | public val `when`: Float, + |) + | + """.trimMargin(), + ) + } + + // https://github.com/square/kotlinpoet/issues/577 + @Test fun noWrappingBetweenParamNameAndType() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addFunction( + FunSpec.builder("functionWithAPrettyLongNameThatWouldCauseWrapping") + .addParameter("parameterWithALongNameThatWouldAlsoCauseWrapping", String::class) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.Unit + | + |public + | fun functionWithAPrettyLongNameThatWouldCauseWrapping(parameterWithALongNameThatWouldAlsoCauseWrapping: String): + | Unit { + |} + | + """.trimMargin(), + ) + } + + // https://github.com/square/kotlinpoet/issues/576 + @Test fun noWrappingBetweenValAndPropertyName() { + val wireField = ClassName("com.squareup.wire", "WireField") + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.classBuilder("Taco") + .addModifiers(KModifier.DATA) + .addProperty( + PropertySpec.builder("name", String::class) + .addAnnotation( + AnnotationSpec.builder(wireField) + .addMember("tag = %L", 1) + .addMember("adapter = %S", "CustomStringAdapterWithALongNameThatCauses") + .build(), + ) + .initializer("name") + .build(), + ) + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter( + ParameterSpec.builder("name", String::class) + .build(), + ) + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import com.squareup.wire.WireField + |import kotlin.String + | + |public data class Taco( + | @WireField( + | tag = 1, + | adapter = "CustomStringAdapterWithALongNameThatCauses", + | ) + | public val name: String, + |) + | + """.trimMargin(), + ) + } + + // https://github.com/square/kotlinpoet/issues/578 + @Test fun wrappingInsideKdocKeepsKdocFormatting() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.classBuilder("Builder") + .addKdoc( + "Builder class for Foo. Allows creating instances of Foo by initializing " + + "a subset of their fields, following the Builder pattern.\n", + ) + .addFunction( + FunSpec.builder("summary_text") + .addKdoc( + "The description for the choice, e.g. \"Currently unavailable due to " + + "high demand. Please try later.\" May be null.", + ) + .addParameter("summary_text", String::class.asClassName().copy(nullable = true)) + .returns(ClassName("com.squareup.tacos", "Builder")) + .addStatement("this.summary_text = summary_text") + .addStatement("return this") + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + | + |/** + | * Builder class for Foo. Allows creating instances of Foo by initializing a subset of their fields, + | * following the Builder pattern. + | */ + |public class Builder { + | /** + | * The description for the choice, e.g. "Currently unavailable due to high demand. Please try + | * later." May be null. + | */ + | public fun summary_text(summary_text: String?): Builder { + | this.summary_text = summary_text + | return this + | } + |} + | + """.trimMargin(), + ) + } + + // https://github.com/square/kotlinpoet/issues/606 + @Test fun typeNamesInsideTemplateStringsGetImported() { + val taco = ClassName("com.squareup.tacos", "Taco") + val file = FileSpec.builder("com.squareup.example", "Tacos") + .addFunction( + FunSpec.builder("main") + .addStatement("println(%P)", CodeBlock.of("Here's a taco: \${%T()}", taco)) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.example + | + |import com.squareup.tacos.Taco + |import kotlin.Unit + | + |public fun main(): Unit { + | println(${'"'}""Here's a taco: ${'$'}{Taco()}""${'"'}) + |} + | + """.trimMargin(), + ) + } + + // https://github.com/square/kotlinpoet/issues/606 + @Test fun memberNamesInsideTemplateStringsGetImported() { + val contentToString = MemberName("kotlin.collections", "contentToString") + val file = FileSpec.builder("com.squareup.example", "Tacos") + .addFunction( + FunSpec.builder("main") + .addStatement("val ints = arrayOf(1, 2, 3)") + .addStatement("println(%P)", CodeBlock.of("\${ints.%M()}", contentToString)) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.example + | + |import kotlin.Unit + |import kotlin.collections.contentToString + | + |public fun main(): Unit { + | val ints = arrayOf(1, 2, 3) + | println(${'"'}""${'$'}{ints.contentToString()}""${'"'}) + |} + | + """.trimMargin(), + ) + } + + // https://github.com/square/kotlinpoet/issues/701 + @Test fun noIllegalCharacterInIdentifier() { + assertThrows<IllegalArgumentException> { + TypeSpec.enumBuilder("MyEnum") + .addEnumConstant("with.dots") // dots are illegal, so this should fail + .build().toString() + }.hasMessageThat().isEqualTo("Can't escape identifier `with.dots` because it contains illegal characters: .") + } + + // https://github.com/square/kotlinpoet/issues/814 + @Test fun percentAtTheEndOfKdoc() { + val paramSpec1 = ParameterSpec.builder("a", Int::class) + .addKdoc("Progress in %%") + .build() + val paramSpec2 = ParameterSpec.builder("b", Int::class) + .addKdoc("Some other parameter with %%") + .build() + val funSpec = FunSpec.builder("test") + .addParameters(listOf(paramSpec1, paramSpec2)) + .build() + assertThat(funSpec.toString()).isEqualTo( + """ + |/** + | * @param a Progress in % + | * @param b Some other parameter with % + | */ + |public fun test(a: kotlin.Int, b: kotlin.Int): kotlin.Unit { + |} + | + """.trimMargin(), + ) + } + + // https://github.com/square/kotlinpoet/issues/1031 + @Test fun superClassGetsFullyQualifiedOnConflict() { + val namespace = "test" + + val kotlinExceptionName = ClassName("kotlin", "Exception") + val customExceptionName = ClassName(namespace, "Exception") + val customException = TypeSpec + .classBuilder("Exception") + .superclass(kotlinExceptionName) + .addFunction( + FunSpec + .builder("test") + .addParameter("e", customExceptionName) + .build(), + ) + .build() + + val file = FileSpec.builder(namespace, "Exception") + .addType(customException) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package test + | + |import kotlin.Unit + | + |public class Exception : kotlin.Exception() { + | public fun test(e: Exception): Unit { + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun allStringsAreUnderscore() { + val file = FileSpec.builder("com.squareup.tacos", "SourceWithUnderscores") + .addType( + TypeSpec.classBuilder("SourceWithUnderscores") + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("_", Float::class) + .addParameter("____", Float::class) + .build(), + ) + .addProperty( + PropertySpec.builder("_", Float::class) + .initializer("_") + .build(), + ) + .addProperty( + PropertySpec.builder("____", Float::class) + .initializer("____") + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Float + | + |public class SourceWithUnderscores( + | public val `_`: Float, + | public val `____`: Float, + |) + | + """.trimMargin(), + ) + } + + @Test fun generatedImportAliases() { + val squareTaco = ClassName("com.squareup.tacos", "Taco") + val blockTaco = ClassName("xyz.block.tacos", "Taco") + val kotlinIsNullOrEmpty = MemberName("kotlin.text", "isNullOrEmpty") + val cashIsNullOrEmpty = MemberName("com.squareup.cash.util", "isNullOrEmpty") + val file = FileSpec.builder("com.example", "Test") + .addFunction( + FunSpec.builder("main") + .addStatement("val squareTaco = %L", squareTaco.constructorReference()) + .addStatement("val blockTaco = %L", blockTaco.constructorReference()) + .addStatement("val isSquareTacoNull = %S.%M()", "Taco", kotlinIsNullOrEmpty) + .addStatement("val isBlockTacoNull = %S.%M()", "Taco", cashIsNullOrEmpty) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.example + | + |import kotlin.Unit + |import com.squareup.cash.util.isNullOrEmpty as utilIsNullOrEmpty + |import com.squareup.tacos.Taco as SquareupTacosTaco + |import kotlin.text.isNullOrEmpty as textIsNullOrEmpty + |import xyz.block.tacos.Taco as BlockTacosTaco + | + |public fun main(): Unit { + | val squareTaco = ::SquareupTacosTaco + | val blockTaco = ::BlockTacosTaco + | val isSquareTacoNull = "Taco".textIsNullOrEmpty() + | val isBlockTacoNull = "Taco".utilIsNullOrEmpty() + |} + | + """.trimMargin(), + ) + } + + @Test fun memberImportsOverGeneratedImportAliases() { + val squareTaco = ClassName("com.squareup.tacos", "Taco") + val blockTaco = ClassName("xyz.block.tacos", "Taco") + val kotlinIsNullOrEmpty = MemberName("kotlin.text", "isNullOrEmpty") + val cashIsNullOrEmpty = MemberName("com.squareup.cash.util", "isNullOrEmpty") + val file = FileSpec.builder("com.example", "Test") + .addAliasedImport(squareTaco, "SquareTaco") + .addAliasedImport(blockTaco, "BlockTaco") + .addAliasedImport(kotlinIsNullOrEmpty, "kotlinIsNullOrEmpty") + .addAliasedImport(cashIsNullOrEmpty, "cashIsNullOrEmpty") + .addFunction( + FunSpec.builder("main") + .addStatement("val squareTaco = %L", squareTaco.constructorReference()) + .addStatement("val blockTaco = %L", blockTaco.constructorReference()) + .addStatement("val isSquareTacoNull = %S.%M()", "Taco", kotlinIsNullOrEmpty) + .addStatement("val isBlockTacoNull = %S.%M()", "Taco", cashIsNullOrEmpty) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.example + | + |import kotlin.Unit + |import com.squareup.cash.util.isNullOrEmpty as cashIsNullOrEmpty + |import com.squareup.tacos.Taco as SquareTaco + |import kotlin.text.isNullOrEmpty as kotlinIsNullOrEmpty + |import xyz.block.tacos.Taco as BlockTaco + | + |public fun main(): Unit { + | val squareTaco = ::SquareTaco + | val blockTaco = ::BlockTaco + | val isSquareTacoNull = "Taco".kotlinIsNullOrEmpty() + | val isBlockTacoNull = "Taco".cashIsNullOrEmpty() + |} + | + """.trimMargin(), + ) + } +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/LambdaTypeNameTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/LambdaTypeNameTest.kt new file mode 100644 index 00000000..5b72b79d --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/LambdaTypeNameTest.kt @@ -0,0 +1,195 @@ +/* + * Copyright (C) 2017 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.truth.Truth.assertThat +import com.squareup.kotlinpoet.KModifier.VARARG +import javax.annotation.Nullable +import kotlin.test.Test + +@OptIn(ExperimentalKotlinPoetApi::class) +class LambdaTypeNameTest { + + @Retention(AnnotationRetention.RUNTIME) + annotation class HasSomeAnnotation + + @HasSomeAnnotation + inner class IsAnnotated + + @Test fun receiverWithoutAnnotationHasNoParens() { + val typeName = LambdaTypeName.get( + receiver = Int::class.asClassName(), + parameters = listOf(), + returnType = Unit::class.asTypeName(), + ) + assertThat(typeName.toString()).isEqualTo("kotlin.Int.() -> kotlin.Unit") + } + + @Test fun receiverWithAnnotationHasParens() { + val annotation = IsAnnotated::class.java.getAnnotation(HasSomeAnnotation::class.java) + val typeName = LambdaTypeName.get( + receiver = Int::class.asClassName().copy( + annotations = listOf(AnnotationSpec.get(annotation, includeDefaultValues = true)), + ), + parameters = listOf(), + returnType = Unit::class.asTypeName(), + ) + assertThat(typeName.toString()).isEqualTo( + "(@com.squareup.kotlinpoet.LambdaTypeNameTest.HasSomeAnnotation kotlin.Int).() -> kotlin.Unit", + ) + } + + @Test fun contextReceiver() { + val typeName = LambdaTypeName.get( + receiver = Int::class.asTypeName(), + parameters = listOf(), + returnType = Unit::class.asTypeName(), + contextReceivers = listOf(STRING), + ) + assertThat(typeName.toString()).isEqualTo( + "context(kotlin.String) kotlin.Int.() -> kotlin.Unit", + ) + } + + @Test fun nullableFunctionWithContextReceiver() { + val typeName = LambdaTypeName.get( + receiver = Int::class.asTypeName(), + parameters = listOf(), + returnType = Unit::class.asTypeName(), + contextReceivers = listOf(STRING), + ).copy(nullable = true) + assertThat(typeName.toString()).isEqualTo( + "(context(kotlin.String) kotlin.Int.() -> kotlin.Unit)?", + ) + } + + @Test fun suspendingFunctionWithContextReceiver() { + val typeName = LambdaTypeName.get( + receiver = Int::class.asTypeName(), + parameters = listOf(), + returnType = Unit::class.asTypeName(), + contextReceivers = listOf(STRING), + ).copy(suspending = true) + assertThat(typeName.toString()).isEqualTo( + "suspend context(kotlin.String) kotlin.Int.() -> kotlin.Unit", + ) + } + + @Test fun functionWithMultipleContextReceivers() { + val typeName = LambdaTypeName.get( + Int::class.asTypeName(), + listOf(), + Unit::class.asTypeName(), + listOf(STRING, BOOLEAN), + ) + assertThat(typeName.toString()).isEqualTo( + "context(kotlin.String, kotlin.Boolean) kotlin.Int.() -> kotlin.Unit", + ) + } + + @Test fun functionWithGenericContextReceiver() { + val genericType = TypeVariableName("T") + val typeName = LambdaTypeName.get( + Int::class.asTypeName(), + listOf(), + Unit::class.asTypeName(), + listOf(genericType), + ) + + assertThat(typeName.toString()).isEqualTo( + "context(T) kotlin.Int.() -> kotlin.Unit", + ) + } + + @Test fun functionWithAnnotatedContextReceiver() { + val annotatedType = STRING.copy(annotations = listOf(AnnotationSpec.get(FunSpecTest.TestAnnotation()))) + val typeName = LambdaTypeName.get( + Int::class.asTypeName(), + listOf(), + Unit::class.asTypeName(), + listOf(annotatedType), + ) + + assertThat(typeName.toString()).isEqualTo( + "context(@com.squareup.kotlinpoet.FunSpecTest.TestAnnotation kotlin.String) kotlin.Int.() -> kotlin.Unit", + ) + } + + @Test fun paramsWithAnnotationsForbidden() { + assertThrows<IllegalArgumentException> { + LambdaTypeName.get( + parameters = arrayOf( + ParameterSpec.builder("foo", Int::class) + .addAnnotation(Nullable::class) + .build(), + ), + returnType = Unit::class.asTypeName(), + ) + }.hasMessageThat().isEqualTo("Parameters with annotations are not allowed") + } + + @Test fun paramsWithModifiersForbidden() { + assertThrows<IllegalArgumentException> { + LambdaTypeName.get( + parameters = arrayOf( + ParameterSpec.builder("foo", Int::class) + .addModifiers(VARARG) + .build(), + ), + returnType = Unit::class.asTypeName(), + ) + }.hasMessageThat().isEqualTo("Parameters with modifiers are not allowed") + } + + @Test fun paramsWithDefaultValueForbidden() { + assertThrows<IllegalArgumentException> { + LambdaTypeName.get( + parameters = arrayOf( + ParameterSpec.builder("foo", Int::class) + .defaultValue("42") + .build(), + ), + returnType = Unit::class.asTypeName(), + ) + }.hasMessageThat().isEqualTo("Parameters with default values are not allowed") + } + + @Test fun lambdaReturnType() { + val returnTypeName = LambdaTypeName.get( + parameters = arrayOf(Int::class.asTypeName()), + returnType = Unit::class.asTypeName(), + ) + val typeName = LambdaTypeName.get( + parameters = arrayOf(Int::class.asTypeName()), + returnType = returnTypeName, + ) + assertThat(typeName.toString()) + .isEqualTo("(kotlin.Int) -> ((kotlin.Int) -> kotlin.Unit)") + } + + @Test fun lambdaParameterType() { + val parameterTypeName = LambdaTypeName.get( + parameters = arrayOf(Int::class.asTypeName()), + returnType = Int::class.asTypeName(), + ) + val typeName = LambdaTypeName.get( + parameters = arrayOf(parameterTypeName), + returnType = Unit::class.asTypeName(), + ) + assertThat(typeName.toString()) + .isEqualTo("((kotlin.Int) -> kotlin.Int) -> kotlin.Unit") + } +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/LineWrapperTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/LineWrapperTest.kt new file mode 100644 index 00000000..8fdbda83 --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/LineWrapperTest.kt @@ -0,0 +1,232 @@ +/* + * Copyright (C) 2016 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test + +class LineWrapperTest { + @Test fun wrap() { + val out = StringBuffer() + val lineWrapper = LineWrapper(out, " ", 10) + lineWrapper.append("abcde fghij", indentLevel = 2) + lineWrapper.close() + assertThat(out.toString()).isEqualTo( + """ + |abcde + | fghij + """.trimMargin(), + ) + } + + @Test fun noWrap() { + val out = StringBuffer() + val lineWrapper = LineWrapper(out, " ", 10) + lineWrapper.append("abcde fghi", indentLevel = 2) + lineWrapper.close() + assertThat(out.toString()).isEqualTo("abcde fghi") + } + + @Test fun multipleWrite() { + val out = StringBuffer() + val lineWrapper = LineWrapper(out, " ", 10) + lineWrapper.append("ab cd ef gh ij kl mn op qr", indentLevel = 1) + lineWrapper.close() + assertThat(out.toString()).isEqualTo( + """ + |ab cd ef + | gh ij kl + | mn op qr + """.trimMargin(), + ) + } + + @Test fun fencepost() { + val out = StringBuffer() + val lineWrapper = LineWrapper(out, " ", 10) + lineWrapper.append("abcde", indentLevel = 2) + lineWrapper.append("fghij k", indentLevel = 2) + lineWrapper.append("lmnop", indentLevel = 2) + lineWrapper.close() + assertThat(out.toString()).isEqualTo( + """ + |abcdefghij + | klmnop + """.trimMargin(), + ) + } + + @Test fun overlyLongLinesWithoutLeadingSpace() { + val out = StringBuffer() + val lineWrapper = LineWrapper(out, " ", 10) + lineWrapper.append("abcdefghijkl", indentLevel = 2) + lineWrapper.close() + assertThat(out.toString()).isEqualTo("abcdefghijkl") + } + + @Test fun overlyLongLinesWithLeadingSpace() { + val out = StringBuffer() + val lineWrapper = LineWrapper(out, " ", 10) + lineWrapper.append(" abcdefghijkl", indentLevel = 2) + lineWrapper.close() + assertThat(out.toString()).isEqualTo("\n abcdefghijkl") + } + + @Test fun noWrapEmbeddedNewlines() { + val out = StringBuffer() + val lineWrapper = LineWrapper(out, " ", 10) + lineWrapper.append("abcde fghi\njklmn", indentLevel = 2) + lineWrapper.append("opqrstuvwxy", indentLevel = 2) + lineWrapper.close() + assertThat(out.toString()).isEqualTo( + """ + |abcde fghi + |jklmnopqrstuvwxy + """.trimMargin(), + ) + } + + @Test fun wrapEmbeddedNewlines() { + val out = StringBuffer() + val lineWrapper = LineWrapper(out, " ", 10) + lineWrapper.append("abcde fghij\nklmn", indentLevel = 2) + lineWrapper.append("opqrstuvwxy", indentLevel = 2) + lineWrapper.close() + assertThat(out.toString()).isEqualTo( + """ + |abcde + | fghij + |klmnopqrstuvwxy + """.trimMargin(), + ) + } + + @Test fun noWrapMultipleNewlines() { + val out = StringBuffer() + val lineWrapper = LineWrapper(out, " ", 10) + lineWrapper.append("abcde fghi\nklmnopq\nr stuvwxyz", indentLevel = 2) + lineWrapper.close() + assertThat(out.toString()).isEqualTo( + """ + |abcde fghi + |klmnopq + |r stuvwxyz + """.trimMargin(), + ) + } + + @Test fun wrapMultipleNewlines() { + val out = StringBuffer() + val lineWrapper = LineWrapper(out, " ", 10) + lineWrapper.append("abcde fghi\nklmnopq\nrs tuvwxyz1", indentLevel = 2) + lineWrapper.close() + assertThat(out.toString()).isEqualTo( + """ + |abcde fghi + |klmnopq + |rs + | tuvwxyz1 + """.trimMargin(), + ) + } + + @Test fun noWrapPrecedingUnaryPlus() { + val out = StringBuffer() + val lineWrapper = LineWrapper(out, " ", 10) + lineWrapper.append("a + b + c", indentLevel = 2) + lineWrapper.close() + assertThat(out.toString()).isEqualTo( + """ + |a + + | b + + | c + """.trimMargin(), + ) + } + + @Test fun noWrapPrecedingUnaryMinus() { + val out = StringBuffer() + val lineWrapper = LineWrapper(out, " ", 10) + lineWrapper.append("a - b - c", indentLevel = 2) + lineWrapper.close() + assertThat(out.toString()).isEqualTo( + """ + |a - + | b - + | c + """.trimMargin(), + ) + } + + @Test fun appendNonWrapping() { + val out = StringBuffer() + val lineWrapper = LineWrapper(out, " ", 10) + lineWrapper.append("ab cd ef", indentLevel = 2) + lineWrapper.appendNonWrapping("gh ij kl mn") + lineWrapper.close() + assertThat(out.toString()).isEqualTo( + """ + |ab cd + | efgh ij kl mn + """.trimMargin(), + ) + } + + @Test fun appendNonWrappingSpace() { + val out = StringBuffer() + val lineWrapper = LineWrapper(out, " ", 10) + lineWrapper.append("ab cd ef", indentLevel = 2) + lineWrapper.append("gh·ij·kl·mn", indentLevel = 2) + lineWrapper.close() + assertThat(out.toString()).isEqualTo( + """ + |ab cd + | efgh ij kl mn + """.trimMargin(), + ) + } + + @Test fun loneUnsafeUnaryOperator() { + val out = StringBuffer() + val lineWrapper = LineWrapper(out, " ", 10) + lineWrapper.append(" -1", indentLevel = 2) + lineWrapper.close() + assertThat(out.toString()).isEqualTo( + """ + | -1 + """.trimMargin(), + ) + } + + @Test fun linePrefix() { + val out = StringBuffer() + val lineWrapper = LineWrapper(out, " ", 10) + lineWrapper.append("/**\n") + lineWrapper.append(" * ") + lineWrapper.append("a b c d e f g h i j k l m n\n", linePrefix = " * ") + lineWrapper.append(" */") + lineWrapper.close() + assertThat(out.toString()).isEqualTo( + """ + |/** + | * a b c d + | * e f g h i j + | * k l m n + | */ + """.trimMargin(), + ) + } +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/LineWrappingTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/LineWrappingTest.kt new file mode 100644 index 00000000..332a25b9 --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/LineWrappingTest.kt @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class LineWrappingTest { + @Test fun codeSpacesWrap() { + val wrapMe = FunSpec.builder("wrapMe") + .addStatement( + "return %L * %L * %L * %L * %L * %L * %L * %L * %L * %L * %L * %L", + 10000000000, 20000000000, 30000000000, 40000000000, 50000000000, 60000000000, + 70000000000, 80000000000, 90000000000, 10000000000, 20000000000, 30000000000, + ) + .build() + assertThat(toString(wrapMe)).isEqualTo( + """ + |package com.squareup.tacos + | + |public fun wrapMe() = 10_000_000_000 * 20_000_000_000 * 30_000_000_000 * 40_000_000_000 * + | 50_000_000_000 * 60_000_000_000 * 70_000_000_000 * 80_000_000_000 * 90_000_000_000 * + | 10_000_000_000 * 20_000_000_000 * 30_000_000_000 + | + """.trimMargin(), + ) + } + + @Test fun stringSpacesDoNotWrap() { + val wrapMe = FunSpec.builder("wrapMe") + .addStatement( + "return %S+%S+%S+%S+%S+%S+%S+%S+%S+%S+%S+%S", + "Aaaa Aaaa", "Bbbb Bbbb", "Cccc Cccc", "Dddd Dddd", "Eeee Eeee", "Ffff Ffff", + "Gggg Gggg", "Hhhh Hhhh", "Iiii Iiii", "Jjjj Jjjj", "Kkkk Kkkk", "Llll Llll", + ) + .build() + assertThat(toString(wrapMe)).isEqualTo( + """ + |package com.squareup.tacos + | + |public fun wrapMe() = + | "Aaaa Aaaa"+"Bbbb Bbbb"+"Cccc Cccc"+"Dddd Dddd"+"Eeee Eeee"+"Ffff Ffff"+"Gggg Gggg"+"Hhhh Hhhh"+"Iiii Iiii"+"Jjjj Jjjj"+"Kkkk Kkkk"+"Llll Llll" + | + """.trimMargin(), + ) + } + + @Test fun nonwrappingWhitespaceDoesNotWrap() { + val wrapMe = FunSpec.builder("wrapMe") + .addStatement( + "return %L·*·%L·*·%L·*·%L·*·%L·*·%L·*·%L·*·%L·*·%L·*·%L·*·%L·*·%L", + 10000000000, 20000000000, 30000000000, 40000000000, 50000000000, 60000000000, + 70000000000, 80000000000, 90000000000, 10000000000, 20000000000, 30000000000, + ) + .build() + assertThat(toString(wrapMe)).isEqualTo( + """ + |package com.squareup.tacos + | + |public fun wrapMe() = + | 10_000_000_000 * 20_000_000_000 * 30_000_000_000 * 40_000_000_000 * 50_000_000_000 * 60_000_000_000 * 70_000_000_000 * 80_000_000_000 * 90_000_000_000 * 10_000_000_000 * 20_000_000_000 * 30_000_000_000 + | + """.trimMargin(), + ) + } + + @Test fun nonwrappingWhitespaceIsRetainedInStrings() { + val wrapMe = FunSpec.builder("wrapMe") + .addStatement("return %S", "a·b") + .build() + assertThat(toString(wrapMe)).isEqualTo( + """ + |package com.squareup.tacos + | + |public fun wrapMe() = "a·b" + | + """.trimMargin(), + ) + } + + @Test fun insignificantWhitespaceRetained() { + val wrapMe = FunSpec.builder("wrapMe") + .addStatement("val a = 8") + .addStatement("val b = 64") + .addStatement("val c = 512") + .addStatement("val d = 4096") + .build() + assertThat(toString(wrapMe)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + | + |public fun wrapMe(): Unit { + | val a = 8 + | val b = 64 + | val c = 512 + | val d = 4096 + |} + | + """.trimMargin(), + ) + } + + @Test fun spacesPrecedingUnaryOperatorsDoNotWrap() { + val wrapMe = FunSpec.builder("wrapMe") + .addStatement("val aaaaaa = %S +1", "x".repeat(80)) + .addStatement("val bbbbbb = %S +1", "x".repeat(81)) + .addStatement("val cccccc = %S -1", "x".repeat(80)) + .addStatement("val dddddd = %S -1", "x".repeat(81)) + .build() + assertThat(toString(wrapMe)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + | + |public fun wrapMe(): Unit { + | val aaaaaa = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +1 + | val bbbbbb = + | "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" +1 + | val cccccc = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -1 + | val dddddd = + | "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" -1 + |} + | + """.trimMargin(), + ) + } + + @Test fun parameterWrapping() { + val funSpecBuilder = FunSpec.builder("call") + funSpecBuilder.addCode("«call(") + for (i in 0..31) { + funSpecBuilder.addParameter("s$i", String::class) + funSpecBuilder.addCode(if (i > 0) ", %S" else "%S", i) + } + funSpecBuilder.addCode(")»\n") + + val taco = TypeSpec.classBuilder("Taco") + .addFunction(funSpecBuilder.build()) + .build() + assertThat(toString(taco)) + .isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.Unit + | + |public class Taco { + | public fun call( + | s0: String, + | s1: String, + | s2: String, + | s3: String, + | s4: String, + | s5: String, + | s6: String, + | s7: String, + | s8: String, + | s9: String, + | s10: String, + | s11: String, + | s12: String, + | s13: String, + | s14: String, + | s15: String, + | s16: String, + | s17: String, + | s18: String, + | s19: String, + | s20: String, + | s21: String, + | s22: String, + | s23: String, + | s24: String, + | s25: String, + | s26: String, + | s27: String, + | s28: String, + | s29: String, + | s30: String, + | s31: String, + | ): Unit { + | call("0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13", "14", "15", "16", + | "17", "18", "19", "20", "21", "22", "23", "24", "25", "26", "27", "28", "29", "30", "31") + | } + |} + | + """.trimMargin(), + ) + } + + private fun toString(typeSpec: TypeSpec): String { + return FileSpec.get("com.squareup.tacos", typeSpec).toString() + } + + private fun toString(funSpec: FunSpec): String { + val fileSpec = FileSpec.builder("com.squareup.tacos", "${funSpec.name}.kt") + .addFunction(funSpec) + .build() + return fileSpec.toString() + } +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/MemberNameTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/MemberNameTest.kt new file mode 100644 index 00000000..308aeb2c --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/MemberNameTest.kt @@ -0,0 +1,612 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.truth.Truth.assertThat +import com.squareup.kotlinpoet.KModifier.OVERRIDE +import com.squareup.kotlinpoet.MemberName.Companion.member +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import org.junit.Before +import org.junit.Test + +class MemberNameTest { + @Test fun memberNames() { + val randomTaco = MemberName("com.squareup.tacos", "randomTaco") + val bestTacoEver = MemberName("com.squareup.tacos", "bestTacoEver") + val funSpec = FunSpec.builder("makeTastyTacos") + .addStatement("val randomTaco = %M()", randomTaco) + .addStatement("val bestTaco = %M", bestTacoEver) + .build() + val file = FileSpec.builder("com.example", "Tacos") + .addFunction(funSpec) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.example + | + |import com.squareup.tacos.bestTacoEver + |import com.squareup.tacos.randomTaco + |import kotlin.Unit + | + |public fun makeTastyTacos(): Unit { + | val randomTaco = randomTaco() + | val bestTaco = bestTacoEver + |} + | + """.trimMargin(), + ) + } + + @Test fun memberInsideCompanionObject() { + val companion = ClassName("com.squareup.tacos", "Taco").nestedClass("Companion") + val createTaco = MemberName(companion, "createTaco") + val file = FileSpec.builder("com.example", "Tacos") + .addFunction( + FunSpec.builder("makeTastyTacos") + .addStatement("%M()", createTaco) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.example + | + |import com.squareup.tacos.Taco.Companion.createTaco + |import kotlin.Unit + | + |public fun makeTastyTacos(): Unit { + | createTaco() + |} + | + """.trimMargin(), + ) + } + + @Test fun memberInsideSamePackage() { + val createTaco = MemberName("com.squareup.tacos", "createTaco") + val file = FileSpec.builder("com.squareup.tacos", "Tacos") + .addFunction( + FunSpec.builder("makeTastyTacos") + .addStatement("%M()", createTaco) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + | + |public fun makeTastyTacos(): Unit { + | createTaco() + |} + | + """.trimMargin(), + ) + } + + @Test fun memberInsideClassInSamePackage() { + val createTaco = MemberName( + ClassName("com.squareup.tacos", "Town"), + "createTaco", + ) + val file = FileSpec.builder("com.squareup.tacos", "Tacos") + .addFunction( + FunSpec.builder("makeTastyTacos") + .addStatement("%M()", createTaco) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import com.squareup.tacos.Town.createTaco + |import kotlin.Unit + | + |public fun makeTastyTacos(): Unit { + | createTaco() + |} + | + """.trimMargin(), + ) + } + + @Test fun memberNamesClash() { + val createSquareTaco = MemberName("com.squareup.tacos", "createTaco") + val createTwitterTaco = MemberName("com.twitter.tacos", "createTaco") + val file = FileSpec.builder("com.example", "Tacos") + .addFunction( + FunSpec.builder("makeTastyTacos") + .addStatement("%M()", createSquareTaco) + .addStatement("%M()", createTwitterTaco) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.example + | + |import kotlin.Unit + |import com.squareup.tacos.createTaco as squareupTacosCreateTaco + |import com.twitter.tacos.createTaco as twitterTacosCreateTaco + | + |public fun makeTastyTacos(): Unit { + | squareupTacosCreateTaco() + | twitterTacosCreateTaco() + |} + | + """.trimMargin(), + ) + } + + @Test fun memberNamesInsideCompanionsClash() { + val squareTacos = ClassName("com.squareup.tacos", "SquareTacos") + val twitterTacos = ClassName("com.twitter.tacos", "TwitterTacos") + val createSquareTaco = MemberName(squareTacos.nestedClass("Companion"), "createTaco") + val createTwitterTaco = MemberName(twitterTacos.nestedClass("Companion"), "createTaco") + val file = FileSpec.builder("com.example", "Tacos") + .addFunction( + FunSpec.builder("makeTastyTacos") + .addStatement("%M()", createSquareTaco) + .addStatement("%M()", createTwitterTaco) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.example + | + |import kotlin.Unit + |import com.squareup.tacos.SquareTacos.Companion.createTaco as squareupTacosCreateTaco + |import com.twitter.tacos.TwitterTacos.Companion.createTaco as twitterTacosCreateTaco + | + |public fun makeTastyTacos(): Unit { + | squareupTacosCreateTaco() + | twitterTacosCreateTaco() + |} + | + """.trimMargin(), + ) + } + + @Test fun memberAndClassNamesClash() { + val squareTacosClass = ClassName("com.squareup.tacos", "SquareTacos") + val squareTacosFunction = MemberName("com.squareup.tacos.math", "SquareTacos") + val file = FileSpec.builder("com.example", "Tacos") + .addFunction( + FunSpec.builder("makeTastyTacos") + .addStatement("val tacos = %T()", squareTacosClass) + .addStatement("%M(tacos)", squareTacosFunction) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.example + | + |import com.squareup.tacos.SquareTacos + |import kotlin.Unit + | + |public fun makeTastyTacos(): Unit { + | val tacos = SquareTacos() + | com.squareup.tacos.math.SquareTacos(tacos) + |} + | + """.trimMargin(), + ) + } + + @Test fun importedMemberAndClassFunctionNameClash() { + val kotlinErrorMember = MemberName("kotlin", "error") + val file = FileSpec.builder("com.squareup.tacos", "TacoTest") + .addType( + TypeSpec.classBuilder("TacoTest") + .addFunction( + FunSpec.builder("test") + .addStatement("%M(%S)", kotlinErrorMember, "errorText") + .build(), + ) + .addFunction( + FunSpec + .builder("error") + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + | + |public class TacoTest { + | public fun test(): Unit { + | kotlin.error("errorText") + | } + | + | public fun error(): Unit { + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun importedMemberAndSuperClassFunctionNameClashForInnerClass() { + val kotlinErrorMember = MemberName("kotlin", "error") + val file = FileSpec.builder("com.squareup.tacos", "TacoTest") + .addType( + TypeSpec.classBuilder("Test") + .addFunction( + FunSpec + .builder("error") + .build(), + ) + .addType( + TypeSpec.classBuilder("TacoTest") + .addModifiers(KModifier.INNER) + .addFunction( + FunSpec.builder("test") + .addStatement("%M(%S)", kotlinErrorMember, "errorText") + .build(), + ) + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + | + |public class Test { + | public fun error(): Unit { + | } + | + | public inner class TacoTest { + | public fun test(): Unit { + | kotlin.error("errorText") + | } + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun importedMemberAndSuperClassFunctionNameDontClashForNonInnerClass() { + val kotlinErrorMember = MemberName("kotlin", "error") + val file = FileSpec.builder("com.squareup.tacos", "TacoTest") + .addType( + TypeSpec.classBuilder("Test") + .addFunction( + FunSpec + .builder("error") + .build(), + ) + .addType( + TypeSpec.classBuilder("TacoTest") + .addFunction( + FunSpec.builder("test") + .addStatement("%M(%S)", kotlinErrorMember, "errorText") + .build(), + ) + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + |import kotlin.error + | + |public class Test { + | public fun error(): Unit { + | } + | + | public class TacoTest { + | public fun test(): Unit { + | error("errorText") + | } + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun memberNameAliases() { + val createSquareTaco = MemberName("com.squareup.tacos", "createTaco") + val createTwitterTaco = MemberName("com.twitter.tacos", "createTaco") + val file = FileSpec.builder("com.example", "Tacos") + .addAliasedImport(createSquareTaco, "createSquareTaco") + .addAliasedImport(createTwitterTaco, "createTwitterTaco") + .addFunction( + FunSpec.builder("makeTastyTacos") + .addStatement("%M()", createSquareTaco) + .addStatement("%M()", createTwitterTaco) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.example + | + |import kotlin.Unit + |import com.squareup.tacos.createTaco as createSquareTaco + |import com.twitter.tacos.createTaco as createTwitterTaco + | + |public fun makeTastyTacos(): Unit { + | createSquareTaco() + | createTwitterTaco() + |} + | + """.trimMargin(), + ) + } + + @Test fun keywordsEscaping() { + val `when` = MemberName("org.mockito", "when") + val file = FileSpec.builder("com.squareup.tacos", "TacoTest") + .addType( + TypeSpec.classBuilder("TacoTest") + .addFunction( + FunSpec.builder("setUp") + .addAnnotation(Before::class) + .addStatement("%M(tacoService.createTaco()).thenReturn(tastyTaco())", `when`) + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + |import org.junit.Before + |import org.mockito.`when` + | + |public class TacoTest { + | @Before + | public fun setUp(): Unit { + | `when`(tacoService.createTaco()).thenReturn(tastyTaco()) + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun clashingNamesKeywordsEscaping() { + val squareTacos = ClassName("com.squareup.tacos", "SquareTacos") + val twitterTacos = ClassName("com.twitter.tacos", "TwitterTacos") + val whenSquareTaco = MemberName(squareTacos.nestedClass("Companion"), "when") + val whenTwitterTaco = MemberName(twitterTacos.nestedClass("Companion"), "when") + val file = FileSpec.builder("com.example", "Tacos") + .addFunction( + FunSpec.builder("whenTastyTacos") + .addStatement("%M()", whenSquareTaco) + .addStatement("%M()", whenTwitterTaco) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.example + | + |import kotlin.Unit + |import com.squareup.tacos.SquareTacos.Companion.`when` as squareupTacosWhen + |import com.twitter.tacos.TwitterTacos.Companion.`when` as twitterTacosWhen + | + |public fun whenTastyTacos(): Unit { + | squareupTacosWhen() + | twitterTacosWhen() + |} + | + """.trimMargin(), + ) + } + + @Test fun memberReferences() { + val randomTaco = MemberName("com.squareup.tacos", "randomTaco") + val bestTacoEver = ClassName("com.squareup.tacos", "TacoTruck") + .member("bestTacoEver") + val funSpec = FunSpec.builder("makeTastyTacos") + .addStatement("val randomTacoFactory = %L", randomTaco.reference()) + .addStatement("val bestTacoFactory = %L", bestTacoEver.reference()) + .build() + val file = FileSpec.builder("com.example", "Tacos") + .addFunction(funSpec) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.example + | + |import com.squareup.tacos.TacoTruck + |import com.squareup.tacos.randomTaco + |import kotlin.Unit + | + |public fun makeTastyTacos(): Unit { + | val randomTacoFactory = ::randomTaco + | val bestTacoFactory = TacoTruck::bestTacoEver + |} + | + """.trimMargin(), + ) + } + + @Test fun spacesEscaping() { + val produceTacos = MemberName("com.squareup.taco factory", "produce tacos") + val file = FileSpec.builder("com.squareup.tacos", "TacoTest") + .addFunction( + FunSpec.builder("main") + .addStatement("println(%M())", produceTacos) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import com.squareup.`taco factory`.`produce tacos` + |import kotlin.Unit + | + |public fun main(): Unit { + | println(`produce tacos`()) + |} + | + """.trimMargin(), + ) + } + + @Test fun memberExtension_className() { + val regex = ClassName("kotlin.text", "Regex") + assertThat(regex.member("fromLiteral")) + .isEqualTo(MemberName(regex, "fromLiteral")) + } + + @Test fun memberExtension_kclass() { + assertThat(Regex::class.member("fromLiteral")) + .isEqualTo(MemberName(ClassName("kotlin.text", "Regex"), "fromLiteral")) + } + + @Test fun memberExtension_class() { + assertThat(Regex::class.java.member("fromLiteral")) + .isEqualTo(MemberName(ClassName("kotlin.text", "Regex"), "fromLiteral")) + } + + @Test fun `%N escapes MemberNames`() { + val taco = ClassName("com.squareup.tacos", "Taco") + val packager = ClassName("com.squareup.tacos", "TacoPackager") + val file = FileSpec.builder("com.example", "Test") + .addFunction( + FunSpec.builder("packageTacos") + .addParameter("tacos", LIST.parameterizedBy(taco)) + .addParameter("packager", packager) + .addStatement("packager.%N(tacos)", packager.member("package")) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.example + | + |import com.squareup.tacos.Taco + |import com.squareup.tacos.TacoPackager + |import kotlin.Unit + |import kotlin.collections.List + | + |public fun packageTacos(tacos: List<Taco>, packager: TacoPackager): Unit { + | packager.`package`(tacos) + |} + | + """.trimMargin(), + ) + } + + @Test fun importOperator() { + val taco = ClassName("com.squareup.tacos", "Taco") + val meat = ClassName("com.squareup.tacos.ingredient", "Meat") + val iterator = MemberName("com.squareup.tacos.internal", KOperator.ITERATOR) + val minusAssign = MemberName("com.squareup.tacos.internal", KOperator.MINUS_ASSIGN) + val file = FileSpec.builder("com.example", "Test") + .addFunction( + FunSpec.builder("makeTacoHealthy") + .addParameter("taco", taco) + .beginControlFlow("for (ingredient %M taco)", iterator) + .addStatement("if (ingredient is %T) taco %M ingredient", meat, minusAssign) + .endControlFlow() + .addStatement("return taco") + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.example + | + |import com.squareup.tacos.Taco + |import com.squareup.tacos.`internal`.iterator + |import com.squareup.tacos.`internal`.minusAssign + |import com.squareup.tacos.ingredient.Meat + |import kotlin.Unit + | + |public fun makeTacoHealthy(taco: Taco): Unit { + | for (ingredient in taco) { + | if (ingredient is Meat) taco -= ingredient + | } + | return taco + |} + | + """.trimMargin(), + ) + } + + // https://github.com/square/kotlinpoet/issues/1089 + @Test fun `extension MemberName imported if name clash`() { + val hashCode = MemberName("kotlin", "hashCode", isExtension = true) + val file = FileSpec.builder("com.squareup.tacos", "Message") + .addType( + TypeSpec.classBuilder("Message") + .addFunction( + FunSpec.builder("hashCode") + .addModifiers(OVERRIDE) + .returns(INT) + .addCode( + buildCodeBlock { + addStatement("var result = super.hashCode") + beginControlFlow("if (result == 0)") + addStatement("result = result * 37 + embedded_message.%M()", hashCode) + addStatement("super.hashCode = result") + endControlFlow() + addStatement("return result") + }, + ) + .build(), + ) + .build(), + ) + .build() + //language=kotlin + assertThat(file.toString()).isEqualTo( + """ + package com.squareup.tacos + + import kotlin.Int + import kotlin.hashCode + + public class Message { + public override fun hashCode(): Int { + var result = super.hashCode + if (result == 0) { + result = result * 37 + embedded_message.hashCode() + super.hashCode = result + } + return result + } + } + + """.trimIndent(), + ) + } +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/NameAllocatorTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/NameAllocatorTest.kt new file mode 100644 index 00000000..28c7d34d --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/NameAllocatorTest.kt @@ -0,0 +1,100 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test + +class NameAllocatorTest { + @Test fun usage() { + val nameAllocator = NameAllocator() + assertThat(nameAllocator.newName("foo", 1)).isEqualTo("foo") + assertThat(nameAllocator.newName("bar", 2)).isEqualTo("bar") + assertThat(nameAllocator[1]).isEqualTo("foo") + assertThat(nameAllocator[2]).isEqualTo("bar") + } + + @Test fun nameCollision() { + val nameAllocator = NameAllocator() + assertThat(nameAllocator.newName("foo")).isEqualTo("foo") + assertThat(nameAllocator.newName("foo")).isEqualTo("foo_") + assertThat(nameAllocator.newName("foo")).isEqualTo("foo__") + } + + @Test fun nameCollisionWithTag() { + val nameAllocator = NameAllocator() + assertThat(nameAllocator.newName("foo", 1)).isEqualTo("foo") + assertThat(nameAllocator.newName("foo", 2)).isEqualTo("foo_") + assertThat(nameAllocator.newName("foo", 3)).isEqualTo("foo__") + assertThat(nameAllocator[1]).isEqualTo("foo") + assertThat(nameAllocator[2]).isEqualTo("foo_") + assertThat(nameAllocator[3]).isEqualTo("foo__") + } + + @Test fun characterMappingSubstitute() { + val nameAllocator = NameAllocator() + assertThat(nameAllocator.newName("a-b", 1)).isEqualTo("a_b") + } + + @Test fun characterMappingSurrogate() { + val nameAllocator = NameAllocator() + assertThat(nameAllocator.newName("a\uD83C\uDF7Ab", 1)).isEqualTo("a_b") + } + + @Test fun characterMappingInvalidStartButValidPart() { + val nameAllocator = NameAllocator() + assertThat(nameAllocator.newName("1ab", 1)).isEqualTo("_1ab") + } + + @Test fun characterMappingInvalidStartIsInvalidPart() { + val nameAllocator = NameAllocator() + assertThat(nameAllocator.newName("&ab", 1)).isEqualTo("_ab") + } + + @Test fun kotlinKeyword() { + val nameAllocator = NameAllocator() + assertThat(nameAllocator.newName("when", 1)).isEqualTo("when_") + assertThat(nameAllocator[1]).isEqualTo("when_") + } + + @Test fun tagReuseForbidden() { + val nameAllocator = NameAllocator() + nameAllocator.newName("foo", 1) + assertThrows<IllegalArgumentException> { + nameAllocator.newName("bar", 1) + }.hasMessageThat().isEqualTo("tag 1 cannot be used for both 'foo' and 'bar'") + } + + @Test fun useBeforeAllocateForbidden() { + val nameAllocator = NameAllocator() + assertThrows<IllegalArgumentException> { + nameAllocator[1] + }.hasMessageThat().isEqualTo("unknown tag: 1") + } + + @Test fun cloneUsage() { + val outerAllocator = NameAllocator() + outerAllocator.newName("foo", 1) + + val innerAllocator1 = outerAllocator.copy() + assertThat(innerAllocator1.newName("bar", 2)).isEqualTo("bar") + assertThat(innerAllocator1.newName("foo", 3)).isEqualTo("foo_") + + val innerAllocator2 = outerAllocator.copy() + assertThat(innerAllocator2.newName("foo", 2)).isEqualTo("foo_") + assertThat(innerAllocator2.newName("bar", 3)).isEqualTo("bar") + } +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/ParameterSpecTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/ParameterSpecTest.kt new file mode 100644 index 00000000..d764f441 --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/ParameterSpecTest.kt @@ -0,0 +1,180 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.truth.Truth.assertThat +import javax.lang.model.element.Modifier +import kotlin.test.Test +import kotlin.test.assertFailsWith + +class ParameterSpecTest { + @Test fun equalsAndHashCode() { + var a = ParameterSpec.builder("foo", Int::class) + .build() + var b = ParameterSpec.builder("foo", Int::class) + .build() + assertThat(a == b).isTrue() + assertThat(a.hashCode()).isEqualTo(b.hashCode()) + a = ParameterSpec.builder("i", Int::class) + .addModifiers(KModifier.NOINLINE) + .build() + b = ParameterSpec.builder("i", Int::class) + .addModifiers(KModifier.NOINLINE) + .build() + assertThat(a == b).isTrue() + assertThat(a.hashCode()).isEqualTo(b.hashCode()) + } + + @Test fun escapeKeywordInParameterName() { + val parameterSpec = ParameterSpec.builder("if", String::class) + .build() + assertThat(parameterSpec.toString()).isEqualTo("`if`: kotlin.String") + } + + @Test fun escapePunctuationInParameterName() { + val parameterSpec = ParameterSpec.builder("with-hyphen", String::class) + .build() + assertThat(parameterSpec.toString()).isEqualTo("`with-hyphen`: kotlin.String") + } + + @Test fun generalBuilderEqualityTest() { + val parameterSpec = ParameterSpec.builder("Nuts", String::class) + .addAnnotation(ClassName("com.squareup.kotlinpoet", "Food")) + .addModifiers(KModifier.VARARG) + .defaultValue("Almonds") + .build() + + assertThat(parameterSpec.toBuilder().build()).isEqualTo(parameterSpec) + } + + @Test fun modifyModifiers() { + val builder = ParameterSpec + .builder("word", String::class) + .addModifiers(KModifier.NOINLINE) + + builder.modifiers.clear() + builder.modifiers.add(KModifier.CROSSINLINE) + + assertThat(builder.build().modifiers).containsExactly(KModifier.CROSSINLINE) + } + + @Test fun modifyAnnotations() { + val builder = ParameterSpec + .builder("word", String::class) + .addAnnotation( + AnnotationSpec.builder(JvmName::class.asClassName()) + .addMember("name = %S", "jvmWord") + .build(), + ) + + val javaWord = AnnotationSpec.builder(JvmName::class.asClassName()) + .addMember("name = %S", "javaWord") + .build() + builder.annotations.clear() + builder.annotations.add(javaWord) + + assertThat(builder.build().annotations).containsExactly(javaWord) + } + + // https://github.com/square/kotlinpoet/issues/462 + @Test fun codeBlockDefaultValue() { + val param = ParameterSpec.builder("arg", ANY).build() + val defaultValue = CodeBlock.builder() + .beginControlFlow("{ %L ->", param) + .addStatement("println(\"arg=\$%N\")", param) + .endControlFlow() + .build() + val lambdaTypeName = ClassName.bestGuess("com.example.SomeTypeAlias") + val paramSpec = ParameterSpec.builder("parameter", lambdaTypeName) + .defaultValue(defaultValue) + .build() + assertThat(paramSpec.toString()).isEqualTo( + """ + |parameter: com.example.SomeTypeAlias = { arg: kotlin.Any -> + | println("arg=${'$'}arg") + |} + | + """.trimMargin(), + ) + } + + @Test fun annotatedLambdaType() { + val annotation = AnnotationSpec.builder(ClassName("com.squareup.tacos", "Annotation")).build() + val type = LambdaTypeName.get(returnType = UNIT).copy(annotations = listOf(annotation)) + val spec = FileSpec.builder("com.squareup.tacos", "Taco") + .addFunction( + FunSpec.builder("foo") + .addParameter("bar", type) + .build(), + ) + .build() + assertThat(spec.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + | + |public fun foo(bar: @Annotation () -> Unit): Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun doublePropertyInitialization() { + val codeBlockDefaultValue = ParameterSpec.builder("listA", String::class) + .defaultValue(CodeBlock.builder().add("foo").build()) + .defaultValue(CodeBlock.builder().add("bar").build()) + .build() + + assertThat(CodeBlock.of("bar")).isEqualTo(codeBlockDefaultValue.defaultValue) + + val formatDefaultValue = ParameterSpec.builder("listA", String::class) + .defaultValue("foo") + .defaultValue("bar") + .build() + + assertThat(CodeBlock.of("bar")).isEqualTo(formatDefaultValue.defaultValue) + } + + @Suppress("DEPRECATION_ERROR") + @Test + fun jvmModifiersAreNotAllowed() { + val e = assertFailsWith<IllegalArgumentException> { + ParameterSpec.builder("value", INT) + .jvmModifiers(listOf(Modifier.FINAL)) + .build() + } + assertThat(e).hasMessageThat().contains("JVM modifiers are not permitted on parameters in Kotlin") + } + + @Test + fun illegalModifiers() { + val builder = ParameterSpec.builder("value", INT) + + val e = assertFailsWith<IllegalArgumentException> { + // Legal + builder.addModifiers(KModifier.NOINLINE) + builder.addModifiers(KModifier.CROSSINLINE) + builder.addModifiers(KModifier.VARARG) + // Everything else is illegal + builder.addModifiers(KModifier.FINAL) + builder.addModifiers(KModifier.PRIVATE) + builder.build() + } + assertThat(e).hasMessageThat().contains("Modifiers [FINAL, PRIVATE] are not allowed on Kotlin parameters. Allowed modifiers: [VARARG, NOINLINE, CROSSINLINE]") + } +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/ParameterizedTypeNameTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/ParameterizedTypeNameTest.kt new file mode 100644 index 00000000..f4e5625b --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/ParameterizedTypeNameTest.kt @@ -0,0 +1,164 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.truth.Truth.assertThat +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.plusParameter +import java.io.Closeable +import kotlin.reflect.KClass +import kotlin.reflect.KFunction +import kotlin.reflect.KMutableProperty +import kotlin.reflect.KType +import kotlin.reflect.KTypeProjection +import kotlin.reflect.KVariance +import kotlin.reflect.full.createType +import org.junit.Test + +class ParameterizedTypeNameTest { + @Test fun classNamePlusParameter() { + val typeName = ClassName("kotlin.collections", "List") + .plusParameter(ClassName("kotlin", "String")) + assertThat(typeName.toString()).isEqualTo("kotlin.collections.List<kotlin.String>") + } + + @Test fun classNamePlusTwoParameters() { + val typeName = ClassName("kotlin.collections", "Map") + .plusParameter(ClassName("kotlin", "String")) + .plusParameter(ClassName("kotlin", "Int")) + assertThat(typeName.toString()).isEqualTo("kotlin.collections.Map<kotlin.String, kotlin.Int>") + } + + @Test fun classNamePlusTypeVariableParameter() { + val t = TypeVariableName("T") + val mapOfT = Map::class.asTypeName().plusParameter(t) + assertThat(mapOfT.toString()).isEqualTo("kotlin.collections.Map<T>") + } + + @Test fun kClassPlusParameter() { + val typeName = List::class.plusParameter(String::class) + assertThat(typeName.toString()).isEqualTo("kotlin.collections.List<kotlin.String>") + } + + @Test fun kClassPlusTwoParameters() { + val typeName = Map::class + .plusParameter(String::class) + .plusParameter(Int::class) + assertThat(typeName.toString()).isEqualTo("kotlin.collections.Map<kotlin.String, kotlin.Int>") + } + + @Test fun classPlusParameter() { + val typeName = java.util.List::class.java.plusParameter(java.lang.String::class.java) + assertThat(typeName.toString()).isEqualTo("java.util.List<java.lang.String>") + } + + @Test fun primitiveArray() { + assertThat(ByteArray::class.asTypeName().toString()).isEqualTo("kotlin.ByteArray") + assertThat(CharArray::class.asTypeName().toString()).isEqualTo("kotlin.CharArray") + assertThat(ShortArray::class.asTypeName().toString()).isEqualTo("kotlin.ShortArray") + assertThat(IntArray::class.asTypeName().toString()).isEqualTo("kotlin.IntArray") + assertThat(LongArray::class.asTypeName().toString()).isEqualTo("kotlin.LongArray") + assertThat(FloatArray::class.asTypeName().toString()).isEqualTo("kotlin.FloatArray") + assertThat(DoubleArray::class.asTypeName().toString()).isEqualTo("kotlin.DoubleArray") + } + + @Test fun arrayPlusPrimitiveParameter() { + val invariantInt = KTypeProjection(KVariance.INVARIANT, Int::class.createType()) + val typeName = Array<Unit>::class.createType(listOf(invariantInt)).asTypeName() + assertThat(typeName.toString()).isEqualTo("kotlin.Array<kotlin.Int>") + } + + @Test fun arrayPlusObjectParameter() { + val invariantCloseable = KTypeProjection(KVariance.INVARIANT, Closeable::class.createType()) + val typeName = Array<Unit>::class.createType(listOf(invariantCloseable)).asTypeName() + assertThat(typeName.toString()).isEqualTo("kotlin.Array<java.io.Closeable>") + } + + @Test fun arrayPlusNullableParameter() { + val invariantNullableCloseable = KTypeProjection(KVariance.INVARIANT, Closeable::class.createType(nullable = true)) + val typeName = Array<Unit>::class.createType(listOf(invariantNullableCloseable)).asTypeName() + assertThat(typeName.toString()).isEqualTo("kotlin.Array<java.io.Closeable?>") + } + + @Test fun typeParameter() { + val funWithParam: () -> Closeable = this::withParam + val typeName = (funWithParam as KFunction<*>).returnType.asTypeName() + assertThat(typeName.toString()).isEqualTo("Param") + } + + @Test fun nullableTypeParameter() { + val funWithParam: () -> Closeable? = this::withNullableParam + val typeName = (funWithParam as KFunction<*>).returnType.asTypeName() + assertThat(typeName.toString()).isEqualTo("Param?") + } + + @Test fun classPlusTwoParameters() { + val typeName = java.util.Map::class.java + .plusParameter(java.lang.String::class.java) + .plusParameter(java.lang.Integer::class.java) + assertThat(typeName.toString()).isEqualTo("java.util.Map<java.lang.String, java.lang.Integer>") + } + + @Test fun copyingTypeArguments() { + val typeName = java.util.Map::class.java + .plusParameter(java.lang.String::class.java) + .plusParameter(java.lang.Integer::class.java) + .nestedClass( + "Entry", + listOf( + java.lang.String::class.java.asClassName(), + java.lang.Integer::class.java.asClassName(), + ), + ) + .copy(typeArguments = listOf(STAR, STAR)) + assertThat(typeName.toString()).isEqualTo("java.util.Map<java.lang.String, java.lang.Integer>.Entry<*, *>") + } + + interface Projections { + val outVariance: KClass<out Annotation> + val inVariance: KClass<in Test> + val invariantNullable: KClass<Test>? + val star: KClass<*> + val multiVariant: Map<in String, List<Map<KClass<out Number>, *>?>> + val outAnyOnTypeWithoutBoundsAndVariance: KMutableProperty<out Any> + } + + private fun assertKTypeProjections(kType: KType) = assertThat(kType.asTypeName().toString()).isEqualTo(kType.toString()) + + @Test fun kTypeOutProjection() = assertKTypeProjections(Projections::outVariance.returnType) + + @Test fun kTypeInProjection() = assertKTypeProjections(Projections::inVariance.returnType) + + @Test fun kTypeInvariantNullableProjection() = assertKTypeProjections(Projections::invariantNullable.returnType) + + @Test fun kTypeStarProjection() = assertKTypeProjections(Projections::star.returnType) + + @Test fun kTypeMultiVariantProjection() = assertKTypeProjections(Projections::multiVariant.returnType) + + @Test fun kTypeOutAnyOnTypeWithoutBoundsVariance() = assertKTypeProjections(Projections::outAnyOnTypeWithoutBoundsAndVariance.returnType) + + private fun <Param : Closeable> withParam(): Param = throw NotImplementedError("for testing purposes") + + private fun <Param : Closeable> withNullableParam(): Param? = throw NotImplementedError("for testing purposes") + + @Test fun annotatedLambdaTypeParameter() { + val annotation = AnnotationSpec.builder(ClassName("", "Annotation")).build() + val typeName = Map::class.asTypeName() + .plusParameter(String::class.asTypeName()) + .plusParameter(LambdaTypeName.get(returnType = UNIT).copy(annotations = listOf(annotation))) + assertThat(typeName.toString()) + .isEqualTo("kotlin.collections.Map<kotlin.String, @Annotation () -> kotlin.Unit>") + } +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/PropertySpecTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/PropertySpecTest.kt new file mode 100644 index 00000000..5eda0f78 --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/PropertySpecTest.kt @@ -0,0 +1,769 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.truth.Truth.assertThat +import com.squareup.kotlinpoet.FunSpec.Companion.GETTER +import com.squareup.kotlinpoet.FunSpec.Companion.SETTER +import com.squareup.kotlinpoet.KModifier.EXTERNAL +import com.squareup.kotlinpoet.KModifier.PRIVATE +import com.squareup.kotlinpoet.KModifier.PUBLIC +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import java.io.Serializable +import java.util.function.Function +import kotlin.reflect.KClass +import kotlin.test.Test + +@OptIn(ExperimentalKotlinPoetApi::class) +class PropertySpecTest { + annotation class TestAnnotation + + @Test fun nullable() { + val type = String::class.asClassName().copy(nullable = true) + val a = PropertySpec.builder("foo", type).build() + assertThat(a.toString()).isEqualTo("val foo: kotlin.String?\n") + } + + @Test fun delegated() { + val prop = PropertySpec.builder("foo", String::class) + .delegate("Delegates.notNull()") + .build() + assertThat(prop.toString()).isEqualTo("val foo: kotlin.String by Delegates.notNull()\n") + } + + @Test fun emptySetter() { + val prop = PropertySpec.builder("foo", String::class) + .mutable() + .setter( + FunSpec.setterBuilder() + .addModifiers(PRIVATE) + .build(), + ) + .build() + + assertThat(prop.toString()).isEqualTo( + """ + |var foo: kotlin.String + | private set + | + """.trimMargin(), + ) + } + + // https://github.com/square/kotlinpoet/issues/952 + @Test fun emptySetterCannotHaveBody() { + assertThrows<IllegalArgumentException> { + PropertySpec.builder("foo", String::class) + .mutable() + .setter( + FunSpec.setterBuilder() + .addStatement("body()") + .build(), + ) + .build() + }.hasMessageThat().isEqualTo("parameterless setter cannot have code") + } + + @Test fun externalGetterAndSetter() { + val prop = PropertySpec.builder("foo", String::class) + .mutable() + .getter( + FunSpec.getterBuilder() + .addModifiers(EXTERNAL) + .build(), + ) + .setter( + FunSpec.setterBuilder() + .addModifiers(EXTERNAL) + .build(), + ) + .build() + + assertThat(prop.toString()).isEqualTo( + """ + |var foo: kotlin.String + | external get + | external set + | + """.trimMargin(), + ) + } + + @Test fun externalGetterCannotHaveBody() { + assertThrows<IllegalArgumentException> { + PropertySpec.builder("foo", String::class) + .getter( + FunSpec.getterBuilder() + .addModifiers(EXTERNAL) + .addStatement("return %S", "foo") + .build(), + ) + .build() + }.hasMessageThat().isEqualTo("external getter cannot have code") + } + + @Test fun publicGetterAndSetter() { + val prop = PropertySpec.builder("foo", String::class) + .mutable() + .getter( + FunSpec.getterBuilder() + .addModifiers(PUBLIC) + .addStatement("return %S", "_foo") + .build(), + ) + .setter( + FunSpec.setterBuilder() + .addModifiers(PUBLIC) + .addParameter("value", String::class) + .build(), + ) + .build() + + assertThat(prop.toString()).isEqualTo( + """ + |var foo: kotlin.String + | public get() = "_foo" + | public set(`value`) { + | } + | + """.trimMargin(), + ) + } + + @Test fun inlineSingleAccessorVal() { + val prop = PropertySpec.builder("foo", String::class) + .getter( + FunSpec.getterBuilder() + .addModifiers(KModifier.INLINE) + .addStatement("return %S", "foo") + .build(), + ) + .build() + + assertThat(prop.toString()).isEqualTo( + """ + |inline val foo: kotlin.String + | get() = "foo" + | + """.trimMargin(), + ) + } + + @Test fun inlineSingleAccessorVar() { + val prop = PropertySpec.builder("foo", String::class) + .mutable() + .getter( + FunSpec.getterBuilder() + .addModifiers(KModifier.INLINE) + .addStatement("return %S", "foo") + .build(), + ) + .build() + + assertThat(prop.toString()).isEqualTo( + """ + |var foo: kotlin.String + | inline get() = "foo" + | + """.trimMargin(), + ) + } + + @Test fun inlineBothAccessors() { + val prop = PropertySpec.builder("foo", String::class.asTypeName()) + .mutable() + .getter( + FunSpec.getterBuilder() + .addModifiers(KModifier.INLINE) + .addStatement("return %S", "foo") + .build(), + ) + .setter( + FunSpec.setterBuilder() + .addModifiers(KModifier.INLINE) + .addParameter("value", String::class) + .build(), + ) + .build() + + assertThat(prop.toString()).isEqualTo( + """ + |inline var foo: kotlin.String + | get() = "foo" + | set(`value`) { + | } + | + """.trimMargin(), + ) + } + + @Test fun inlineForbiddenOnProperty() { + assertThrows<IllegalArgumentException> { + PropertySpec.builder("foo", String::class) + .addModifiers(KModifier.INLINE) + .build() + }.hasMessageThat().isEqualTo( + "KotlinPoet doesn't allow setting the inline modifier on " + + "properties. You should mark either the getter, the setter, or both inline.", + ) + } + + @Test fun equalsAndHashCode() { + val type = Int::class + var a = PropertySpec.builder("foo", type).build() + var b = PropertySpec.builder("foo", type).build() + assertThat(a == b).isTrue() + assertThat(a.hashCode()).isEqualTo(b.hashCode()) + a = PropertySpec.builder("FOO", type, KModifier.PUBLIC, KModifier.LATEINIT).build() + b = PropertySpec.builder("FOO", type, KModifier.PUBLIC, KModifier.LATEINIT).build() + assertThat(a == b).isTrue() + assertThat(a.hashCode()).isEqualTo(b.hashCode()) + } + + @Test fun escapeKeywordInPropertyName() { + val prop = PropertySpec.builder("object", String::class) + .build() + assertThat(prop.toString()).isEqualTo( + """ + |val `object`: kotlin.String + | + """.trimMargin(), + ) + } + + @Test fun escapeKeywordInVariableName() { + val prop = PropertySpec.builder("object", String::class) + .mutable() + .build() + assertThat(prop.toString()).isEqualTo( + """ + |var `object`: kotlin.String + | + """.trimMargin(), + ) + } + + @Test fun externalTopLevel() { + val prop = PropertySpec.builder("foo", String::class) + .addModifiers(KModifier.EXTERNAL) + .build() + + assertThat(prop.toString()).isEqualTo( + """ + |external val foo: kotlin.String + | + """.trimMargin(), + ) + } + + @Test fun escapePunctuationInPropertyName() { + val prop = PropertySpec.builder("with-hyphen", String::class) + .build() + + assertThat(prop.toString()).isEqualTo( + """ + |val `with-hyphen`: kotlin.String + | + """.trimMargin(), + ) + } + + @Test fun generalBuilderEqualityTest() { + val originatingElement = FakeElement() + val prop = PropertySpec.builder("tacos", Int::class) + .mutable() + .addAnnotation(ClassName("com.squareup.kotlinpoet", "Vegan")) + .addKdoc("Can make it vegan!") + .addModifiers(KModifier.PUBLIC) + .addTypeVariable(TypeVariableName("T")) + .delegate("Delegates.notNull()") + .receiver(Int::class) + .getter( + FunSpec.getterBuilder() + .addModifiers(KModifier.INLINE) + .addStatement("return %S", 42) + .build(), + ) + .setter( + FunSpec.setterBuilder() + .addModifiers(KModifier.INLINE) + .addParameter("value", Int::class) + .build(), + ) + .addOriginatingElement(originatingElement) + .build() + + val newProp = prop.toBuilder().build() + assertThat(newProp).isEqualTo(prop) + assertThat(newProp.originatingElements).containsExactly(originatingElement) + } + + @Test fun modifyModifiers() { + val builder = PropertySpec + .builder("word", String::class) + .addModifiers(PRIVATE) + + builder.modifiers.clear() + builder.modifiers.add(KModifier.INTERNAL) + + assertThat(builder.build().modifiers).containsExactly(KModifier.INTERNAL) + } + + @Test fun modifyAnnotations() { + val builder = PropertySpec + .builder("word", String::class) + .addAnnotation( + AnnotationSpec.builder(JvmName::class.asClassName()) + .addMember("name = %S", "jvmWord") + .build(), + ) + + val javaWord = AnnotationSpec.builder(JvmName::class.asClassName()) + .addMember("name = %S", "javaWord") + .build() + builder.annotations.clear() + builder.annotations.add(javaWord) + + assertThat(builder.build().annotations).containsExactly(javaWord) + } + + // https://github.com/square/kotlinpoet/issues/437 + @Test fun typeVariable() { + val t = TypeVariableName("T", Any::class) + val prop = PropertySpec.builder("someFunction", t, PRIVATE) + .addTypeVariable(t) + .receiver(KClass::class.asClassName().parameterizedBy(t)) + .getter( + FunSpec.getterBuilder() + .addModifiers(KModifier.INLINE) + .addStatement("return stuff as %T", t) + .build(), + ) + .build() + assertThat(prop.toString()).isEqualTo( + """ + |private inline val <T : kotlin.Any> kotlin.reflect.KClass<T>.someFunction: T + | get() = stuff as T + | + """.trimMargin(), + ) + } + + @Test fun typeVariablesWithWhere() { + val t = TypeVariableName("T", Serializable::class, Cloneable::class) + val r = TypeVariableName("R", Any::class) + val function = Function::class.asClassName().parameterizedBy(t, r) + val prop = PropertySpec.builder("property", String::class, PRIVATE) + .receiver(function) + .addTypeVariables(listOf(t, r)) + .getter( + FunSpec.getterBuilder() + .addStatement("return %S", "") + .build(), + ) + .build() + assertThat(prop.toString()).isEqualTo( + """ + |private val <T, R : kotlin.Any> java.util.function.Function<T, R>.`property`: kotlin.String where T : java.io.Serializable, T : kotlin.Cloneable + | get() = "" + | + """.trimMargin(), + ) + } + + @Test fun reifiedTypeVariable() { + val t = TypeVariableName("T").copy(reified = true) + val prop = PropertySpec.builder("someFunction", t, PRIVATE) + .addTypeVariable(t) + .receiver(KClass::class.asClassName().parameterizedBy(t)) + .getter( + FunSpec.getterBuilder() + .addModifiers(KModifier.INLINE) + .addStatement("return stuff as %T", t) + .build(), + ) + .build() + assertThat(prop.toString()).isEqualTo( + """ + |private inline val <reified T> kotlin.reflect.KClass<T>.someFunction: T + | get() = stuff as T + | + """.trimMargin(), + ) + } + + @Test fun reifiedTypeVariableNotAllowedWhenNoAccessors() { + assertThrows<IllegalArgumentException> { + PropertySpec.builder("property", String::class) + .addTypeVariable(TypeVariableName("T").copy(reified = true)) + .build() + }.hasMessageThat().isEqualTo( + "only type parameters of properties with inline getters and/or setters can be reified!", + ) + } + + @Test fun reifiedTypeVariableNotAllowedWhenGetterNotInline() { + assertThrows<IllegalArgumentException> { + PropertySpec.builder("property", String::class) + .addTypeVariable(TypeVariableName("T").copy(reified = true)) + .getter( + FunSpec.getterBuilder() + .addStatement("return %S", "") + .build(), + ) + .build() + }.hasMessageThat().isEqualTo( + "only type parameters of properties with inline getters and/or setters can be reified!", + ) + } + + @Test fun reifiedTypeVariableNotAllowedWhenSetterNotInline() { + assertThrows<IllegalArgumentException> { + PropertySpec.builder("property", String::class.asTypeName()) + .mutable() + .addTypeVariable(TypeVariableName("T").copy(reified = true)) + .setter( + FunSpec.setterBuilder() + .addParameter("value", String::class) + .addStatement("println()") + .build(), + ) + .build() + }.hasMessageThat().isEqualTo( + "only type parameters of properties with inline getters and/or setters can be reified!", + ) + } + + @Test fun reifiedTypeVariableNotAllowedWhenOnlySetterIsInline() { + assertThrows<IllegalArgumentException> { + PropertySpec.builder("property", String::class.asTypeName()) + .mutable() + .addTypeVariable(TypeVariableName("T").copy(reified = true)) + .getter( + FunSpec.getterBuilder() + .addStatement("return %S", "") + .build(), + ) + .setter( + FunSpec.setterBuilder() + .addModifiers(KModifier.INLINE) + .addParameter("value", String::class) + .addStatement("println()") + .build(), + ) + .build() + }.hasMessageThat().isEqualTo( + "only type parameters of properties with inline getters and/or setters can be reified!", + ) + } + + @Test fun setterNotAllowedWhenPropertyIsNotMutable() { + assertThrows<IllegalArgumentException> { + PropertySpec.builder("property", String::class.asTypeName()) + .setter( + FunSpec.setterBuilder() + .addModifiers(KModifier.INLINE) + .addParameter("value", String::class) + .addStatement("println()") + .build(), + ) + .build() + }.hasMessageThat().isEqualTo("only a mutable property can have a setter") + } + + // https://github.com/square/kotlinpoet/issues/462 + @Test fun codeBlockInitializer() { + val param = ParameterSpec.builder("arg", ANY).build() + val initializer = CodeBlock.builder() + .beginControlFlow("{ %L ->", param) + .addStatement("println(\"arg=\$%N\")", param) + .endControlFlow() + .build() + val lambdaTypeName = ClassName.bestGuess("com.example.SomeTypeAlias") + val property = PropertySpec.builder("property", lambdaTypeName) + .initializer(initializer) + .build() + assertThat(property.toString()).isEqualTo( + """ + |val `property`: com.example.SomeTypeAlias = { arg: kotlin.Any -> + | println("arg=${'$'}arg") + |} + | + | + """.trimMargin(), + ) + } + + @Test fun doublePropertyInitialization() { + val codeBlockInitializer = PropertySpec.builder("listA", String::class) + .initializer(CodeBlock.builder().add("foo").build()) + .initializer(CodeBlock.builder().add("bar").build()) + .build() + + assertThat(CodeBlock.of("bar")).isEqualTo(codeBlockInitializer.initializer) + + val formatInitializer = PropertySpec.builder("listA", String::class) + .initializer("foo") + .initializer("bar") + .build() + + assertThat(CodeBlock.of("bar")).isEqualTo(formatInitializer.initializer) + } + + @Test fun propertyKdocWithoutLinebreak() { + val property = PropertySpec.builder("topping", String::class) + .addKdoc("The topping you want on your pizza") + .build() + assertThat(property.toString()).isEqualTo( + """ + |/** + | * The topping you want on your pizza + | */ + |val topping: kotlin.String + | + """.trimMargin(), + ) + } + + @Test fun propertyKdocWithLinebreak() { + val property = PropertySpec.builder("topping", String::class) + .addKdoc("The topping you want on your pizza\n") + .build() + assertThat(property.toString()).isEqualTo( + """ + |/** + | * The topping you want on your pizza + | */ + |val topping: kotlin.String + | + """.trimMargin(), + ) + } + + @Test fun getterKdoc() { + val property = PropertySpec.builder("amount", Int::class) + .initializer("4") + .getter( + FunSpec.getterBuilder() + .addKdoc("Simple multiplier") + .addStatement("return %L * 5", "field") + .build(), + ) + .build() + + assertThat(property.toString()).isEqualTo( + """ + |val amount: kotlin.Int = 4 + | /** + | * Simple multiplier + | */ + | get() = field * 5 + | + """.trimMargin(), + ) + } + + @Test fun constProperty() { + val text = "This is a long string with a newline\nin the middle." + val spec = FileSpec.builder("testsrc", "Test") + .addProperty( + PropertySpec.builder("FOO", String::class, KModifier.CONST) + .initializer("%S", text) + .build(), + ) + .build() + assertThat(spec.toString()).isEqualTo( + """ + |package testsrc + | + |import kotlin.String + | + |public const val FOO: String = "This is a long string with a newline\nin the middle." + | + """.trimMargin(), + ) + } + + @Test fun annotatedLambdaType() { + val annotation = AnnotationSpec.builder(ClassName("com.squareup.tacos", "Annotation")).build() + val type = LambdaTypeName.get(returnType = UNIT).copy(annotations = listOf(annotation)) + val spec = FileSpec.builder("com.squareup.tacos", "Taco") + .addProperty(PropertySpec.builder("foo", type).build()) + .build() + assertThat(spec.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + | + |public val foo: @Annotation () -> Unit + | + """.trimMargin(), + ) + } + + // https://github.com/square/kotlinpoet/issues/1002 + @Test fun visibilityOmittedOnAccessors() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addProperty( + PropertySpec.builder("foo", String::class, PRIVATE) + .mutable() + .getter( + FunSpec.getterBuilder() + .addStatement("return %S", "foo") + .build(), + ) + .setter( + FunSpec.setterBuilder() + .addParameter("foo", String::class) + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + //language=kotlin + """ + package com.squareup.tacos + + import kotlin.String + + private var foo: String + get() = "foo" + set(foo) { + } + + """.trimIndent(), + ) + } + + @Test fun varWithContextReceiverWithoutCustomAccessors() { + val mutablePropertySpecBuilder = { + PropertySpec.builder("foo", STRING) + .mutable() + .contextReceivers(INT) + } + + assertThrows<IllegalArgumentException> { + mutablePropertySpecBuilder() + .getter( + FunSpec.getterBuilder() + .build(), + ) + .build() + }.hasMessageThat() + .isEqualTo("mutable properties with context receivers require a $SETTER") + + assertThrows<IllegalArgumentException> { + mutablePropertySpecBuilder() + .setter( + FunSpec.setterBuilder() + .build(), + ) + .build() + }.hasMessageThat() + .isEqualTo("properties with context receivers require a $GETTER") + } + + @Test fun valWithContextReceiverWithoutGetter() { + assertThrows<IllegalArgumentException> { + PropertySpec.builder("foo", STRING) + .mutable(false) + .contextReceivers(INT) + .build() + }.hasMessageThat() + .isEqualTo("properties with context receivers require a $GETTER") + } + + @Test fun varWithContextReceiver() { + val propertySpec = PropertySpec.builder("foo", INT) + .mutable() + .contextReceivers(STRING) + .getter( + FunSpec.getterBuilder() + .addStatement("return \"\"") + .build(), + ) + .setter( + FunSpec.setterBuilder() + .addParameter( + ParameterSpec.builder("value", STRING) + .build(), + ) + .addStatement("") + .build(), + ) + .build() + + assertThat(propertySpec.toString()).isEqualTo( + """ + |context(kotlin.String) + |var foo: kotlin.Int + | get() = "" + | set(`value`) { + | + | } + | + """.trimMargin(), + ) + } + + @Test fun valWithContextReceiver() { + val propertySpec = PropertySpec.builder("foo", INT) + .mutable(false) + .contextReceivers(STRING) + .getter( + FunSpec.getterBuilder() + .addStatement("return length") + .build(), + ) + .build() + + assertThat(propertySpec.toString()).isEqualTo( + """ + |context(kotlin.String) + |val foo: kotlin.Int + | get() = length + | + """.trimMargin(), + ) + } + + @OptIn(DelicateKotlinPoetApi::class) + @Test + fun annotatedValWithContextReceiver() { + val propertySpec = PropertySpec.builder("foo", INT) + .mutable(false) + .addAnnotation(AnnotationSpec.get(TestAnnotation())) + .contextReceivers(STRING) + .getter( + FunSpec.getterBuilder() + .addStatement("return length") + .build(), + ) + .build() + + assertThat(propertySpec.toString()).isEqualTo( + """ + |context(kotlin.String) + |@com.squareup.kotlinpoet.PropertySpecTest.TestAnnotation + |val foo: kotlin.Int + | get() = length + | + """.trimMargin(), + ) + } +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/StringsTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/StringsTest.kt new file mode 100644 index 00000000..49bec351 --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/StringsTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class StringsTest { + @Test fun singleLineStringWithDollarSymbols() { + val stringWithTemplate = "$" + "annoyingUser" + " is annoying." + val funSpec = FunSpec.builder("getString") + .addStatement("return %S", stringWithTemplate) + .build() + assertThat(funSpec.toString()) + .isEqualTo("public fun getString() = \"\${\'\$\'}annoyingUser is annoying.\"\n") + } + + @Test fun multilineStringWithDollarSymbols() { + val stringWithTemplate = "Some string\n" + "$" + "annoyingUser" + " is annoying." + val funSpec = FunSpec.builder("getString") + .addStatement("return %S", stringWithTemplate) + .build() + assertThat(funSpec.toString()).isEqualTo( + "public fun getString() = \"\"\"\n" + + "|Some string\n" + + "|\${\'\$\'}annoyingUser is annoying.\n" + + "\"\"\".trimMargin()\n", + ) + } + + @Test fun singleLineStringTemplate() { + val stringWithTemplate = "$" + "annoyingUser" + " is annoying." + val funSpec = FunSpec.builder("getString") + .addStatement("return %P", stringWithTemplate) + .build() + assertThat(funSpec.toString()) + .isEqualTo("public fun getString() = \"\"\"\$annoyingUser is annoying.\"\"\"\n") + } + + @Test fun multilineStringTemplate() { + val stringWithTemplate = "Some string\n" + "$" + "annoyingUser" + " is annoying." + val funSpec = FunSpec.builder("getString") + .addStatement("return %P", stringWithTemplate) + .build() + assertThat(funSpec.toString()).isEqualTo( + "public fun getString() = \"\"\"\n" + + "|Some string\n" + + "|\$annoyingUser is annoying.\n" + + "\"\"\".trimMargin()\n", + ) + } + + // https://github.com/square/kotlinpoet/issues/572 + @Test fun templateStringWithStringLiteralReference() { + val string = "SELECT * FROM socialFeedItem WHERE message IS NOT NULL AND userId \${ if (userId == null) \"IS\" else \"=\" } ?1 ORDER BY datetime(creation_time) DESC" + val funSpec = FunSpec.builder("getString") + .addStatement("return %P", string) + .build() + assertThat(funSpec.toString()) + .isEqualTo("public fun getString() = \"\"\"SELECT * FROM socialFeedItem WHERE message IS NOT NULL AND userId \${ if (userId == null) \"IS\" else \"=\" } ?1 ORDER BY datetime(creation_time) DESC\"\"\"\n") + } +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/TaggableTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/TaggableTest.kt new file mode 100644 index 00000000..504798b6 --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/TaggableTest.kt @@ -0,0 +1,130 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.truth.Truth.assertThat +import com.squareup.kotlinpoet.KModifier.CROSSINLINE +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +class TaggableTest(val builder: Taggable.Builder<*>) { + + companion object { + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun data() = arrayOf( + AnnotationSpec.builder(JvmStatic::class), + FileSpec.builder("test", "Test"), + FunSpec.builder("test"), + ParameterSpec.builder("test", String::class.asClassName()), + PropertySpec.builder("test", String::class.asClassName()), + TypeAliasSpec.builder("Test", String::class.asClassName()), + TypeSpec.classBuilder("Test"), + ) + } + + @Before fun setUp() { + builder.tags.clear() + } + + @Test fun builderShouldMakeDefensiveCopy() { + builder.tag(String::class, "test") + val taggable = builder.buildTaggable() + builder.tags.remove(String::class) + assertThat(taggable.tag<String>()).isEqualTo("test") + } + + @Test fun missingShouldBeNull() { + val taggable = builder.buildTaggable() + assertThat(taggable.tag<Int>()).isNull() + } + + @Test fun kclassParamFlow() { + builder.tag(String::class, "test") + val taggable = builder.buildTaggable() + assertThat(taggable.tag(String::class)).isEqualTo("test") + } + + @Test fun javaClassParamFlow() { + builder.tag(String::class.java, "test") + val taggable = builder.buildTaggable() + assertThat(taggable.tag(String::class.java)).isEqualTo("test") + } + + @Test fun kclassInJavaClassOut() { + builder.tag(String::class, "test") + val taggable = builder.buildTaggable() + assertThat(taggable.tag(String::class.java)).isEqualTo("test") + } + + @Test fun javaClassInkClassOut() { + builder.tag(String::class.java, "test") + val taggable = builder.buildTaggable() + assertThat(taggable.tag(String::class)).isEqualTo("test") + } + + private fun Taggable.Builder<*>.buildTaggable(): Taggable { + // Apply blocks test inline builder tag functions don't break the chain. Result is discarded + return when (this) { + is AnnotationSpec.Builder -> build().apply { + toBuilder() + .tag(1) + .addMember(CodeBlock.of("")) + .build() + } + is FileSpec.Builder -> build().apply { + toBuilder() + .tag(1) + .addFileComment("Test") + .build() + } + is FunSpec.Builder -> build().apply { + toBuilder() + .tag(1) + .returns(String::class) + .build() + } + is ParameterSpec.Builder -> build().apply { + toBuilder() + .tag(1) + .addModifiers(CROSSINLINE) + .build() + } + is PropertySpec.Builder -> build().apply { + toBuilder() + .tag(1) + .initializer(CodeBlock.of("")) + .build() + } + is TypeAliasSpec.Builder -> build().apply { + toBuilder() + .tag(1) + .addKdoc(CodeBlock.of("")) + .build() + } + is TypeSpec.Builder -> build().apply { + toBuilder() + .tag(1) + .addKdoc(CodeBlock.of("")) + .build() + } + else -> TODO("Unsupported type ${this::class.simpleName}") + } + } +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/TestFiler.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/TestFiler.kt new file mode 100644 index 00000000..bb24d564 --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/TestFiler.kt @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2014 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import java.io.OutputStream +import java.nio.file.FileSystem +import java.nio.file.Files +import java.nio.file.Path +import javax.annotation.processing.Filer +import javax.lang.model.element.AnnotationMirror +import javax.lang.model.element.Element +import javax.lang.model.element.ElementKind +import javax.lang.model.element.ElementVisitor +import javax.lang.model.element.Modifier +import javax.lang.model.element.Name +import javax.lang.model.type.TypeMirror +import javax.tools.FileObject +import javax.tools.JavaFileManager +import javax.tools.JavaFileObject +import javax.tools.SimpleJavaFileObject + +internal class TestFiler( + fileSystem: FileSystem, + private val fileSystemRoot: Path, +) : Filer { + + internal inner class Source(private val path: Path) : + SimpleJavaFileObject(path.toUri(), JavaFileObject.Kind.SOURCE) { + override fun openOutputStream(): OutputStream { + val parent = path.parent + if (!Files.exists(parent)) fileSystemProvider.createDirectory(parent) + return fileSystemProvider.newOutputStream(path) + } + } + + private val separator = fileSystem.separator + private val fileSystemProvider = fileSystem.provider() + private val originatingElementsMap = mutableMapOf<Path, List<Element>>() + + fun getOriginatingElements(path: Path) = originatingElementsMap[path] ?: throw NullPointerException("Could not find $path") + + override fun createSourceFile( + name: CharSequence, + vararg originatingElements: Element, + ): JavaFileObject { + val relative = name.toString().replace(".", separator) + ".kt" // Assumes well-formed. + val path = fileSystemRoot.resolve(relative) + originatingElementsMap[path] = originatingElements.toList() + return Source(path) + } + + override fun createClassFile(name: CharSequence, vararg originatingElements: Element) = + throw UnsupportedOperationException("Not implemented.") + + override fun createResource( + location: JavaFileManager.Location, + pkg: CharSequence, + relativeName: CharSequence, + vararg originatingElements: Element, + ): FileObject { + val relative = pkg.toString().replace(".", separator) + separator + relativeName + val path = fileSystemRoot.resolve(relative) + originatingElementsMap[path] = originatingElements.toList() + return Source(path) + } + + override fun getResource( + location: JavaFileManager.Location, + pkg: CharSequence, + relativeName: CharSequence, + ) = throw UnsupportedOperationException("Not implemented.") +} + +internal class FakeElement : Element { + + override fun getModifiers(): MutableSet<Modifier> { + TODO() + } + + override fun getSimpleName(): Name { + TODO() + } + + override fun getKind(): ElementKind { + TODO() + } + + override fun asType(): TypeMirror { + TODO() + } + + override fun getEnclosingElement(): Element { + TODO() + } + + override fun <R : Any?, P : Any?> accept(v: ElementVisitor<R, P>?, p: P): R { + TODO() + } + + override fun <A : Annotation?> getAnnotationsByType(annotationType: Class<A>?): Array<A> { + TODO() + } + + override fun <A : Annotation?> getAnnotation(annotationType: Class<A>?): A { + TODO() + } + + override fun getAnnotationMirrors(): MutableList<out AnnotationMirror> { + TODO() + } + + override fun getEnclosedElements(): MutableList<out Element> { + TODO() + } +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/TypeAliasSpecTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/TypeAliasSpecTest.kt new file mode 100644 index 00000000..f5c7eb22 --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/TypeAliasSpecTest.kt @@ -0,0 +1,251 @@ +/* + * Copyright (C) 2017 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.truth.Truth.assertThat +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import java.util.concurrent.atomic.AtomicReference +import kotlin.annotation.AnnotationRetention.RUNTIME +import kotlin.annotation.AnnotationTarget.TYPEALIAS +import kotlin.test.Test + +class TypeAliasSpecTest { + + @Test fun simpleTypeAlias() { + val typeAliasSpec = TypeAliasSpec + .builder("Word", String::class) + .build() + + assertThat(typeAliasSpec.toString()).isEqualTo( + """ + |public typealias Word = kotlin.String + | + """.trimMargin(), + ) + } + + @Test fun typeVariable() { + val v = TypeVariableName("V") + val typeAliasSpec = TypeAliasSpec + .builder("Word", List::class.asClassName().parameterizedBy(v)) + .addTypeVariable(v) + .build() + + assertThat(typeAliasSpec.toString()).isEqualTo( + """ + |public typealias Word<V> = kotlin.collections.List<V> + | + """.trimMargin(), + ) + } + + @Test fun publicVisibility() { + val typeAliasSpec = TypeAliasSpec + .builder("Word", String::class) + .addModifiers(KModifier.PUBLIC) + .build() + + assertThat(typeAliasSpec.toString()).isEqualTo( + """ + |public typealias Word = kotlin.String + | + """.trimMargin(), + ) + } + + @Test fun internalVisibility() { + val typeAliasSpec = TypeAliasSpec + .builder("Word", String::class) + .addModifiers(KModifier.INTERNAL) + .build() + + assertThat(typeAliasSpec.toString()).isEqualTo( + """ + |internal typealias Word = kotlin.String + | + """.trimMargin(), + ) + } + + @Test fun privateVisibility() { + val typeAliasSpec = TypeAliasSpec + .builder("Word", String::class) + .addModifiers(KModifier.PRIVATE) + .build() + + assertThat(typeAliasSpec.toString()).isEqualTo( + """ + |private typealias Word = kotlin.String + | + """.trimMargin(), + ) + } + + @Test fun implTypeAlias() { + val typeName = AtomicReference::class.asClassName().parameterizedBy(TypeVariableName("V")) + val typeAliasSpec = TypeAliasSpec + .builder("AtomicRef", typeName) + .addModifiers(KModifier.ACTUAL) + .addTypeVariable(TypeVariableName("V")) + .build() + + assertThat(typeAliasSpec.toString()).isEqualTo( + """ + |public actual typealias AtomicRef<V> = java.util.concurrent.atomic.AtomicReference<V> + | + """.trimMargin(), + ) + } + + @Test fun kdoc() { + val typeAliasSpec = TypeAliasSpec + .builder("Word", String::class) + .addKdoc("Word is just a type alias for [String](%T).\n", String::class) + .build() + + assertThat(typeAliasSpec.toString()).isEqualTo( + """ + |/** + | * Word is just a type alias for [String](kotlin.String). + | */ + |public typealias Word = kotlin.String + | + """.trimMargin(), + ) + } + + @Test fun annotations() { + val typeAliasSpec = TypeAliasSpec + .builder("Word", String::class) + .addAnnotation( + AnnotationSpec.builder(TypeAliasAnnotation::class.asClassName()) + .addMember("value = %S", "words!") + .build(), + ) + .build() + + assertThat(typeAliasSpec.toString()).isEqualTo( + """ + |@com.squareup.kotlinpoet.TypeAliasAnnotation(value = "words!") + |public typealias Word = kotlin.String + | + """.trimMargin(), + ) + } + + @Test fun kdocWithoutNewLine() { + val typeAliasSpec = TypeAliasSpec + .builder("Word", String::class) + .addKdoc("Word is just a type alias for [String](%T).", String::class) + .build() + + assertThat(typeAliasSpec.toString()).isEqualTo( + """ + |/** + | * Word is just a type alias for [String](kotlin.String). + | */ + |public typealias Word = kotlin.String + | + """.trimMargin(), + ) + } + + @Test fun equalsAndHashCode() { + val a = TypeAliasSpec.builder("Word", String::class).addModifiers(KModifier.PUBLIC).build() + val b = TypeAliasSpec.builder("Word", String::class).addModifiers(KModifier.PUBLIC).build() + + assertThat(a == b).isTrue() + assertThat(a.hashCode()).isEqualTo(b.hashCode()) + } + + @Test fun generalBuilderEqualityTest() { + val typeParam = TypeVariableName("V") + val typeAliasSpec = TypeAliasSpec + .builder("Bio", Pair::class.parameterizedBy(String::class, String::class)) + .addKdoc("First nand Last Name.\n") + .addModifiers(KModifier.PUBLIC) + .addTypeVariable(typeParam) + .build() + assertThat(typeAliasSpec.toBuilder().build()).isEqualTo(typeAliasSpec) + } + + @Test fun modifyModifiers() { + val builder = TypeAliasSpec + .builder("Word", String::class) + .addModifiers(KModifier.PRIVATE) + + builder.modifiers.clear() + builder.modifiers.add(KModifier.INTERNAL) + + assertThat(builder.build().modifiers).containsExactly(KModifier.INTERNAL) + } + + @Test fun modifyTypeVariableNames() { + val builder = TypeAliasSpec + .builder("Word", String::class) + .addTypeVariable(TypeVariableName("V")) + + val tVar = TypeVariableName("T") + builder.typeVariables.clear() + builder.typeVariables.add(tVar) + + assertThat(builder.build().typeVariables).containsExactly(tVar) + } + + @Test fun modifyAnnotations() { + val builder = TypeAliasSpec + .builder("Word", String::class) + .addAnnotation( + AnnotationSpec.builder(TypeAliasAnnotation::class.asClassName()) + .addMember("value = %S", "value1") + .build(), + ) + + val javaWord = AnnotationSpec.builder(TypeAliasAnnotation::class.asClassName()) + .addMember("value = %S", "value2") + .build() + builder.annotations.clear() + builder.annotations.add(javaWord) + + assertThat(builder.build().annotations).containsExactly(javaWord) + } + + @Test fun nameEscaping() { + val typeAlias = TypeAliasSpec.builder("fun", String::class).build() + assertThat(typeAlias.toString()).isEqualTo( + """ + |public typealias `fun` = kotlin.String + | + """.trimMargin(), + ) + } + + @Test fun annotatedLambdaType() { + val annotation = AnnotationSpec.builder(ClassName("", "Annotation")).build() + val type = LambdaTypeName.get(returnType = UNIT).copy(annotations = listOf(annotation)) + val spec = TypeAliasSpec.builder("lambda", type).build() + assertThat(spec.toString()).isEqualTo( + """ + |public typealias lambda = @Annotation () -> kotlin.Unit + | + """.trimMargin(), + ) + } +} + +@Retention(RUNTIME) +@Target(TYPEALIAS) +annotation class TypeAliasAnnotation(val value: String) diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/TypeNameKotlinTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/TypeNameKotlinTest.kt new file mode 100644 index 00000000..3c16242a --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/TypeNameKotlinTest.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.truth.Truth.assertThat +import org.junit.Test + +class TypeNameKotlinTest { + + @Test + fun typeNameOf_simple() { + val type = typeNameOf<TypeNameKotlinTest>() + assertThat(type.toString()).isEqualTo("com.squareup.kotlinpoet.TypeNameKotlinTest") + } + + @Test + fun typeNameOf_simple_intrinsic() { + val type = typeNameOf<String>() + assertThat(type.toString()).isEqualTo("kotlin.String") + } + + @Test + fun typeNameOf_array_primitive() { + val type = typeNameOf<IntArray>() + assertThat(type.toString()).isEqualTo("kotlin.IntArray") + } + + @Test + fun typeNameOf_array_parameterized() { + val type = typeNameOf<Array<String>>() + assertThat(type.toString()).isEqualTo("kotlin.Array<kotlin.String>") + } + + @Test + fun typeNameOf_nullable() { + val type = typeNameOf<String?>() + assertThat(type.toString()).isEqualTo("kotlin.String?") + } + + @Test + fun typeNameOf_generic() { + val type = typeNameOf<List<String>>() + assertThat(type.toString()).isEqualTo("kotlin.collections.List<kotlin.String>") + } + + @Test + fun typeNameOf_generic_wildcard_out() { + val type = typeNameOf<GenericType<out String>>() + assertThat(type.toString()).isEqualTo("com.squareup.kotlinpoet.TypeNameKotlinTest.GenericType<out kotlin.String>") + } + + @Test + fun typeNameOf_generic_wildcard_in() { + val type = typeNameOf<GenericType<in String>>() + assertThat(type.toString()).isEqualTo("com.squareup.kotlinpoet.TypeNameKotlinTest.GenericType<in kotlin.String>") + } + + @Test + fun typeNameOf_complex() { + val type = typeNameOf<Map<String, List<Map<*, GenericType<in Set<Array<GenericType<out String>?>>>>>>>() + assertThat(type.toString()).isEqualTo("kotlin.collections.Map<kotlin.String, kotlin.collections.List<kotlin.collections.Map<*, com.squareup.kotlinpoet.TypeNameKotlinTest.GenericType<in kotlin.collections.Set<kotlin.Array<com.squareup.kotlinpoet.TypeNameKotlinTest.GenericType<out kotlin.String>?>>>>>>") + } + + @Suppress("unused") + class GenericType<T> + + @Test + fun tag() { + val type = typeNameOf<String>().copy(tags = mapOf(String::class to "Test")) + assertThat(type.tag<String>()).isEqualTo("Test") + } + + @Test + fun existingTagsShouldBePreserved() { + val type = typeNameOf<String>().copy(tags = mapOf(String::class to "Test")) + val copied = type.copy(nullable = true) + assertThat(copied.tag<String>()).isEqualTo("Test") + } +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/TypeNameTest.java b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/TypeNameTest.java new file mode 100644 index 00000000..70019f9d --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/TypeNameTest.java @@ -0,0 +1,161 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet; + +import java.io.Serializable; +import java.lang.reflect.Method; +import java.util.Comparator; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import org.junit.Test; + +import static com.google.common.truth.Truth.assertThat; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertNotEquals; + +public class TypeNameTest { + + protected <E extends Enum<E>> E generic(E[] values) { + return values[0]; + } + + protected static class TestGeneric<T> { + class Inner {} + + class InnerGeneric<T2> {} + + static class NestedNonGeneric {} + } + + protected static TestGeneric<String>.Inner testGenericStringInner() { + return null; + } + + protected static TestGeneric<Integer>.Inner testGenericIntInner() { + return null; + } + + protected static TestGeneric<Short>.InnerGeneric<Long> testGenericInnerLong() { + return null; + } + + protected static TestGeneric<Short>.InnerGeneric<Integer> testGenericInnerInt() { + return null; + } + + protected static TestGeneric.NestedNonGeneric testNestedNonGeneric() { + return null; + } + + @Test public void genericType() throws Exception { + Method recursiveEnum = getClass().getDeclaredMethod("generic", Enum[].class); + TypeNames.get(recursiveEnum.getReturnType()); + TypeNames.get(recursiveEnum.getGenericReturnType()); + TypeName genericTypeName = TypeNames.get(recursiveEnum.getParameterTypes()[0]); + TypeNames.get(recursiveEnum.getGenericParameterTypes()[0]); + + // Make sure the generic argument is present + assertThat(genericTypeName.toString()).contains("Enum"); + } + + @Test public void innerClassInGenericType() throws Exception { + Method genericStringInner = getClass().getDeclaredMethod("testGenericStringInner"); + TypeNames.get(genericStringInner.getReturnType()); + TypeName genericTypeName = TypeNames.get(genericStringInner.getGenericReturnType()); + assertNotEquals(TypeNames.get(genericStringInner.getGenericReturnType()), + TypeNames.get(getClass().getDeclaredMethod("testGenericIntInner").getGenericReturnType())); + + // Make sure the generic argument is present + assertThat(genericTypeName.toString()).isEqualTo( + TestGeneric.class.getCanonicalName() + "<java.lang.String>.Inner"); + } + + @Test public void innerGenericInGenericType() throws Exception { + Method genericStringInner = getClass().getDeclaredMethod("testGenericInnerLong"); + TypeNames.get(genericStringInner.getReturnType()); + TypeName genericTypeName = TypeNames.get(genericStringInner.getGenericReturnType()); + assertNotEquals(TypeNames.get(genericStringInner.getGenericReturnType()), + TypeNames.get(getClass().getDeclaredMethod("testGenericInnerInt").getGenericReturnType())); + + // Make sure the generic argument is present + assertThat(genericTypeName.toString()).isEqualTo( + TestGeneric.class.getCanonicalName() + "<java.lang.Short>.InnerGeneric<java.lang.Long>"); + } + + @Test public void innerStaticInGenericType() throws Exception { + Method staticInGeneric = getClass().getDeclaredMethod("testNestedNonGeneric"); + TypeNames.get(staticInGeneric.getReturnType()); + TypeName typeName = TypeNames.get(staticInGeneric.getGenericReturnType()); + + // Make sure there are no generic arguments + assertThat(typeName.toString()).isEqualTo( + TestGeneric.class.getCanonicalName() + ".NestedNonGeneric"); + } + + @Test public void equalsAndHashCodePrimitive() { + assertEqualsHashCodeAndToString(TypeNames.BOOLEAN, TypeNames.BOOLEAN); + assertEqualsHashCodeAndToString(TypeNames.BYTE, TypeNames.BYTE); + assertEqualsHashCodeAndToString(TypeNames.CHAR, TypeNames.CHAR); + assertEqualsHashCodeAndToString(TypeNames.DOUBLE, TypeNames.DOUBLE); + assertEqualsHashCodeAndToString(TypeNames.FLOAT, TypeNames.FLOAT); + assertEqualsHashCodeAndToString(TypeNames.INT, TypeNames.INT); + assertEqualsHashCodeAndToString(TypeNames.LONG, TypeNames.LONG); + assertEqualsHashCodeAndToString(TypeNames.SHORT, TypeNames.SHORT); + assertEqualsHashCodeAndToString(TypeNames.UNIT, TypeNames.UNIT); + } + + @Test public void equalsAndHashCodeClassName() { + assertEqualsHashCodeAndToString(ClassNames.get(Object.class), ClassNames.get(Object.class)); + assertEqualsHashCodeAndToString(TypeNames.get(Object.class), ClassNames.get(Object.class)); + assertEqualsHashCodeAndToString(ClassName.bestGuess("java.lang.Object"), + ClassNames.get(Object.class)); + } + + @Test public void equalsAndHashCodeParameterizedTypeName() { + assertEqualsHashCodeAndToString(ParameterizedTypeName.get(List.class, Object.class), + ParameterizedTypeName.get(List.class, Object.class)); + assertEqualsHashCodeAndToString(ParameterizedTypeName.get(Set.class, UUID.class), + ParameterizedTypeName.get(Set.class, UUID.class)); + assertNotEquals(ClassNames.get(List.class), ParameterizedTypeName.get(List.class, + String.class)); + } + + @Test public void equalsAndHashCodeTypeVariableName() { + assertEqualsHashCodeAndToString(TypeVariableName.get("A"), + TypeVariableName.get("A")); + TypeVariableName typeVar1 = TypeVariableName.get("T", Comparator.class, Serializable.class); + TypeVariableName typeVar2 = TypeVariableName.get("T", Comparator.class, Serializable.class); + assertEqualsHashCodeAndToString(typeVar1, typeVar2); + } + + @Test public void equalsAndHashCodeWildcardTypeName() { + assertEqualsHashCodeAndToString(WildcardTypeName.producerOf(Object.class), + WildcardTypeName.producerOf(Object.class)); + assertEqualsHashCodeAndToString(WildcardTypeName.producerOf(Serializable.class), + WildcardTypeName.producerOf(Serializable.class)); + assertEqualsHashCodeAndToString(WildcardTypeName.consumerOf(String.class), + WildcardTypeName.consumerOf(String.class)); + } + + private void assertEqualsHashCodeAndToString(TypeName a, TypeName b) { + assertEquals(a.toString(), b.toString()); + assertThat(a.equals(b)).isTrue(); + assertThat(a.hashCode()).isEqualTo(b.hashCode()); + assertFalse(a.equals(null)); + } +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/TypeSpecTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/TypeSpecTest.kt new file mode 100644 index 00000000..cf6c3e29 --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/TypeSpecTest.kt @@ -0,0 +1,5364 @@ +/* + * Copyright (C) 2015 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.collect.ImmutableMap +import com.google.common.truth.Truth.assertThat +import com.google.testing.compile.CompilationRule +import com.squareup.kotlinpoet.KModifier.ABSTRACT +import com.squareup.kotlinpoet.KModifier.DATA +import com.squareup.kotlinpoet.KModifier.IN +import com.squareup.kotlinpoet.KModifier.INNER +import com.squareup.kotlinpoet.KModifier.INTERNAL +import com.squareup.kotlinpoet.KModifier.PRIVATE +import com.squareup.kotlinpoet.KModifier.PUBLIC +import com.squareup.kotlinpoet.KModifier.VARARG +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.jvm.throws +import java.io.IOException +import java.io.Serializable +import java.lang.Deprecated +import java.math.BigDecimal +import java.util.AbstractSet +import java.util.Collections +import java.util.Comparator +import java.util.EventListener +import java.util.Locale +import java.util.Random +import java.util.concurrent.Callable +import java.util.function.Consumer +import java.util.logging.Logger +import javax.lang.model.element.TypeElement +import kotlin.reflect.KClass +import kotlin.reflect.KFunction +import kotlin.test.Ignore +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.assertFailsWith +import kotlin.test.fail +import org.junit.Rule + +@OptIn(ExperimentalKotlinPoetApi::class) +class TypeSpecTest { + private val tacosPackage = "com.squareup.tacos" + + @Rule @JvmField + val compilation = CompilationRule() + + private fun getElement(`class`: Class<*>): TypeElement { + return compilation.elements.getTypeElement(`class`.canonicalName) + } + + private fun getElement(`class`: KClass<*>): TypeElement { + return getElement(`class`.java) + } + + @Test fun basic() { + val taco = TypeSpec.classBuilder("Taco") + .addFunction( + FunSpec.builder("toString") + .addModifiers(KModifier.PUBLIC, KModifier.FINAL, KModifier.OVERRIDE) + .returns(String::class) + .addStatement("return %S", "taco") + .build(), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + | + |public class Taco { + | public final override fun toString(): String = "taco" + |} + | + """.trimMargin(), + ) + assertEquals(1906837485, taco.hashCode().toLong()) // Update expected number if source changes. + } + + @Test fun interestingTypes() { + val listOfAny = List::class.asClassName().parameterizedBy(STAR) + val listOfExtends = List::class.asClassName() + .parameterizedBy(WildcardTypeName.producerOf(Serializable::class)) + val listOfSuper = List::class.asClassName() + .parameterizedBy(WildcardTypeName.consumerOf(String::class)) + val taco = TypeSpec.classBuilder("Taco") + .addProperty("star", listOfAny) + .addProperty("outSerializable", listOfExtends) + .addProperty("inString", listOfSuper) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.io.Serializable + |import kotlin.String + |import kotlin.collections.List + | + |public class Taco { + | public val star: List<*> + | + | public val outSerializable: List<out Serializable> + | + | public val inString: List<in String> + |} + | + """.trimMargin(), + ) + } + + @Test fun anonymousInnerClass() { + val foo = ClassName(tacosPackage, "Foo") + val bar = ClassName(tacosPackage, "Bar") + val thingThang = ClassName(tacosPackage, "Thing", "Thang") + val thingThangOfFooBar = thingThang.parameterizedBy(foo, bar) + val thung = ClassName(tacosPackage, "Thung") + val simpleThung = ClassName(tacosPackage, "SimpleThung") + val thungOfSuperBar = thung.parameterizedBy(WildcardTypeName.consumerOf(bar)) + val thungOfSuperFoo = thung.parameterizedBy(WildcardTypeName.consumerOf(foo)) + val simpleThungOfBar = simpleThung.parameterizedBy(bar) + + val thungParameter = ParameterSpec.builder("thung", thungOfSuperFoo) + .build() + val aSimpleThung = TypeSpec.anonymousClassBuilder() + .superclass(simpleThungOfBar) + .addSuperclassConstructorParameter("%N", thungParameter) + .addFunction( + FunSpec.builder("doSomething") + .addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE) + .addParameter("bar", bar) + .addCode("/* code snippets */\n") + .build(), + ) + .build() + val aThingThang = TypeSpec.anonymousClassBuilder() + .superclass(thingThangOfFooBar) + .addFunction( + FunSpec.builder("call") + .addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE) + .returns(thungOfSuperBar) + .addParameter(thungParameter) + .addStatement("return %L", aSimpleThung) + .build(), + ) + .build() + val taco = TypeSpec.classBuilder("Taco") + .addProperty( + PropertySpec.builder("NAME", thingThangOfFooBar) + .initializer("%L", aThingThang) + .build(), + ) + .build() + + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + | + |public class Taco { + | public val NAME: Thing.Thang<Foo, Bar> = object : Thing.Thang<Foo, Bar>() { + | public override fun call(thung: Thung<in Foo>): Thung<in Bar> = object : SimpleThung<Bar>(thung) + | { + | public override fun doSomething(bar: Bar): Unit { + | /* code snippets */ + | } + | } + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun anonymousClassWithSuperClassConstructorCall() { + val superclass = ArrayList::class.parameterizedBy(String::class) + val anonymousClass = TypeSpec.anonymousClassBuilder() + .addSuperclassConstructorParameter("%L", "4") + .superclass(superclass) + .build() + val taco = TypeSpec.classBuilder("Taco") + .addProperty( + PropertySpec.builder("names", superclass) + .initializer("%L", anonymousClass) + .build(), + ).build() + + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.util.ArrayList + |import kotlin.String + | + |public class Taco { + | public val names: ArrayList<String> = object : ArrayList<String>(4) { + | } + |} + | + """.trimMargin(), + ) + } + + // https://github.com/square/kotlinpoet/issues/315 + @Test fun anonymousClassWithMultipleSuperTypes() { + val superclass = ClassName("com.squareup.wire", "Message") + val anonymousClass = TypeSpec.anonymousClassBuilder() + .superclass(superclass) + .addSuperinterface(Runnable::class) + .addFunction( + FunSpec.builder("run") + .addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE) + .addCode("/* code snippets */\n") + .build(), + ).build() + val taco = TypeSpec.classBuilder("Taco") + .addProperty( + PropertySpec.builder("NAME", Runnable::class) + .initializer("%L", anonymousClass) + .build(), + ).build() + + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import com.squareup.wire.Message + |import java.lang.Runnable + |import kotlin.Unit + | + |public class Taco { + | public val NAME: Runnable = object : Message(), Runnable { + | public override fun run(): Unit { + | /* code snippets */ + | } + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun anonymousClassWithoutSuperType() { + val anonymousClass = TypeSpec.anonymousClassBuilder().build() + val taco = TypeSpec.classBuilder("Taco") + .addProperty( + PropertySpec.builder("NAME", Any::class) + .initializer("%L", anonymousClass) + .build(), + ).build() + + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Any + | + |public class Taco { + | public val NAME: Any = object { + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun annotatedParameters() { + val service = TypeSpec.classBuilder("Foo") + .addFunction( + FunSpec.constructorBuilder() + .addModifiers(KModifier.PUBLIC) + .addParameter("id", Long::class) + .addParameter( + ParameterSpec.builder("one", String::class) + .addAnnotation(ClassName(tacosPackage, "Ping")) + .build(), + ) + .addParameter( + ParameterSpec.builder("two", String::class) + .addAnnotation(ClassName(tacosPackage, "Ping")) + .build(), + ) + .addParameter( + ParameterSpec.builder("three", String::class) + .addAnnotation( + AnnotationSpec.builder(ClassName(tacosPackage, "Pong")) + .addMember("%S", "pong") + .build(), + ) + .build(), + ) + .addParameter( + ParameterSpec.builder("four", String::class) + .addAnnotation(ClassName(tacosPackage, "Ping")) + .build(), + ) + .addCode("/* code snippets */\n") + .build(), + ) + .build() + + assertThat(toString(service)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Long + |import kotlin.String + | + |public class Foo { + | public constructor( + | id: Long, + | @Ping one: String, + | @Ping two: String, + | @Pong("pong") three: String, + | @Ping four: String, + | ) { + | /* code snippets */ + | } + |} + | + """.trimMargin(), + ) + } + + /** + * We had a bug where annotations were preventing us from doing the right thing when resolving + * imports. https://github.com/square/javapoet/issues/422 + */ + @Test fun annotationsAndJavaLangTypes() { + val freeRange = ClassName("javax.annotation", "FreeRange") + val taco = TypeSpec.classBuilder("EthicalTaco") + .addProperty( + "meat", + String::class.asClassName() + .copy(annotations = listOf(AnnotationSpec.builder(freeRange).build())), + ) + .build() + + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import javax.`annotation`.FreeRange + |import kotlin.String + | + |public class EthicalTaco { + | public val meat: @FreeRange String + |} + | + """.trimMargin(), + ) + } + + @Test fun retrofitStyleInterface() { + val observable = ClassName(tacosPackage, "Observable") + val fooBar = ClassName(tacosPackage, "FooBar") + val thing = ClassName(tacosPackage, "Thing") + val things = ClassName(tacosPackage, "Things") + val map = Map::class.asClassName() + val string = String::class.asClassName() + val headers = ClassName(tacosPackage, "Headers") + val post = ClassName(tacosPackage, "POST") + val body = ClassName(tacosPackage, "Body") + val queryMap = ClassName(tacosPackage, "QueryMap") + val header = ClassName(tacosPackage, "Header") + val service = TypeSpec.interfaceBuilder("Service") + .addFunction( + FunSpec.builder("fooBar") + .addModifiers(KModifier.PUBLIC, KModifier.ABSTRACT) + .addAnnotation( + AnnotationSpec.builder(headers) + .addMember("%S", "Accept: application/json") + .addMember("%S", "User-Agent: foobar") + .build(), + ) + .addAnnotation( + AnnotationSpec.builder(post) + .addMember("%S", "/foo/bar") + .build(), + ) + .returns(observable.parameterizedBy(fooBar)) + .addParameter( + ParameterSpec.builder("things", things.parameterizedBy(thing)) + .addAnnotation(body) + .build(), + ) + .addParameter( + ParameterSpec.builder("query", map.parameterizedBy(string, string)) + .addAnnotation( + AnnotationSpec.builder(queryMap) + .addMember("encodeValues = %L", "false") + .build(), + ) + .build(), + ) + .addParameter( + ParameterSpec.builder("authorization", string) + .addAnnotation( + AnnotationSpec.builder(header) + .addMember("%S", "Authorization") + .build(), + ) + .build(), + ) + .build(), + ) + .build() + + assertThat(toString(service)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.collections.Map + | + |public interface Service { + | @Headers( + | "Accept: application/json", + | "User-Agent: foobar", + | ) + | @POST("/foo/bar") + | public fun fooBar( + | @Body things: Things<Thing>, + | @QueryMap(encodeValues = false) query: Map<String, String>, + | @Header("Authorization") authorization: String, + | ): Observable<FooBar> + |} + | + """.trimMargin(), + ) + } + + @Test fun annotatedProperty() { + val taco = TypeSpec.classBuilder("Taco") + .addProperty( + PropertySpec.builder("thing", String::class, KModifier.PRIVATE) + .addAnnotation( + AnnotationSpec.builder(ClassName(tacosPackage, "JsonAdapter")) + .addMember("%T::class", ClassName(tacosPackage, "Foo")) + .build(), + ) + .build(), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + | + |public class Taco { + | @JsonAdapter(Foo::class) + | private val thing: String + |} + | + """.trimMargin(), + ) + } + + @Test fun annotatedPropertyUseSiteTarget() { + val taco = TypeSpec.classBuilder("Taco") + .addProperty( + PropertySpec.builder("thing", String::class, KModifier.PRIVATE) + .addAnnotation( + AnnotationSpec.builder(ClassName(tacosPackage, "JsonAdapter")) + .addMember("%T::class", ClassName(tacosPackage, "Foo")) + .useSiteTarget(AnnotationSpec.UseSiteTarget.FIELD) + .build(), + ) + .build(), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + | + |public class Taco { + | @field:JsonAdapter(Foo::class) + | private val thing: String + |} + | + """.trimMargin(), + ) + } + + @Test fun annotatedClass() { + val someType = ClassName(tacosPackage, "SomeType") + val taco = TypeSpec.classBuilder("Foo") + .addAnnotation( + AnnotationSpec.builder(ClassName(tacosPackage, "Something")) + .addMember("%T.%N", someType, "PROPERTY") + .addMember("%L", 12) + .addMember("%S", "goodbye") + .build(), + ) + .addModifiers(KModifier.PUBLIC) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |@Something( + | SomeType.PROPERTY, + | 12, + | "goodbye", + |) + |public class Foo + | + """.trimMargin(), + ) + } + + @Test fun enumWithSubclassing() { + val roshambo = TypeSpec.enumBuilder("Roshambo") + .addModifiers(KModifier.PUBLIC) + .addEnumConstant( + "ROCK", + TypeSpec.anonymousClassBuilder() + .addKdoc("Avalanche!\n") + .build(), + ) + .addEnumConstant( + "PAPER", + TypeSpec.anonymousClassBuilder() + .addSuperclassConstructorParameter("%S", "flat") + .addFunction( + FunSpec.builder("toString") + .addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE, KModifier.OVERRIDE) + .returns(String::class) + .addCode("return %S\n", "paper airplane!") + .build(), + ) + .build(), + ) + .addEnumConstant( + "SCISSORS", + TypeSpec.anonymousClassBuilder() + .addSuperclassConstructorParameter("%S", "peace sign") + .build(), + ) + .addProperty( + PropertySpec.builder("handPosition", String::class, KModifier.PRIVATE) + .initializer("handPosition") + .build(), + ) + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("handPosition", String::class) + .build(), + ) + .addFunction( + FunSpec.constructorBuilder() + .addCode("this(%S)\n", "fist") + .build(), + ) + .build() + assertThat(toString(roshambo)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + | + |public enum class Roshambo( + | private val handPosition: String, + |) { + | /** + | * Avalanche! + | */ + | ROCK, + | PAPER("flat") { + | public override fun toString(): String = "paper airplane!" + | }, + | SCISSORS("peace sign"), + | ; + | + | public constructor() { + | this("fist") + | } + |} + | + """.trimMargin(), + ) + } + + /** https://github.com/square/javapoet/issues/193 */ + @Test fun enumsMayDefineAbstractFunctions() { + val roshambo = TypeSpec.enumBuilder("Tortilla") + .addModifiers(KModifier.PUBLIC) + .addEnumConstant( + "CORN", + TypeSpec.anonymousClassBuilder() + .addFunction( + FunSpec.builder("fold") + .addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE) + .build(), + ) + .build(), + ) + .addFunction( + FunSpec.builder("fold") + .addModifiers(KModifier.PUBLIC, KModifier.ABSTRACT) + .build(), + ) + .build() + assertThat(toString(roshambo)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + | + |public enum class Tortilla { + | CORN { + | public override fun fold(): Unit { + | } + | }, + | ; + | + | public abstract fun fold(): Unit + |} + | + """.trimMargin(), + ) + } + + @Test fun enumsMayHavePrivateConstructorVals() { + val enum = TypeSpec.enumBuilder("MyEnum") + .primaryConstructor( + FunSpec.constructorBuilder().addParameter("number", Int::class).build(), + ) + .addProperty( + PropertySpec.builder("number", Int::class) + .addModifiers(PRIVATE) + .initializer("number") + .build(), + ) + .build() + assertThat(toString(enum)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Int + | + |public enum class MyEnum( + | private val number: Int, + |) + | + """.trimMargin(), + ) + } + + @Test fun classesMayHavePrivateConstructorPropertiesInTheirPrimaryConstructors() { + val myClass = TypeSpec.classBuilder("MyClass") + .primaryConstructor( + FunSpec.constructorBuilder().addParameter("number", Int::class).build(), + ) + .addProperty( + PropertySpec.builder("number", Int::class) + .initializer("number") + .addModifiers(PRIVATE) + .build(), + ) + .build() + assertThat(toString(myClass)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Int + | + |public class MyClass( + | private val number: Int, + |) + | + """.trimMargin(), + ) + } + + @Test fun sealedClassesMayDefineAbstractMembers() { + val sealedClass = TypeSpec.classBuilder("Sealed") + .addModifiers(KModifier.SEALED) + .addProperty(PropertySpec.builder("name", String::class).addModifiers(ABSTRACT).build()) + .addFunction(FunSpec.builder("fold").addModifiers(KModifier.PUBLIC, KModifier.ABSTRACT).build()) + .build() + assertThat(toString(sealedClass)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.Unit + | + |public sealed class Sealed { + | public abstract val name: String + | + | public abstract fun fold(): Unit + |} + | + """.trimMargin(), + ) + } + + @Test fun classesMayHaveVarargConstructorProperties() { + val variable = TypeSpec.classBuilder("Variable") + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter(ParameterSpec.builder("name", String::class, VARARG).build()) + .build(), + ) + .addProperty(PropertySpec.builder("name", String::class).initializer("name").build()) + .build() + assertThat(toString(variable)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + | + |public class Variable( + | public vararg val name: String, + |) + | + """.trimMargin(), + ) + } + + /** https://github.com/square/kotlinpoet/issues/942 */ + @Test fun noConstructorPropertiesWithCustomGetter() { + val taco = TypeSpec.classBuilder("ObservantTaco") + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter(ParameterSpec.builder("contents", String::class).build()) + .build(), + ) + .addProperty( + PropertySpec.builder("contents", String::class).initializer("contents") + .getter(FunSpec.getterBuilder().addCode("println(%S)\nreturn field", "contents observed!").build()) + .build(), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + | + |public class ObservantTaco( + | contents: String, + |) { + | public val contents: String = contents + | get() { + | println("contents observed!") + | return field + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun noConstructorPropertiesWithCustomSetter() { + val taco = TypeSpec.classBuilder("ObservantTaco") + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter(ParameterSpec.builder("contents", String::class).build()) + .build(), + ) + .addProperty( + PropertySpec.builder("contents", String::class).initializer("contents") + .mutable() + .setter( + FunSpec.setterBuilder() + .addParameter("value", String::class) + .addCode("println(%S)\nfield = value", "contents changed!").build(), + ) + .build(), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + | + |public class ObservantTaco( + | contents: String, + |) { + | public var contents: String = contents + | set(`value`) { + | println("contents changed!") + | field = value + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun onlyEnumsMayHaveEnumConstants() { + assertThrows<IllegalStateException> { + TypeSpec.classBuilder("Roshambo") + .addEnumConstant("ROCK") + .build() + } + } + + /** https://github.com/square/kotlinpoet/issues/621 */ + @Test fun enumWithMembersButNoConstansts() { + val roshambo = TypeSpec.enumBuilder("RenderPassCreate") + .addType(TypeSpec.companionObjectBuilder().build()) + .build() + assertThat(toString(roshambo)).isEqualTo( + """ + |package com.squareup.tacos + | + |public enum class RenderPassCreate { + | ; + | public companion object + |} + | + """.trimMargin(), + ) + } + + @Test fun enumWithMembersButNoConstructorCall() { + val roshambo = TypeSpec.enumBuilder("Roshambo") + .addEnumConstant( + "SPOCK", + TypeSpec.anonymousClassBuilder() + .addFunction( + FunSpec.builder("toString") + .addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE) + .returns(String::class) + .addStatement("return %S", "west side") + .build(), + ) + .build(), + ) + .build() + assertThat(toString(roshambo)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + | + |public enum class Roshambo { + | SPOCK { + | public override fun toString(): String = "west side" + | }, + |} + | + """.trimMargin(), + ) + } + + /** https://github.com/square/javapoet/issues/253 */ + @Test fun enumWithAnnotatedValues() { + val roshambo = TypeSpec.enumBuilder("Roshambo") + .addModifiers(KModifier.PUBLIC) + .addEnumConstant( + "ROCK", + TypeSpec.anonymousClassBuilder() + .addAnnotation(java.lang.Deprecated::class) + .build(), + ) + .addEnumConstant("PAPER") + .addEnumConstant("SCISSORS") + .build() + assertThat(toString(roshambo)).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.lang.Deprecated + | + |public enum class Roshambo { + | @Deprecated + | ROCK, + | PAPER, + | SCISSORS, + |} + | + """.trimMargin(), + ) + } + + @Test fun funThrows() { + val taco = TypeSpec.classBuilder("Taco") + .addModifiers(KModifier.ABSTRACT) + .addFunction( + FunSpec.builder("throwOne") + .throws(IOException::class) + .build(), + ) + .addFunction( + FunSpec.builder("throwTwo") + .throws(IOException::class.asClassName(), ClassName(tacosPackage, "SourCreamException")) + .build(), + ) + .addFunction( + FunSpec.builder("abstractThrow") + .addModifiers(KModifier.ABSTRACT) + .throws(IOException::class) + .build(), + ) + .addFunction( + FunSpec.builder("nativeThrow") + .addModifiers(KModifier.EXTERNAL) + .throws(IOException::class) + .build(), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.io.IOException + |import kotlin.Unit + |import kotlin.jvm.Throws + | + |public abstract class Taco { + | @Throws(IOException::class) + | public fun throwOne(): Unit { + | } + | + | @Throws( + | IOException::class, + | SourCreamException::class, + | ) + | public fun throwTwo(): Unit { + | } + | + | @Throws(IOException::class) + | public abstract fun abstractThrow(): Unit + | + | @Throws(IOException::class) + | public external fun nativeThrow(): Unit + |} + | + """.trimMargin(), + ) + } + + @Test fun typeVariables() { + val t = TypeVariableName("T") + val p = TypeVariableName("P", Number::class) + val location = ClassName(tacosPackage, "Location") + val typeSpec = TypeSpec.classBuilder("Location") + .addTypeVariable(t) + .addTypeVariable(p) + .addSuperinterface(Comparable::class.asClassName().parameterizedBy(p)) + .addProperty("label", t) + .addProperty("x", p) + .addProperty("y", p) + .addFunction( + FunSpec.builder("compareTo") + .addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE) + .returns(Int::class) + .addParameter("p", p) + .addStatement("return 0") + .build(), + ) + .addFunction( + FunSpec.builder("of") + .addModifiers(KModifier.PUBLIC) + .addTypeVariable(t) + .addTypeVariable(p) + .returns(location.parameterizedBy(t, p)) + .addParameter("label", t) + .addParameter("x", p) + .addParameter("y", p) + .addStatement("throw %T(%S)", UnsupportedOperationException::class, "TODO") + .build(), + ) + .build() + assertThat(toString(typeSpec)).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.lang.UnsupportedOperationException + |import kotlin.Comparable + |import kotlin.Int + |import kotlin.Number + | + |public class Location<T, P : Number> : Comparable<P> { + | public val label: T + | + | public val x: P + | + | public val y: P + | + | public override fun compareTo(p: P): Int = 0 + | + | public fun <T, P : Number> of( + | label: T, + | x: P, + | y: P, + | ): Location<T, P> = throw UnsupportedOperationException("TODO") + |} + | + """.trimMargin(), + ) + } + + @Test fun typeVariableWithBounds() { + val a = AnnotationSpec.builder(ClassName("com.squareup.tacos", "A")).build() + val p = TypeVariableName("P", Number::class) + val q = TypeVariableName("Q", Number::class).copy(annotations = listOf(a)) as TypeVariableName + val typeSpec = TypeSpec.classBuilder("Location") + .addTypeVariable(p.copy(bounds = p.bounds + listOf(Comparable::class.asTypeName()))) + .addTypeVariable(q.copy(bounds = q.bounds + listOf(Comparable::class.asTypeName()))) + .addProperty("x", p) + .addProperty("y", q) + .primaryConstructor(FunSpec.constructorBuilder().build()) + .superclass(Number::class) + .addSuperinterface(Comparable::class) + .build() + assertThat(toString(typeSpec)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Comparable + |import kotlin.Number + | + |public class Location<P, Q>() : Number(), Comparable where P : Number, P : Comparable, Q : Number, Q + | : Comparable { + | public val x: P + | + | public val y: @A Q + |} + | + """.trimMargin(), + ) + } + + @Test fun classImplementsExtends() { + val taco = ClassName(tacosPackage, "Taco") + val food = ClassName("com.squareup.tacos", "Food") + val typeSpec = TypeSpec.classBuilder("Taco") + .addModifiers(KModifier.ABSTRACT) + .superclass(AbstractSet::class.asClassName().parameterizedBy(food)) + .addSuperinterface(Serializable::class) + .addSuperinterface(Comparable::class.asClassName().parameterizedBy(taco)) + .build() + assertThat(toString(typeSpec)).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.io.Serializable + |import java.util.AbstractSet + |import kotlin.Comparable + | + |public abstract class Taco : AbstractSet<Food>(), Serializable, Comparable<Taco> + | + """.trimMargin(), + ) + } + + @Test fun classImplementsExtendsSameName() { + val javapoetTaco = ClassName(tacosPackage, "Taco") + val tacoBellTaco = ClassName("com.taco.bell", "Taco") + val fishTaco = ClassName("org.fish.taco", "Taco") + val typeSpec = TypeSpec.classBuilder("Taco") + .superclass(fishTaco) + .addSuperinterface(Comparable::class.asClassName().parameterizedBy(javapoetTaco)) + .addSuperinterface(tacoBellTaco) + .build() + assertThat(toString(typeSpec)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Comparable + | + |public class Taco : org.fish.taco.Taco(), Comparable<Taco>, com.taco.bell.Taco + | + """.trimMargin(), + ) + } + + @Test fun classImplementsInnerClass() { + val outer = ClassName(tacosPackage, "Outer") + val inner = outer.nestedClass("Inner") + val callable = Callable::class.asClassName() + val typeSpec = TypeSpec.classBuilder("Outer") + .superclass(callable.parameterizedBy(inner)) + .addType( + TypeSpec.classBuilder("Inner") + .addModifiers(KModifier.INNER) + .build(), + ) + .build() + + assertThat(toString(typeSpec)).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.util.concurrent.Callable + | + |public class Outer : Callable<Outer.Inner>() { + | public inner class Inner + |} + | + """.trimMargin(), + ) + } + + @Test fun enumImplements() { + val typeSpec = TypeSpec.enumBuilder("Food") + .addSuperinterface(Serializable::class) + .addSuperinterface(Cloneable::class) + .addEnumConstant("LEAN_GROUND_BEEF") + .addEnumConstant("SHREDDED_CHEESE") + .build() + assertThat(toString(typeSpec)).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.io.Serializable + |import kotlin.Cloneable + | + |public enum class Food : Serializable, Cloneable { + | LEAN_GROUND_BEEF, + | SHREDDED_CHEESE, + |} + | + """.trimMargin(), + ) + } + + @Test fun enumWithConstructorsAndKeywords() { + val primaryConstructor = FunSpec.constructorBuilder() + .addParameter("value", Int::class) + .build() + val typeSpec = TypeSpec.enumBuilder("Sort") + .primaryConstructor(primaryConstructor) + .addEnumConstant( + "open", + TypeSpec.anonymousClassBuilder() + .addSuperclassConstructorParameter("%L", 0) + .build(), + ) + .addEnumConstant( + "closed", + TypeSpec.anonymousClassBuilder() + .addSuperclassConstructorParameter("%L", 1) + .build(), + ) + .build() + assertThat(toString(typeSpec)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Int + | + |public enum class Sort( + | `value`: Int, + |) { + | `open`(0), + | closed(1), + |} + | + """.trimMargin(), + ) + } + + @Test fun interfaceExtends() { + val taco = ClassName(tacosPackage, "Taco") + val typeSpec = TypeSpec.interfaceBuilder("Taco") + .addSuperinterface(Serializable::class) + .addSuperinterface(Comparable::class.asClassName().parameterizedBy(taco)) + .build() + assertThat(toString(typeSpec)).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.io.Serializable + |import kotlin.Comparable + | + |public interface Taco : Serializable, Comparable<Taco> + | + """.trimMargin(), + ) + } + + @Test fun funInterface() { + val taco = ClassName(tacosPackage, "Taco") + val typeSpec = TypeSpec.funInterfaceBuilder(taco) + .addFunction( + FunSpec.builder("sam") + .addModifiers(ABSTRACT) + .build(), + ) + .addFunction(FunSpec.builder("notSam").build()) + .build() + assertThat(typeSpec.isFunctionalInterface).isTrue() + assertThat(toString(typeSpec)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + | + |public fun interface Taco { + | public fun sam(): Unit + | + | public fun notSam(): Unit { + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun funInterface_empty_shouldError() { + assertThrows<IllegalStateException> { + TypeSpec.funInterfaceBuilder("Taco") + .build() + }.hasMessageThat() + .contains("Functional interfaces must have exactly one abstract function. Contained 0") + } + + @Test fun funInterface_multipleAbstract_shouldError() { + assertThrows<IllegalStateException> { + TypeSpec.funInterfaceBuilder("Taco") + .addFunction( + FunSpec.builder("fun1") + .addModifiers(ABSTRACT) + .build(), + ) + .addFunction( + FunSpec.builder("fun2") + .addModifiers(ABSTRACT) + .build(), + ) + .build() + }.hasMessageThat() + .contains("Functional interfaces must have exactly one abstract function. Contained 2") + } + + @Test fun nestedClasses() { + val taco = ClassName(tacosPackage, "Combo", "Taco") + val topping = ClassName(tacosPackage, "Combo", "Taco", "Topping") + val chips = ClassName(tacosPackage, "Combo", "Chips") + val sauce = ClassName(tacosPackage, "Combo", "Sauce") + val typeSpec = TypeSpec.classBuilder("Combo") + .addProperty("taco", taco) + .addProperty("chips", chips) + .addType( + TypeSpec.classBuilder(taco.simpleName) + .addProperty("toppings", List::class.asClassName().parameterizedBy(topping)) + .addProperty("sauce", sauce) + .addType( + TypeSpec.enumBuilder(topping.simpleName) + .addEnumConstant("SHREDDED_CHEESE") + .addEnumConstant("LEAN_GROUND_BEEF") + .build(), + ) + .build(), + ) + .addType( + TypeSpec.classBuilder(chips.simpleName) + .addProperty("topping", topping) + .addProperty("dippingSauce", sauce) + .build(), + ) + .addType( + TypeSpec.enumBuilder(sauce.simpleName) + .addEnumConstant("SOUR_CREAM") + .addEnumConstant("SALSA") + .addEnumConstant("QUESO") + .addEnumConstant("MILD") + .addEnumConstant("FIRE") + .build(), + ) + .build() + + assertThat(toString(typeSpec)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.collections.List + | + |public class Combo { + | public val taco: Taco + | + | public val chips: Chips + | + | public class Taco { + | public val toppings: List<Topping> + | + | public val sauce: Sauce + | + | public enum class Topping { + | SHREDDED_CHEESE, + | LEAN_GROUND_BEEF, + | } + | } + | + | public class Chips { + | public val topping: Taco.Topping + | + | public val dippingSauce: Sauce + | } + | + | public enum class Sauce { + | SOUR_CREAM, + | SALSA, + | QUESO, + | MILD, + | FIRE, + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun annotation() { + val annotation = TypeSpec.annotationBuilder("MyAnnotation") + .addModifiers(KModifier.PUBLIC) + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter( + ParameterSpec.builder("test", Int::class) + .build(), + ) + .build(), + ) + .addProperty( + PropertySpec.builder("test", Int::class) + .initializer("test") + .build(), + ) + .build() + + assertThat(toString(annotation)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Int + | + |public annotation class MyAnnotation( + | public val test: Int, + |) + | + """.trimMargin(), + ) + } + + @Test fun annotationWithNestedTypes() { + val annotationName = ClassName(tacosPackage, "TacoDelivery") + val kindName = annotationName.nestedClass("Kind") + val annotation = TypeSpec.annotationBuilder(annotationName) + .addModifiers(PUBLIC) + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter( + ParameterSpec.builder("kind", kindName) + .build(), + ) + .addParameter( + ParameterSpec.builder("quantity", Int::class) + .defaultValue("QUANTITY_DEFAULT") + .build(), + ) + .build(), + ) + .addProperty( + PropertySpec.builder("kind", kindName) + .initializer("kind") + .build(), + ) + .addProperty( + PropertySpec.builder("quantity", Int::class) + .initializer("quantity") + .build(), + ) + .addType( + TypeSpec.enumBuilder("Kind") + .addEnumConstant("SOFT") + .addEnumConstant("HARD") + .build(), + ) + .addType( + TypeSpec.companionObjectBuilder() + .addProperty( + PropertySpec + .builder("QUANTITY_DEFAULT", Int::class, KModifier.CONST) + .initializer("%L", 10_000) + .build(), + ) + .build(), + ) + .build() + + assertThat(toString(annotation)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Int + | + |public annotation class TacoDelivery( + | public val kind: Kind, + | public val quantity: Int = QUANTITY_DEFAULT, + |) { + | public enum class Kind { + | SOFT, + | HARD, + | } + | + | public companion object { + | public const val QUANTITY_DEFAULT: Int = 10_000 + | } + |} + | + """.trimMargin(), + ) + } + + @Ignore @Test + fun innerAnnotationInAnnotationDeclaration() { + val bar = TypeSpec.annotationBuilder("Bar") + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter( + ParameterSpec.builder("value", java.lang.Deprecated::class) + .build(), + ) + .build(), + ) + .addProperty( + PropertySpec.builder("value", java.lang.Deprecated::class) + .initializer("value") + .build(), + ) + .build() + + assertThat(toString(bar)).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.lang.Deprecated + | + |annotation class Bar() { + | fun value(): Deprecated default @Deprecated + |} + | + """.trimMargin(), + ) + } + + @Test fun interfaceWithProperties() { + val taco = TypeSpec.interfaceBuilder("Taco") + .addProperty("v", Int::class) + .build() + + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Int + | + |public interface Taco { + | public val v: Int + |} + | + """.trimMargin(), + ) + } + + @Test fun expectClass() { + val classA = TypeSpec.expectClassBuilder("ClassA") + .addFunction( + FunSpec.builder("test") + .build(), + ) + .build() + + assertThat(classA.toString()).isEqualTo( + """ + |public expect class ClassA { + | public fun test(): kotlin.Unit + |} + | + """.trimMargin(), + ) + } + + @Test fun nestedExpectCompanionObjectWithFunction() { + val classA = TypeSpec.expectClassBuilder("ClassA") + .addType( + TypeSpec.companionObjectBuilder() + .addFunction( + FunSpec.builder("test") + .build(), + ) + .build(), + ) + .build() + + assertThat(classA.toString()).isEqualTo( + """ + |public expect class ClassA { + | public companion object { + | public fun test(): kotlin.Unit + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun nestedExpectClassWithFunction() { + val classA = TypeSpec.expectClassBuilder("ClassA") + .addType( + TypeSpec.classBuilder("ClassB") + .addFunction( + FunSpec.builder("test") + .build(), + ) + .build(), + ) + .build() + + assertThat(classA.toString()).isEqualTo( + """ + |public expect class ClassA { + | public class ClassB { + | public fun test(): kotlin.Unit + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun deeplyNestedExpectClassWithFunction() { + val classA = TypeSpec.expectClassBuilder("ClassA") + .addType( + TypeSpec.classBuilder("ClassB") + .addType( + TypeSpec.classBuilder("ClassC") + .addFunction( + FunSpec.builder("test") + .build(), + ) + .build(), + ) + .build(), + ) + .build() + + assertThat(classA.toString()).isEqualTo( + """ + |public expect class ClassA { + | public class ClassB { + | public class ClassC { + | public fun test(): kotlin.Unit + | } + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun veryDeeplyNestedExpectClassWithFunction() { + val classA = TypeSpec.expectClassBuilder("ClassA") + .addType( + TypeSpec.classBuilder("ClassB") + .addType( + TypeSpec.classBuilder("ClassC") + .addType( + TypeSpec.classBuilder("ClassD") + .addFunction( + FunSpec.builder("test") + .build(), + ) + .build(), + ) + .build(), + ) + .build(), + ) + .build() + + assertThat(classA.toString()).isEqualTo( + """ + |public expect class ClassA { + | public class ClassB { + | public class ClassC { + | public class ClassD { + | public fun test(): kotlin.Unit + | } + | } + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun deeplyNestedExpectClassWithConstructor() { + val classA = TypeSpec.expectClassBuilder("ClassA") + .addType( + TypeSpec.classBuilder("ClassB") + .addType( + TypeSpec.classBuilder("ClassC") + .addFunction( + FunSpec.constructorBuilder() + .build(), + ) + .build(), + ) + .build(), + ) + .build() + + assertThat(classA.toString()).isEqualTo( + """ + |public expect class ClassA { + | public class ClassB { + | public class ClassC { + | public constructor() + | } + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun veryDeeplyNestedExpectClassWithConstructor() { + val classA = TypeSpec.expectClassBuilder("ClassA") + .addType( + TypeSpec.classBuilder("ClassB") + .addType( + TypeSpec.classBuilder("ClassC") + .addType( + TypeSpec.classBuilder("ClassD") + .addFunction( + FunSpec.constructorBuilder() + .build(), + ) + .build(), + ) + .build(), + ) + .build(), + ) + .build() + + assertThat(classA.toString()).isEqualTo( + """ + |public expect class ClassA { + | public class ClassB { + | public class ClassC { + | public class ClassD { + | public constructor() + | } + | } + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun interfaceWithMethods() { + val taco = TypeSpec.interfaceBuilder("Taco") + .addFunction( + FunSpec.builder("aMethod") + .addModifiers(KModifier.ABSTRACT) + .build(), + ) + .addFunction(FunSpec.builder("aDefaultMethod").build()) + .addFunction( + FunSpec.builder("aPrivateMethod") + .addModifiers(KModifier.PRIVATE) + .build(), + ) + .build() + + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + | + |public interface Taco { + | public fun aMethod(): Unit + | + | public fun aDefaultMethod(): Unit { + | } + | + | private fun aPrivateMethod(): Unit { + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun referencedAndDeclaredSimpleNamesConflict() { + val internalTop = PropertySpec.builder( + "internalTop", + ClassName(tacosPackage, "Top"), + ).build() + val internalBottom = PropertySpec.builder( + "internalBottom", + ClassName(tacosPackage, "Top", "Middle", "Bottom"), + ).build() + val externalTop = PropertySpec.builder( + "externalTop", + ClassName(donutsPackage, "Top"), + ).build() + val externalBottom = PropertySpec.builder( + "externalBottom", + ClassName(donutsPackage, "Bottom"), + ).build() + val top = TypeSpec.classBuilder("Top") + .addProperty(internalTop) + .addProperty(internalBottom) + .addProperty(externalTop) + .addProperty(externalBottom) + .addType( + TypeSpec.classBuilder("Middle") + .addProperty(internalTop) + .addProperty(internalBottom) + .addProperty(externalTop) + .addProperty(externalBottom) + .addType( + TypeSpec.classBuilder("Bottom") + .addProperty(internalTop) + .addProperty(internalBottom) + .addProperty(externalTop) + .addProperty(externalBottom) + .build(), + ) + .build(), + ) + .build() + assertThat(toString(top)).isEqualTo( + """ + |package com.squareup.tacos + | + |import com.squareup.donuts.Bottom + | + |public class Top { + | public val internalTop: Top + | + | public val internalBottom: Middle.Bottom + | + | public val externalTop: com.squareup.donuts.Top + | + | public val externalBottom: Bottom + | + | public class Middle { + | public val internalTop: Top + | + | public val internalBottom: Bottom + | + | public val externalTop: com.squareup.donuts.Top + | + | public val externalBottom: com.squareup.donuts.Bottom + | + | public class Bottom { + | public val internalTop: Top + | + | public val internalBottom: Bottom + | + | public val externalTop: com.squareup.donuts.Top + | + | public val externalBottom: com.squareup.donuts.Bottom + | } + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun simpleNamesConflictInThisAndOtherPackage() { + val internalOther = PropertySpec.builder( + "internalOther", + ClassName(tacosPackage, "Other"), + ).build() + val externalOther = PropertySpec.builder( + "externalOther", + ClassName(donutsPackage, "Other"), + ).build() + val gen = TypeSpec.classBuilder("Gen") + .addProperty(internalOther) + .addProperty(externalOther) + .build() + assertThat(toString(gen)).isEqualTo( + """ + |package com.squareup.tacos + | + |public class Gen { + | public val internalOther: Other + | + | public val externalOther: com.squareup.donuts.Other + |} + | + """.trimMargin(), + ) + } + + @Test fun intersectionType() { + val typeVariable = TypeVariableName("T", Comparator::class, Serializable::class) + val taco = TypeSpec.classBuilder("Taco") + .addFunction( + FunSpec.builder("getComparator") + .addTypeVariable(typeVariable) + .returns(typeVariable) + .addStatement("return null") + .build(), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.io.Serializable + |import java.util.Comparator + | + |public class Taco { + | public fun <T> getComparator(): T where T : Comparator, T : Serializable = null + |} + | + """.trimMargin(), + ) + } + + @Test fun primitiveArrayType() { + val taco = TypeSpec.classBuilder("Taco") + .addProperty("ints", IntArray::class) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.IntArray + | + |public class Taco { + | public val ints: IntArray + |} + | + """.trimMargin(), + ) + } + + @Test fun kdoc() { + val taco = TypeSpec.classBuilder("Taco") + .addKdoc("A hard or soft tortilla, loosely folded and filled with whatever\n") + .addKdoc("[random][%T] tex-mex stuff we could find in the pantry\n", Random::class) + .addKdoc(CodeBlock.of("and some [%T] cheese.\n", String::class)) + .addProperty( + PropertySpec.builder("soft", Boolean::class) + .addKdoc("True for a soft flour tortilla; false for a crunchy corn tortilla.\n") + .build(), + ) + .addFunction( + FunSpec.builder("refold") + .addKdoc( + "Folds the back of this taco to reduce sauce leakage.\n" + + "\n" + + "For [%T#KOREAN], the front may also be folded.\n", + Locale::class, + ) + .addParameter("locale", Locale::class) + .build(), + ) + .build() + // Mentioning a type in KDoc will not cause an import to be added (java.util.Random here), but + // the short name will be used if it's already imported (java.util.Locale here). + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.util.Locale + |import kotlin.Boolean + |import kotlin.Unit + | + |/** + | * A hard or soft tortilla, loosely folded and filled with whatever + | * [random][java.util.Random] tex-mex stuff we could find in the pantry + | * and some [kotlin.String] cheese. + | */ + |public class Taco { + | /** + | * True for a soft flour tortilla; false for a crunchy corn tortilla. + | */ + | public val soft: Boolean + | + | /** + | * Folds the back of this taco to reduce sauce leakage. + | * + | * For [Locale#KOREAN], the front may also be folded. + | */ + | public fun refold(locale: Locale): Unit { + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun kdocWithParameters() { + val taco = TypeSpec.classBuilder("Taco") + .addKdoc("A hard or soft tortilla, loosely folded and filled with whatever\n") + .addKdoc("[random][%T] tex-mex stuff we could find in the pantry\n", Random::class) + .addKdoc(CodeBlock.of("and some [%T] cheese.\n", String::class)) + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter( + ParameterSpec.builder("temperature", Double::class) + .addKdoc( + CodeBlock.of( + "%L", + """ + |Taco temperature. Can be as cold as the famous ice tacos from + |the Andes, or hot with lava-like cheese from the depths of + |the Ninth Circle. + | + """.trimMargin(), + ), + ) + .build(), + ) + .addParameter("soft", Boolean::class) + .addParameter( + ParameterSpec.builder("mild", Boolean::class) + .addKdoc(CodeBlock.of("%L", "Whether the taco is mild (ew) or crunchy (ye).\n")) + .build(), + ) + .addParameter("nodoc", Int::class) + .build(), + ) + .addProperty( + PropertySpec.builder("soft", Boolean::class) + .addKdoc("True for a soft flour tortilla; false for a crunchy corn tortilla.\n") + .initializer("soft") + .build(), + ) + .addProperty( + PropertySpec.builder("mild", Boolean::class) + .addKdoc("No one likes mild tacos.") + .initializer("mild") + .build(), + ) + .addProperty( + PropertySpec.builder("nodoc", Int::class, KModifier.PRIVATE) + .initializer("nodoc") + .build(), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Boolean + |import kotlin.Double + |import kotlin.Int + | + |/** + | * A hard or soft tortilla, loosely folded and filled with whatever + | * [random][java.util.Random] tex-mex stuff we could find in the pantry + | * and some [kotlin.String] cheese. + | * + | * @param mild Whether the taco is mild (ew) or crunchy (ye). + | */ + |public class Taco( + | /** + | * Taco temperature. Can be as cold as the famous ice tacos from + | * the Andes, or hot with lava-like cheese from the depths of + | * the Ninth Circle. + | */ + | temperature: Double, + | /** + | * True for a soft flour tortilla; false for a crunchy corn tortilla. + | */ + | public val soft: Boolean, + | /** + | * No one likes mild tacos. + | */ + | public val mild: Boolean, + | private val nodoc: Int, + |) + | + """.trimMargin(), + ) + } + + @Test fun annotationsInAnnotations() { + val beef = ClassName(tacosPackage, "Beef") + val chicken = ClassName(tacosPackage, "Chicken") + val option = ClassName(tacosPackage, "Option") + val mealDeal = ClassName(tacosPackage, "MealDeal") + val menu = TypeSpec.classBuilder("Menu") + .addAnnotation( + AnnotationSpec.builder(mealDeal) + .addMember("%L = %L", "price", 500) + .addMember( + "%L = [%L, %L]", + "options", + AnnotationSpec.builder(option) + .addMember("%S", "taco") + .addMember("%T::class", beef) + .build(), + AnnotationSpec.builder(option) + .addMember("%S", "quesadilla") + .addMember("%T::class", chicken) + .build(), + ) + .build(), + ) + .build() + assertThat(toString(menu)).isEqualTo( + """ + |package com.squareup.tacos + | + |@MealDeal( + | price = 500, + | options = [Option("taco", Beef::class), Option("quesadilla", Chicken::class)], + |) + |public class Menu + | + """.trimMargin(), + ) + } + + @Test fun varargs() { + val taqueria = TypeSpec.classBuilder("Taqueria") + .addFunction( + FunSpec.builder("prepare") + .addParameter("workers", Int::class) + .addParameter("jobs", Runnable::class.asClassName(), VARARG) + .build(), + ) + .build() + assertThat(toString(taqueria)).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.lang.Runnable + |import kotlin.Int + |import kotlin.Unit + | + |public class Taqueria { + | public fun prepare(workers: Int, vararg jobs: Runnable): Unit { + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun varargsNotLast() { + val taqueria = TypeSpec.classBuilder("Taqueria") + .addFunction( + FunSpec.builder("prepare") + .addParameter("workers", Int::class) + .addParameter("jobs", Runnable::class.asClassName(), VARARG) + .addParameter("start", Boolean::class.asClassName()) + .build(), + ) + .build() + assertThat(toString(taqueria)).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.lang.Runnable + |import kotlin.Boolean + |import kotlin.Int + |import kotlin.Unit + | + |public class Taqueria { + | public fun prepare( + | workers: Int, + | vararg jobs: Runnable, + | start: Boolean, + | ): Unit { + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun codeBlocks() { + val ifBlock = CodeBlock.builder() + .beginControlFlow("if (a != b)") + .addStatement("return i") + .endControlFlow() + .build() + val funBody = CodeBlock.builder() + .addStatement("val size = %T.min(listA.size, listB.size)", Math::class) + .beginControlFlow("for (i in 0 until size)") + .addStatement("val %N = %N[i]", "a", "listA") + .addStatement("val %N = %N[i]", "b", "listB") + .add("%L", ifBlock) + .endControlFlow() + .addStatement("return size") + .build() + val propertyBlock = CodeBlock.builder() + .add("%T.<%T, %T>builder()", ImmutableMap::class, String::class, String::class) + .add("\n.add(%S, %S)", '\'', "'") + .add("\n.add(%S, %S)", '&', "&") + .add("\n.add(%S, %S)", '<', "<") + .add("\n.add(%S, %S)", '>', ">") + .add("\n.build()") + .build() + val escapeHtml = PropertySpec.builder( + "ESCAPE_HTML", + Map::class.parameterizedBy(String::class, String::class), + ) + .addModifiers(KModifier.PRIVATE) + .initializer(propertyBlock) + .build() + val util = TypeSpec.classBuilder("Util") + .addProperty(escapeHtml) + .addFunction( + FunSpec.builder("commonPrefixLength") + .returns(Int::class) + .addParameter("listA", List::class.parameterizedBy(String::class)) + .addParameter("listB", List::class.parameterizedBy(String::class)) + .addCode(funBody) + .build(), + ) + .build() + assertThat(toString(util)).isEqualTo( + """ + |package com.squareup.tacos + | + |import com.google.common.collect.ImmutableMap + |import java.lang.Math + |import kotlin.Int + |import kotlin.String + |import kotlin.collections.List + |import kotlin.collections.Map + | + |public class Util { + | private val ESCAPE_HTML: Map<String, String> = ImmutableMap.<String, String>builder() + | .add("'", "'") + | .add("&", "&") + | .add("<", "<") + | .add(">", ">") + | .build() + | + | public fun commonPrefixLength(listA: List<String>, listB: List<String>): Int { + | val size = Math.min(listA.size, listB.size) + | for (i in 0 until size) { + | val a = listA[i] + | val b = listB[i] + | if (a != b) { + | return i + | } + | } + | return size + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun indexedElseIf() { + val taco = TypeSpec.classBuilder("Taco") + .addFunction( + FunSpec.builder("choices") + .beginControlFlow("if (%1L != null || %1L == %2L)", "taco", "otherTaco") + .addStatement("%T.out.println(%S)", System::class, "only one taco? NOO!") + .nextControlFlow("else if (%1L.%3L && %2L.%3L)", "taco", "otherTaco", "isSupreme()") + .addStatement("%T.out.println(%S)", System::class, "taco heaven") + .endControlFlow() + .build(), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.lang.System + |import kotlin.Unit + | + |public class Taco { + | public fun choices(): Unit { + | if (taco != null || taco == otherTaco) { + | System.out.println("only one taco? NOO!") + | } else if (taco.isSupreme() && otherTaco.isSupreme()) { + | System.out.println("taco heaven") + | } + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun elseIf() { + val taco = TypeSpec.classBuilder("Taco") + .addFunction( + FunSpec.builder("choices") + .beginControlFlow("if (5 < 4) ") + .addStatement("%T.out.println(%S)", System::class, "wat") + .nextControlFlow("else if (5 < 6)") + .addStatement("%T.out.println(%S)", System::class, "hello") + .endControlFlow() + .build(), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.lang.System + |import kotlin.Unit + | + |public class Taco { + | public fun choices(): Unit { + | if (5 < 4) { + | System.out.println("wat") + | } else if (5 < 6) { + | System.out.println("hello") + | } + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun inlineIndent() { + val taco = TypeSpec.classBuilder("Taco") + .addFunction( + FunSpec.builder("inlineIndent") + .addCode("if (3 < 4) {\n⇥%T.out.println(%S);\n⇤}\n", System::class, "hello") + .build(), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.lang.System + |import kotlin.Unit + | + |public class Taco { + | public fun inlineIndent(): Unit { + | if (3 < 4) { + | System.out.println("hello"); + | } + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun defaultModifiersForMemberInterfacesAndEnums() { + val taco = TypeSpec.classBuilder("Taco") + .addType( + TypeSpec.classBuilder("Meat") + .build(), + ) + .addType( + TypeSpec.interfaceBuilder("Tortilla") + .build(), + ) + .addType( + TypeSpec.enumBuilder("Topping") + .addEnumConstant("SALSA") + .build(), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |public class Taco { + | public class Meat + | + | public interface Tortilla + | + | public enum class Topping { + | SALSA, + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun membersOrdering() { + // Hand out names in reverse-alphabetical order to defend against unexpected sorting. + val taco = TypeSpec.classBuilder("Members") + .addType(TypeSpec.classBuilder("Z").build()) + .addType(TypeSpec.classBuilder("Y").build()) + .addProperty("W", String::class) + .addProperty("U", String::class) + .addFunction(FunSpec.builder("T").build()) + .addFunction(FunSpec.builder("S").build()) + .addFunction(FunSpec.builder("R").build()) + .addFunction(FunSpec.builder("Q").build()) + .addFunction( + FunSpec.constructorBuilder() + .addParameter("p", Int::class) + .build(), + ) + .addFunction( + FunSpec.constructorBuilder() + .addParameter("o", Long::class) + .build(), + ) + .build() + // Static properties, instance properties, constructors, functions, classes. + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Int + |import kotlin.Long + |import kotlin.String + |import kotlin.Unit + | + |public class Members { + | public val W: String + | + | public val U: String + | + | public constructor(p: Int) + | + | public constructor(o: Long) + | + | public fun T(): Unit { + | } + | + | public fun S(): Unit { + | } + | + | public fun R(): Unit { + | } + | + | public fun Q(): Unit { + | } + | + | public class Z + | + | public class Y + |} + | + """.trimMargin(), + ) + } + + @Test fun nativeFunctions() { + val taco = TypeSpec.classBuilder("Taco") + .addFunction( + FunSpec.builder("nativeInt") + .addModifiers(KModifier.EXTERNAL) + .returns(Int::class) + .build(), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Int + | + |public class Taco { + | public external fun nativeInt(): Int + |} + | + """.trimMargin(), + ) + } + + @Test fun nullStringLiteral() { + val taco = TypeSpec.classBuilder("Taco") + .addProperty( + PropertySpec.builder("NULL", String::class) + .initializer("%S", null) + .build(), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + | + |public class Taco { + | public val NULL: String = null + |} + | + """.trimMargin(), + ) + } + + @Test fun annotationToString() { + val annotation = AnnotationSpec.builder(SuppressWarnings::class) + .addMember("%S", "unused") + .build() + assertThat(annotation.toString()).isEqualTo("@java.lang.SuppressWarnings(\"unused\")") + } + + @Test fun codeBlockToString() { + val codeBlock = CodeBlock.builder() + .addStatement("%T %N = %S.substring(0, 3)", String::class, "s", "taco") + .build() + assertThat(codeBlock.toString()).isEqualTo("kotlin.String s = \"taco\".substring(0, 3)\n") + } + + @Test fun propertyToString() { + val property = PropertySpec.builder("s", String::class) + .initializer("%S.substring(0, 3)", "taco") + .build() + assertThat(property.toString()) + .isEqualTo("val s: kotlin.String = \"taco\".substring(0, 3)\n") + } + + @Test fun functionToString() { + val funSpec = FunSpec.builder("toString") + .addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE) + .returns(String::class) + .addStatement("return %S", "taco") + .build() + assertThat(funSpec.toString()) + .isEqualTo("public override fun toString(): kotlin.String = \"taco\"\n") + } + + @Test fun constructorToString() { + val constructor = FunSpec.constructorBuilder() + .addModifiers(KModifier.PUBLIC) + .addParameter("taco", ClassName(tacosPackage, "Taco")) + .addStatement("this.%N = %N", "taco", "taco") + .build() + assertThat(constructor.toString()).isEqualTo( + "" + + "public constructor(taco: com.squareup.tacos.Taco) {\n" + + " this.taco = taco\n" + + "}\n", + ) + } + + @Test fun parameterToString() { + val parameter = ParameterSpec.builder("taco", ClassName(tacosPackage, "Taco")) + .addModifiers(KModifier.CROSSINLINE) + .addAnnotation(ClassName("javax.annotation", "Nullable")) + .build() + assertThat(parameter.toString()) + .isEqualTo("@javax.`annotation`.Nullable crossinline taco: com.squareup.tacos.Taco") + } + + @Test fun classToString() { + val type = TypeSpec.classBuilder("Taco") + .build() + assertThat(type.toString()).isEqualTo( + "" + + "public class Taco\n", + ) + } + + @Test fun anonymousClassToString() { + val type = TypeSpec.anonymousClassBuilder() + .addSuperinterface(Runnable::class) + .addFunction( + FunSpec.builder("run") + .addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE) + .build(), + ) + .build() + assertThat(type.toString()).isEqualTo( + """ + |object : java.lang.Runnable { + | public override fun run(): kotlin.Unit { + | } + |} + """.trimMargin(), + ) + } + + @Test fun interfaceClassToString() { + val type = TypeSpec.interfaceBuilder("Taco") + .build() + assertThat(type.toString()).isEqualTo( + """ + |public interface Taco + | + """.trimMargin(), + ) + } + + @Test fun annotationDeclarationToString() { + val type = TypeSpec.annotationBuilder("Taco") + .build() + assertThat(type.toString()).isEqualTo( + """ + |public annotation class Taco + | + """.trimMargin(), + ) + } + + private fun toString(typeSpec: TypeSpec): String { + return FileSpec.get(tacosPackage, typeSpec).toString() + } + + @Test fun multilineStatement() { + val taco = TypeSpec.classBuilder("Taco") + .addFunction( + FunSpec.builder("toString") + .addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE) + .returns(String::class) + .addStatement( + "val result = %S\n+ %S\n+ %S\n+ %S\n+ %S", + "Taco(", + "beef,", + "lettuce,", + "cheese", + ")", + ) + .addStatement("return result") + .build(), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + | + |public class Taco { + | public override fun toString(): String { + | val result = "Taco(" + | + "beef," + | + "lettuce," + | + "cheese" + | + ")" + | return result + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun multilineStatementWithAnonymousClass() { + val stringComparator = Comparator::class.parameterizedBy(String::class) + val listOfString = List::class.parameterizedBy(String::class) + val prefixComparator = TypeSpec.anonymousClassBuilder() + .addSuperinterface(stringComparator) + .addFunction( + FunSpec.builder("compare") + .addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE) + .returns(Int::class) + .addParameter("a", String::class) + .addParameter("b", String::class) + .addComment("Prefix the strings and compare them") + .addStatement("return a.substring(0, length)\n" + ".compareTo(b.substring(0, length))") + .build(), + ) + .build() + val taco = TypeSpec.classBuilder("Taco") + .addFunction( + FunSpec.builder("comparePrefix") + .returns(stringComparator) + .addParameter("length", Int::class) + .addComment("Return a new comparator for the target length.") + .addStatement("return %L", prefixComparator) + .build(), + ) + .addFunction( + FunSpec.builder("sortPrefix") + .addParameter("list", listOfString) + .addParameter("length", Int::class) + .addStatement("%T.sort(\nlist,\n%L)", Collections::class, prefixComparator) + .build(), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.util.Collections + |import java.util.Comparator + |import kotlin.Int + |import kotlin.String + |import kotlin.Unit + |import kotlin.collections.List + | + |public class Taco { + | public fun comparePrefix(length: Int): Comparator<String> { + | // Return a new comparator for the target length. + | return object : Comparator<String> { + | public override fun compare(a: String, b: String): Int { + | // Prefix the strings and compare them + | return a.substring(0, length) + | .compareTo(b.substring(0, length)) + | } + | } + | } + | + | public fun sortPrefix(list: List<String>, length: Int): Unit { + | Collections.sort( + | list, + | object : Comparator<String> { + | public override fun compare(a: String, b: String): Int { + | // Prefix the strings and compare them + | return a.substring(0, length) + | .compareTo(b.substring(0, length)) + | } + | }) + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun multilineStrings() { + val taco = TypeSpec.classBuilder("Taco") + .addProperty( + PropertySpec.builder("toppings", String::class) + .initializer("%S", "shell\nbeef\nlettuce\ncheese\n") + .build(), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + | + |public class Taco { + | public val toppings: String = ${"\"\"\""} + | |shell + | |beef + | |lettuce + | |cheese + | |${"\"\"\""}.trimMargin() + |} + | + """.trimMargin(), + ) + } + + @Test fun multipleAnnotationAddition() { + val taco = TypeSpec.classBuilder("Taco") + .addAnnotations( + listOf( + AnnotationSpec.builder(SuppressWarnings::class) + .addMember("%S", "unchecked") + .build(), + AnnotationSpec.builder(Deprecated::class).build(), + ), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.lang.Deprecated + |import java.lang.SuppressWarnings + | + |@SuppressWarnings("unchecked") + |@Deprecated + |public class Taco + | + """.trimMargin(), + ) + } + + @Test fun multiplePropertyAddition() { + val taco = TypeSpec.classBuilder("Taco") + .addProperties( + listOf( + PropertySpec.builder("ANSWER", Int::class, KModifier.CONST).build(), + PropertySpec.builder("price", BigDecimal::class, PRIVATE).build(), + ), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.math.BigDecimal + |import kotlin.Int + | + |public class Taco { + | public const val ANSWER: Int + | + | private val price: BigDecimal + |} + | + """.trimMargin(), + ) + } + + @Test fun multipleFunctionAddition() { + val taco = TypeSpec.classBuilder("Taco") + .addFunctions( + listOf( + FunSpec.builder("getAnswer") + .addModifiers(PUBLIC) + .returns(Int::class) + .addStatement("return %L", 42) + .build(), + FunSpec.builder("getRandomQuantity") + .addModifiers(PUBLIC) + .returns(Int::class) + .addKdoc("chosen by fair dice roll ;)\n") + .addStatement("return %L", 4) + .build(), + ), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Int + | + |public class Taco { + | public fun getAnswer(): Int = 42 + | + | /** + | * chosen by fair dice roll ;) + | */ + | public fun getRandomQuantity(): Int = 4 + |} + | + """.trimMargin(), + ) + } + + @Test fun multipleSuperinterfaceAddition() { + val taco = TypeSpec.classBuilder("Taco") + .addSuperinterfaces( + listOf( + Serializable::class.asTypeName(), + EventListener::class.asTypeName(), + ), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.io.Serializable + |import java.util.EventListener + | + |public class Taco : Serializable, EventListener + | + """.trimMargin(), + ) + } + + @Test fun multipleTypeVariableAddition() { + val location = TypeSpec.classBuilder("Location") + .addTypeVariables( + listOf( + TypeVariableName("T"), + TypeVariableName("P", Number::class), + ), + ) + .build() + assertThat(toString(location)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Number + | + |public class Location<T, P : Number> + | + """.trimMargin(), + ) + } + + @Test fun multipleTypeAddition() { + val taco = TypeSpec.classBuilder("Taco") + .addTypes( + listOf( + TypeSpec.classBuilder("Topping").build(), + TypeSpec.classBuilder("Sauce").build(), + ), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |public class Taco { + | public class Topping + | + | public class Sauce + |} + | + """.trimMargin(), + ) + } + + @Test fun tryCatch() { + val taco = TypeSpec.classBuilder("Taco") + .addFunction( + FunSpec.builder("addTopping") + .addParameter("topping", ClassName("com.squareup.tacos", "Topping")) + .beginControlFlow("try") + .addCode("/* do something tricky with the topping */\n") + .nextControlFlow( + "catch (e: %T)", + ClassName("com.squareup.tacos", "IllegalToppingException"), + ) + .endControlFlow() + .build(), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + | + |public class Taco { + | public fun addTopping(topping: Topping): Unit { + | try { + | /* do something tricky with the topping */ + | } catch (e: IllegalToppingException) { + | } + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun ifElse() { + val taco = TypeSpec.classBuilder("Taco") + .addFunction( + FunSpec.builder("isDelicious") + .addParameter("count", INT) + .returns(BOOLEAN) + .beginControlFlow("if (count > 0)") + .addStatement("return true") + .nextControlFlow("else") + .addStatement("return false") + .endControlFlow() + .build(), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Boolean + |import kotlin.Int + | + |public class Taco { + | public fun isDelicious(count: Int): Boolean { + | if (count > 0) { + | return true + | } else { + | return false + | } + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun whenReturn() { + val taco = TypeSpec.classBuilder("Taco") + .addFunction( + FunSpec.builder("toppingPrice") + .addParameter("topping", String::class) + .beginControlFlow("return when(topping)") + .addStatement("%S -> 1", "beef") + .addStatement("%S -> 2", "lettuce") + .addStatement("%S -> 3", "cheese") + .addStatement("else -> throw IllegalToppingException(topping)") + .endControlFlow() + .build(), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + | + |public class Taco { + | public fun toppingPrice(topping: String) = when(topping) { + | "beef" -> 1 + | "lettuce" -> 2 + | "cheese" -> 3 + | else -> throw IllegalToppingException(topping) + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun literalFromAnything() { + val value = object : Any() { + override fun toString(): String { + return "foo" + } + } + assertThat(CodeBlock.of("%L", value).toString()).isEqualTo("foo") + } + + @Test fun nameFromCharSequence() { + assertThat(CodeBlock.of("%N", "text").toString()).isEqualTo("text") + } + + @Test fun nameFromProperty() { + val property = PropertySpec.builder("property", String::class).build() + assertThat(CodeBlock.of("%N", property).toString()).isEqualTo("`property`") + } + + @Test fun nameFromParameter() { + val parameter = ParameterSpec.builder("parameter", String::class).build() + assertThat(CodeBlock.of("%N", parameter).toString()).isEqualTo("parameter") + } + + @Test fun nameFromFunction() { + val funSpec = FunSpec.builder("method") + .addModifiers(KModifier.ABSTRACT) + .returns(String::class) + .build() + assertThat(CodeBlock.of("%N", funSpec).toString()).isEqualTo("method") + } + + @Test fun nameFromType() { + val type = TypeSpec.classBuilder("Type").build() + assertThat(CodeBlock.of("%N", type).toString()).isEqualTo("Type") + } + + @Test fun nameFromUnsupportedType() { + assertThrows<IllegalArgumentException> { + CodeBlock.builder().add("%N", String::class) + }.hasMessageThat().isEqualTo("expected name but was " + String::class) + } + + @Test fun stringFromAnything() { + val value = object : Any() { + override fun toString(): String { + return "foo" + } + } + assertThat(CodeBlock.of("%S", value).toString()).isEqualTo("\"foo\"") + } + + @Test fun stringFromNull() { + assertThat(CodeBlock.of("%S", null).toString()).isEqualTo("null") + } + + @Test fun typeFromTypeName() { + val typeName = String::class.asTypeName() + assertThat(CodeBlock.of("%T", typeName).toString()).isEqualTo("kotlin.String") + } + + @Test fun typeFromTypeMirror() { + val mirror = getElement(String::class).asType() + assertThat(CodeBlock.of("%T", mirror).toString()).isEqualTo("java.lang.String") + } + + @Test fun typeFromTypeElement() { + val element = getElement(String::class) + assertThat(CodeBlock.of("%T", element).toString()).isEqualTo("java.lang.String") + } + + @Test fun typeFromReflectType() { + assertThat(CodeBlock.of("%T", String::class).toString()).isEqualTo("kotlin.String") + } + + @Test fun typeFromUnsupportedType() { + assertThrows<IllegalArgumentException> { + CodeBlock.builder().add("%T", "kotlin.String") + }.hasMessageThat().isEqualTo("expected type but was kotlin.String") + } + + @Test fun tooFewArguments() { + assertThrows<IllegalArgumentException> { + CodeBlock.builder().add("%S") + }.hasMessageThat().isEqualTo("index 1 for '%S' not in range (received 0 arguments)") + } + + @Test fun unusedArgumentsRelative() { + assertThrows<IllegalArgumentException> { + CodeBlock.builder().add("%L %L", "a", "b", "c") + }.hasMessageThat().isEqualTo("unused arguments: expected 2, received 3") + } + + @Test fun unusedArgumentsIndexed() { + assertThrows<IllegalArgumentException> { + CodeBlock.builder().add("%1L %2L", "a", "b", "c") + }.hasMessageThat().isEqualTo("unused argument: %3") + + assertThrows<IllegalArgumentException> { + CodeBlock.builder().add("%1L %1L %1L", "a", "b", "c") + }.hasMessageThat().isEqualTo("unused arguments: %2, %3") + + assertThrows<IllegalArgumentException> { + CodeBlock.builder().add("%3L %1L %3L %1L %3L", "a", "b", "c", "d") + }.hasMessageThat().isEqualTo("unused arguments: %2, %4") + } + + @Test fun superClassOnlyValidForClasses() { + assertThrows<IllegalStateException> { + TypeSpec.annotationBuilder("A").superclass(Any::class.asClassName()) + } + + assertThrows<IllegalStateException> { + TypeSpec.enumBuilder("E").superclass(Any::class.asClassName()) + } + + assertThrows<IllegalStateException> { + TypeSpec.interfaceBuilder("I").superclass(Any::class.asClassName()) + } + } + + @Test fun superClassConstructorParametersOnlyValidForClasses() { + assertThrows<IllegalStateException> { + TypeSpec.annotationBuilder("A").addSuperclassConstructorParameter("") + } + + assertThrows<IllegalStateException> { + TypeSpec.enumBuilder("E").addSuperclassConstructorParameter("") + } + + assertThrows<IllegalStateException> { + TypeSpec.interfaceBuilder("I").addSuperclassConstructorParameter("") + } + } + + @Test fun anonymousClassesCannotHaveModifiersOrTypeVariable() { + assertThrows<IllegalStateException> { + TypeSpec.anonymousClassBuilder().addModifiers(PUBLIC) + } + + assertThrows<IllegalStateException> { + TypeSpec.anonymousClassBuilder().addTypeVariable(TypeVariableName("T")).build() + } + + assertThrows<IllegalStateException> { + TypeSpec.anonymousClassBuilder().addTypeVariables(listOf(TypeVariableName("T"))).build() + } + } + + @Test fun invalidSuperClass() { + assertThrows<IllegalStateException> { + TypeSpec.classBuilder("foo") + .superclass(List::class) + .superclass(Map::class) + } + } + + @Test fun staticCodeBlock() { + val taco = TypeSpec.classBuilder("Taco") + .addProperty("foo", String::class, KModifier.PRIVATE) + .addProperty( + PropertySpec.builder("FOO", String::class, KModifier.PRIVATE, KModifier.CONST) + .initializer("%S", "FOO") + .build(), + ) + .addFunction( + FunSpec.builder("toString") + .addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE) + .returns(String::class) + .addStatement("return FOO") + .build(), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + | + |public class Taco { + | private val foo: String + | + | private const val FOO: String = "FOO" + | + | public override fun toString(): String = FOO + |} + | + """.trimMargin(), + ) + } + + @Test fun initializerBlockInRightPlace() { + val taco = TypeSpec.classBuilder("Taco") + .addProperty("foo", String::class, KModifier.PRIVATE) + .addProperty( + PropertySpec.builder("FOO", String::class, KModifier.PRIVATE, KModifier.CONST) + .initializer("%S", "FOO") + .build(), + ) + .addFunction(FunSpec.constructorBuilder().build()) + .addFunction( + FunSpec.builder("toString") + .addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE) + .returns(String::class) + .addStatement("return FOO") + .build(), + ) + .addInitializerBlock( + CodeBlock.builder() + .addStatement("foo = %S", "FOO") + .build(), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + | + |public class Taco { + | private val foo: String + | + | private const val FOO: String = "FOO" + | + | init { + | foo = "FOO" + | } + | + | public constructor() + | + | public override fun toString(): String = FOO + |} + | + """.trimMargin(), + ) + } + + @Test fun initializersToBuilder() { + // Tests if toBuilder() contains instance initializers + val taco = TypeSpec.classBuilder("Taco") + .addProperty(PropertySpec.builder("foo", String::class, KModifier.PRIVATE).build()) + .addProperty( + PropertySpec.builder("FOO", String::class, KModifier.PRIVATE, KModifier.CONST) + .initializer("%S", "FOO") + .build(), + ) + .addFunction(FunSpec.constructorBuilder().build()) + .addFunction( + FunSpec.builder("toString") + .addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE) + .returns(String::class) + .addStatement("return FOO") + .build(), + ) + .build() + + val recreatedTaco = taco.toBuilder().build() + assertThat(toString(taco)).isEqualTo(toString(recreatedTaco)) + + val initializersAdded = taco.toBuilder() + .addInitializerBlock( + CodeBlock.builder() + .addStatement("foo = %S", "instanceFoo") + .build(), + ) + .build() + + assertThat(toString(initializersAdded)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + | + |public class Taco { + | private val foo: String + | + | private const val FOO: String = "FOO" + | + | init { + | foo = "instanceFoo" + | } + | + | public constructor() + | + | public override fun toString(): String = FOO + |} + | + """.trimMargin(), + ) + } + + @Test fun generalToBuilderEqualityTest() { + val originatingElement = FakeElement() + val comprehensiveTaco = TypeSpec.classBuilder("Taco") + .addKdoc("SuperTaco") + .addAnnotation(SuppressWarnings::class) + .addModifiers(DATA) + .addTypeVariable(TypeVariableName.of("State", listOf(ANY), IN).copy(reified = true)) + .addType( + TypeSpec.companionObjectBuilder() + .build(), + ) + .addType( + TypeSpec.classBuilder("InnerTaco") + .addModifiers(INNER) + .build(), + ) + .primaryConstructor( + FunSpec.constructorBuilder() + .build(), + ) + .superclass(ClassName("texmexfood", "TortillaBased")) + .addSuperclassConstructorParameter("true") + .addProperty( + PropertySpec.builder("meat", ClassName("texmexfood", "Meat")) + .build(), + ) + .addFunction( + FunSpec.builder("fold") + .build(), + ) + .addSuperinterface(ClassName("texmexfood", "Consumable")) + .addOriginatingElement(originatingElement) + .build() + + val newTaco = comprehensiveTaco.toBuilder().build() + assertThat(newTaco).isEqualTo(comprehensiveTaco) + assertThat(newTaco.originatingElements).containsExactly(originatingElement) + } + + @Test fun generalEnumToBuilderEqualityTest() { + val bestTexMexEnum = TypeSpec.enumBuilder("BestTexMex") + .addEnumConstant("TACO") + .addEnumConstant("BREAKFAST_TACO") + .build() + + assertThat(bestTexMexEnum.toBuilder().build()).isEqualTo(bestTexMexEnum) + } + + @Test fun generalInterfaceBuilderEqualityTest() { + val taco = TypeSpec.interfaceBuilder("Taco") + .addProperty("isVegan", Boolean::class) + .addSuperinterface(Runnable::class) + .build() + assertThat(taco.toBuilder().build()).isEqualTo(taco) + } + + @Test fun generalAnnotationBuilderEqualityTest() { + val annotation = TypeSpec.annotationBuilder("MyAnnotation") + .addModifiers(KModifier.PUBLIC) + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter( + ParameterSpec.builder("test", Int::class) + .build(), + ) + .build(), + ) + .addProperty( + PropertySpec.builder("test", Int::class) + .initializer("test") + .build(), + ) + .build() + assertThat(annotation.toBuilder().build()).isEqualTo(annotation) + } + + @Test fun generalExpectClassBuilderEqualityTest() { + val expectSpec = TypeSpec.expectClassBuilder("AtmoicRef") + .addModifiers(KModifier.INTERNAL) + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("value", Int::class) + .build(), + ) + .addProperty(PropertySpec.builder("value", Int::class).build()) + .addFunction( + FunSpec.builder("get") + .returns(Int::class) + .build(), + ) + .build() + assertThat(expectSpec.toBuilder().build()).isEqualTo(expectSpec) + } + + @Test fun generalObjectBuilderEqualityTest() { + val objectSpec = TypeSpec.objectBuilder("MyObject") + .addModifiers(KModifier.PUBLIC) + .addProperty("tacos", Int::class) + .addInitializerBlock(CodeBlock.builder().build()) + .addFunction( + FunSpec.builder("test") + .addModifiers(KModifier.PUBLIC) + .build(), + ) + .build() + assertThat(objectSpec.toBuilder().build()).isEqualTo(objectSpec) + } + + @Test fun generalAnonymousClassBuilderEqualityTest() { + val anonObjectSpec = TypeSpec.anonymousClassBuilder() + .addSuperinterface(Runnable::class) + .addFunction( + FunSpec.builder("run") + .addModifiers(KModifier.PUBLIC, KModifier.OVERRIDE) + .build(), + ) + .build() + assertThat(anonObjectSpec.toBuilder().build()).isEqualTo(anonObjectSpec) + } + + @Test fun initializerBlockUnsupportedExceptionOnInterface() { + val interfaceBuilder = TypeSpec.interfaceBuilder("Taco") + assertThrows<IllegalStateException> { + interfaceBuilder.addInitializerBlock(CodeBlock.builder().build()) + } + } + + @Test fun initializerBlockUnsupportedExceptionOnAnnotation() { + val annotationBuilder = TypeSpec.annotationBuilder("Taco") + assertThrows<IllegalStateException> { + annotationBuilder.addInitializerBlock(CodeBlock.builder().build()) + } + } + + @Test fun equalsAndHashCode() { + var a = TypeSpec.interfaceBuilder("taco").build() + var b = TypeSpec.interfaceBuilder("taco").build() + assertThat(a == b).isTrue() + assertThat(a.hashCode()).isEqualTo(b.hashCode()) + a = TypeSpec.classBuilder("taco").build() + b = TypeSpec.classBuilder("taco").build() + assertThat(a == b).isTrue() + assertThat(a.hashCode()).isEqualTo(b.hashCode()) + a = TypeSpec.enumBuilder("taco").addEnumConstant("SALSA").build() + b = TypeSpec.enumBuilder("taco").addEnumConstant("SALSA").build() + assertThat(a == b).isTrue() + assertThat(a.hashCode()).isEqualTo(b.hashCode()) + a = TypeSpec.annotationBuilder("taco").build() + b = TypeSpec.annotationBuilder("taco").build() + assertThat(a == b).isTrue() + assertThat(a.hashCode()).isEqualTo(b.hashCode()) + } + + @Test fun classNameFactories() { + val className = ClassName("com.example", "Example") + assertThat(TypeSpec.classBuilder(className).build().name).isEqualTo("Example") + assertThat(TypeSpec.interfaceBuilder(className).build().name).isEqualTo("Example") + assertThat(TypeSpec.enumBuilder(className).addEnumConstant("A").build().name).isEqualTo("Example") + assertThat(TypeSpec.annotationBuilder(className).build().name).isEqualTo("Example") + } + + @Test fun objectType() { + val type = TypeSpec.objectBuilder("MyObject") + .addModifiers(KModifier.PUBLIC) + .addProperty("tacos", Int::class) + .addInitializerBlock(CodeBlock.builder().build()) + .addFunction( + FunSpec.builder("test") + .addModifiers(KModifier.PUBLIC) + .build(), + ) + .build() + + assertThat(toString(type)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Int + |import kotlin.Unit + | + |public object MyObject { + | public val tacos: Int + | + | init { + | } + | + | public fun test(): Unit { + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun objectClassWithSupertype() { + val superclass = ClassName("com.squareup.wire", "Message") + val type = TypeSpec.objectBuilder("MyObject") + .addModifiers(KModifier.PUBLIC) + .superclass(superclass) + .addInitializerBlock(CodeBlock.builder().build()) + .addFunction( + FunSpec.builder("test") + .addModifiers(KModifier.PUBLIC) + .build(), + ) + .build() + + assertThat(toString(type)).isEqualTo( + """ + |package com.squareup.tacos + | + |import com.squareup.wire.Message + |import kotlin.Unit + | + |public object MyObject : Message() { + | init { + | } + | + | public fun test(): Unit { + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun companionObject() { + val companion = TypeSpec.companionObjectBuilder() + .addProperty( + PropertySpec.builder("tacos", Int::class) + .initializer("%L", 42) + .build(), + ) + .addFunction( + FunSpec.builder("test") + .addModifiers(KModifier.PUBLIC) + .build(), + ) + .build() + + val type = TypeSpec.classBuilder("MyClass") + .addModifiers(KModifier.PUBLIC) + .addType(companion) + .build() + + assertThat(toString(type)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Int + |import kotlin.Unit + | + |public class MyClass { + | public companion object { + | public val tacos: Int = 42 + | + | public fun test(): Unit { + | } + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun companionObjectWithInitializer() { + val companion = TypeSpec.companionObjectBuilder() + .addProperty( + PropertySpec.builder("tacos", Int::class) + .mutable() + .initializer("%L", 24) + .build(), + ) + .addInitializerBlock( + CodeBlock.builder() + .addStatement("tacos = %L", 42) + .build(), + ) + .build() + + val type = TypeSpec.classBuilder("MyClass") + .addModifiers(KModifier.PUBLIC) + .addType(companion) + .build() + + assertThat(toString(type)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Int + | + |public class MyClass { + | public companion object { + | public var tacos: Int = 24 + | + | init { + | tacos = 42 + | } + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun companionObjectWithName() { + val companion = TypeSpec.companionObjectBuilder("Factory") + .addFunction(FunSpec.builder("tacos").build()) + .build() + + val type = TypeSpec.classBuilder("MyClass") + .addType(companion) + .build() + + assertThat(toString(type)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + | + |public class MyClass { + | public companion object Factory { + | public fun tacos(): Unit { + | } + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun companionObjectOnInterface() { + val companion = TypeSpec.companionObjectBuilder() + .addFunction( + FunSpec.builder("test") + .addModifiers(KModifier.PUBLIC) + .build(), + ) + .build() + + val type = TypeSpec.interfaceBuilder("MyInterface") + .addModifiers(KModifier.PUBLIC) + .addType(companion) + .build() + + assertThat(toString(type)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + | + |public interface MyInterface { + | public companion object { + | public fun test(): Unit { + | } + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun companionObjectOnEnum() { + val companion = TypeSpec.companionObjectBuilder() + .addFunction( + FunSpec.builder("test") + .addModifiers(KModifier.PUBLIC) + .build(), + ) + .build() + + val enumBuilder = TypeSpec.enumBuilder("MyEnum") + .addEnumConstant("FOO") + .addEnumConstant("BAR") + .addModifiers(KModifier.PUBLIC) + .addType(companion) + .build() + + assertThat(toString(enumBuilder)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + | + |public enum class MyEnum { + | FOO, + | BAR, + | ; + | + | public companion object { + | public fun test(): Unit { + | } + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun companionObjectOnObjectNotAllowed() { + val companion = TypeSpec.companionObjectBuilder() + .addFunction( + FunSpec.builder("test") + .addModifiers(KModifier.PUBLIC) + .build(), + ) + .build() + + val objectBuilder = TypeSpec.objectBuilder("MyObject") + .addModifiers(KModifier.PUBLIC) + .addType(companion) + + assertThrows<IllegalArgumentException> { + objectBuilder.build() + } + } + + @Test fun companionObjectSuper() { + val superclass = ClassName("com.squareup.wire", "Message") + val companion = TypeSpec.companionObjectBuilder() + .superclass(superclass) + .addFunction( + FunSpec.builder("test") + .addModifiers(KModifier.PUBLIC) + .build(), + ) + .build() + + val type = TypeSpec.classBuilder("MyClass") + .addModifiers(KModifier.PUBLIC) + .addType(companion) + .build() + + assertThat(toString(type)).isEqualTo( + """ + |package com.squareup.tacos + | + |import com.squareup.wire.Message + |import kotlin.Unit + | + |public class MyClass { + | public companion object : Message() { + | public fun test(): Unit { + | } + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun propertyInPrimaryConstructor() { + val type = TypeSpec.classBuilder("Taco") + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("a", Int::class) + .addParameter("b", String::class) + .build(), + ) + .addProperty( + PropertySpec.builder("a", Int::class) + .initializer("a") + .build(), + ) + .addProperty( + PropertySpec.builder("b", String::class) + .initializer("b") + .build(), + ) + .build() + + assertThat(toString(type)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Int + |import kotlin.String + | + |public class Taco( + | public val a: Int, + | public val b: String, + |) + | + """.trimMargin(), + ) + } + + @Test fun propertyWithKdocInPrimaryConstructor() { + val type = TypeSpec.classBuilder("Taco") + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("a", Int::class) + .addParameter("b", String::class) + .build(), + ) + .addProperty( + PropertySpec.builder("a", Int::class) + .initializer("a") + .addKdoc("KDoc\n") + .build(), + ) + .addProperty( + PropertySpec.builder("b", String::class) + .initializer("b") + .build(), + ) + .build() + + assertThat(toString(type)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Int + |import kotlin.String + | + |public class Taco( + | /** + | * KDoc + | */ + | public val a: Int, + | public val b: String, + |) + | + """.trimMargin(), + ) + } + + @Test fun annotatedConstructor() { + val injectAnnotation = ClassName("javax.inject", "Inject") + val taco = TypeSpec.classBuilder("Taco") + .primaryConstructor( + FunSpec.constructorBuilder() + .addAnnotation(AnnotationSpec.builder(injectAnnotation).build()) + .build(), + ) + .build() + + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import javax.inject.Inject + | + |public class Taco @Inject constructor() + | + """.trimMargin(), + ) + } + + @Test fun internalConstructor() { + val taco = TypeSpec.classBuilder("Taco") + .primaryConstructor( + FunSpec.constructorBuilder() + .addModifiers(INTERNAL) + .build(), + ) + .build() + + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |public class Taco internal constructor() + | + """.trimMargin(), + ) + } + + @Test fun annotatedInternalConstructor() { + val injectAnnotation = ClassName("javax.inject", "Inject") + val taco = TypeSpec.classBuilder("Taco") + .primaryConstructor( + FunSpec.constructorBuilder() + .addAnnotation(AnnotationSpec.builder(injectAnnotation).build()) + .addModifiers(INTERNAL) + .build(), + ) + .build() + + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import javax.inject.Inject + | + |public class Taco @Inject internal constructor() + | + """.trimMargin(), + ) + } + + @Test fun multipleAnnotationsInternalConstructor() { + val injectAnnotation = ClassName("javax.inject", "Inject") + val namedAnnotation = ClassName("javax.inject", "Named") + val taco = TypeSpec.classBuilder("Taco") + .primaryConstructor( + FunSpec.constructorBuilder() + .addAnnotation(AnnotationSpec.builder(injectAnnotation).build()) + .addAnnotation(AnnotationSpec.builder(namedAnnotation).build()) + .addModifiers(INTERNAL) + .build(), + ) + .build() + + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import javax.inject.Inject + |import javax.inject.Named + | + |public class Taco @Inject @Named internal constructor() + | + """.trimMargin(), + ) + } + + @Test fun importNonNullableProperty() { + val type = String::class.asTypeName() + val taco = TypeSpec.classBuilder("Taco") + .addProperty( + PropertySpec.builder("taco", type.copy(nullable = false)) + .initializer("%S", "taco") + .build(), + ) + .addProperty( + PropertySpec.builder("nullTaco", type.copy(nullable = true)) + .initializer("null") + .build(), + ) + .build() + + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + | + |public class Taco { + | public val taco: String = "taco" + | + | public val nullTaco: String? = null + |} + | + """.trimMargin(), + ) + } + + @Test fun superclassConstructorParams() { + val taco = TypeSpec.classBuilder("Foo") + .superclass(ClassName(tacosPackage, "Bar")) + .addSuperclassConstructorParameter("%S", "foo") + .addSuperclassConstructorParameter(CodeBlock.of("%L", 42)) + .build() + + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |public class Foo : Bar("foo", 42) + | + """.trimMargin(), + ) + } + + @Test fun superclassConstructorParamsForbiddenForAnnotation() { + assertThrows<IllegalStateException> { + TypeSpec.annotationBuilder("Taco") + .addSuperclassConstructorParameter("%S", "foo") + } + } + + @Test fun classExtendsNoPrimaryConstructor() { + val typeSpec = TypeSpec.classBuilder("IoException") + .superclass(Exception::class) + .addFunction(FunSpec.constructorBuilder().build()) + .build() + + assertThat(toString(typeSpec)).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.lang.Exception + | + |public class IoException : Exception { + | public constructor() + |} + | + """.trimMargin(), + ) + } + + @Test fun classExtendsNoPrimaryOrSecondaryConstructor() { + val typeSpec = TypeSpec.classBuilder("IoException") + .superclass(Exception::class) + .build() + + assertThat(toString(typeSpec)).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.lang.Exception + | + |public class IoException : Exception() + | + """.trimMargin(), + ) + } + + @Test fun classExtendsNoPrimaryConstructorButSuperclassParams() { + assertThrows<IllegalArgumentException> { + TypeSpec.classBuilder("IoException") + .superclass(Exception::class) + .addSuperclassConstructorParameter("%S", "hey") + .addFunction(FunSpec.constructorBuilder().build()) + .build() + }.hasMessageThat().isEqualTo( + "types without a primary constructor cannot specify secondary constructors and superclass constructor parameters", + ) + } + + @Test fun constructorWithDefaultParamValue() { + val type = TypeSpec.classBuilder("Taco") + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter( + ParameterSpec.builder("a", Int::class) + .defaultValue("1") + .build(), + ) + .addParameter( + ParameterSpec + .builder("b", String::class.asTypeName().copy(nullable = true)) + .defaultValue("null") + .build(), + ) + .build(), + ) + .addProperty( + PropertySpec.builder("a", Int::class) + .initializer("a") + .build(), + ) + .addProperty( + PropertySpec.builder("b", String::class.asTypeName().copy(nullable = true)) + .initializer("b") + .build(), + ) + .build() + + assertThat(toString(type)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Int + |import kotlin.String + | + |public class Taco( + | public val a: Int = 1, + | public val b: String? = null, + |) + | + """.trimMargin(), + ) + } + + @Test fun constructorDelegation() { + val type = TypeSpec.classBuilder("Taco") + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("a", String::class.asTypeName().copy(nullable = true)) + .addParameter("b", String::class.asTypeName().copy(nullable = true)) + .addParameter("c", String::class.asTypeName().copy(nullable = true)) + .build(), + ) + .addProperty( + PropertySpec.builder("a", String::class.asTypeName().copy(nullable = true)) + .initializer("a") + .build(), + ) + .addProperty( + PropertySpec.builder("b", String::class.asTypeName().copy(nullable = true)) + .initializer("b") + .build(), + ) + .addProperty( + PropertySpec.builder("c", String::class.asTypeName().copy(nullable = true)) + .initializer("c") + .build(), + ) + .addFunction( + FunSpec.constructorBuilder() + .addParameter("map", Map::class.parameterizedBy(String::class, String::class)) + .callThisConstructor( + CodeBlock.of("map[%S]", "a"), + CodeBlock.of("map[%S]", "b"), + CodeBlock.of("map[%S]", "c"), + ) + .build(), + ) + .build() + + assertThat(toString(type)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.collections.Map + | + |public class Taco( + | public val a: String?, + | public val b: String?, + | public val c: String?, + |) { + | public constructor(map: Map<String, String>) : this(map["a"], map["b"], map["c"]) + |} + | + """.trimMargin(), + ) + } + + @Test fun internalFunForbiddenInInterface() { + val type = TypeSpec.interfaceBuilder("ITaco") + + assertThrows<IllegalArgumentException> { + type.addFunction( + FunSpec.builder("eat") + .addModifiers(ABSTRACT, INTERNAL) + .build(), + ) + .build() + }.hasMessageThat().isEqualTo("modifiers [ABSTRACT, INTERNAL] must contain none of [INTERNAL, PROTECTED]") + + assertThrows<IllegalArgumentException> { + type.addFunctions( + listOf( + FunSpec.builder("eat") + .addModifiers(ABSTRACT, INTERNAL) + .build(), + ), + ) + .build() + }.hasMessageThat().isEqualTo("modifiers [ABSTRACT, INTERNAL] must contain none of [INTERNAL, PROTECTED]") + } + + @Test fun privateAbstractFunForbiddenInInterface() { + val type = TypeSpec.interfaceBuilder("ITaco") + + assertThrows<IllegalArgumentException> { + type.addFunction( + FunSpec.builder("eat") + .addModifiers(ABSTRACT, PRIVATE) + .build(), + ) + .build() + }.hasMessageThat().isEqualTo("modifiers [ABSTRACT, PRIVATE] must contain none or only one of [ABSTRACT, PRIVATE]") + + assertThrows<IllegalArgumentException> { + type.addFunctions( + listOf( + FunSpec.builder("eat") + .addModifiers(ABSTRACT, PRIVATE) + .build(), + ), + ) + .build() + }.hasMessageThat().isEqualTo("modifiers [ABSTRACT, PRIVATE] must contain none or only one of [ABSTRACT, PRIVATE]") + } + + @Test fun internalFunForbiddenInAnnotation() { + val type = TypeSpec.annotationBuilder("Taco") + + assertThrows<IllegalArgumentException> { + type.addFunction( + FunSpec.builder("eat") + .addModifiers(INTERNAL) + .build(), + ) + .build() + }.hasMessageThat().isEqualTo("annotation class Taco.eat requires modifiers [PUBLIC, ABSTRACT]") + + assertThrows<IllegalArgumentException> { + type.addFunctions( + listOf( + FunSpec.builder("eat") + .addModifiers(INTERNAL) + .build(), + ), + ) + .build() + }.hasMessageThat().isEqualTo("annotation class Taco.eat requires modifiers [PUBLIC, ABSTRACT]") + } + + @Test fun classHeaderFormatting() { + val typeSpec = TypeSpec.classBuilder("Person") + .addModifiers(DATA) + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("id", Int::class) + .addParameter("name", String::class) + .addParameter("surname", String::class) + .build(), + ) + .addProperty( + PropertySpec.builder("id", Int::class, KModifier.OVERRIDE) + .initializer("id") + .build(), + ) + .addProperty( + PropertySpec.builder("name", String::class, KModifier.OVERRIDE) + .initializer("name") + .build(), + ) + .addProperty( + PropertySpec.builder("surname", String::class, KModifier.OVERRIDE) + .initializer("surname") + .build(), + ) + .build() + + assertThat(toString(typeSpec)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Int + |import kotlin.String + | + |public data class Person( + | public override val id: Int, + | public override val name: String, + | public override val surname: String, + |) + | + """.trimMargin(), + ) + } + + @Test + fun classHeaderAnnotations() { + val idParameterSpec = ParameterSpec.builder("id", Int::class) + .addAnnotation(ClassName("com.squareup.kotlinpoet", "Id")) + .defaultValue("1") + .build() + + val typeSpec = TypeSpec.classBuilder("Person") + .addModifiers(DATA) + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter(idParameterSpec) + .build(), + ) + .addProperty( + PropertySpec.builder("id", Int::class) + .addModifiers(PRIVATE) + .initializer("id") + .addAnnotation(ClassName("com.squareup.kotlinpoet", "OrderBy")) + .build(), + ) + .build() + + assertThat(toString(typeSpec)).isEqualTo( + """ + |package com.squareup.tacos + | + |import com.squareup.kotlinpoet.Id + |import com.squareup.kotlinpoet.OrderBy + |import kotlin.Int + | + |public data class Person( + | @OrderBy + | @Id + | private val id: Int = 1, + |) + | + """.trimMargin(), + ) + } + + @Test fun literalPropertySpec() { + val taco = TypeSpec.classBuilder("Taco") + .addFunction( + FunSpec.builder("shell") + .addCode( + CodeBlock.of( + "%L", + PropertySpec.builder("taco1", String::class.asTypeName()) + .initializer("%S", "Taco!").build(), + ), + ) + .addCode( + CodeBlock.of( + "%L", + PropertySpec.builder("taco2", String::class.asTypeName().copy(nullable = true)) + .initializer("null") + .build(), + ), + ) + .addCode( + CodeBlock.of( + "%L", + PropertySpec.builder("taco3", String::class.asTypeName(), KModifier.LATEINIT) + .mutable() + .build(), + ), + ) + .build(), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.Unit + | + |public class Taco { + | public fun shell(): Unit { + | val taco1: String = "Taco!" + | val taco2: String? = null + | lateinit var taco3: String + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun basicDelegateTest() { + val type = TypeSpec.classBuilder("Guac") + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("somethingElse", String::class) + .build(), + ) + .addSuperinterface( + Consumer::class.parameterizedBy(String::class), + CodeBlock.of("({ println(it) })"), + ) + .build() + + val expect = """ + |package com.squareup.tacos + | + |import java.util.function.Consumer + |import kotlin.String + | + |public class Guac( + | somethingElse: String, + |) : Consumer<String> by ({ println(it) }) + | + """.trimMargin() + + assertThat(toString(type)).isEqualTo(expect) + } + + @Test fun testDelegateOnObject() { + val type = TypeSpec.objectBuilder("Guac") + .addSuperinterface( + Consumer::class.parameterizedBy(String::class), + CodeBlock.of("({ println(it) })"), + ) + .build() + + val expect = """ + |package com.squareup.tacos + | + |import java.util.function.Consumer + |import kotlin.String + | + |public object Guac : Consumer<String> by ({ println(it) }) + | + """.trimMargin() + + assertThat(toString(type)).isEqualTo(expect) + } + + @Test fun testMultipleDelegates() { + val type = TypeSpec.classBuilder("StringToInteger") + .primaryConstructor( + FunSpec.constructorBuilder() + .build(), + ) + .addSuperinterface( + Function::class.parameterizedBy(String::class, Int::class), + CodeBlock.of("Function ({ text -> text.toIntOrNull() ?: 0 })"), + ) + .addSuperinterface( + Runnable::class, + CodeBlock.of("Runnable ({ %T.debug(\"Hello world\") })", Logger::class.asTypeName()), + ) + .build() + + val expect = """ + |package com.squareup.tacos + | + |import java.lang.Runnable + |import java.util.logging.Logger + |import kotlin.Function + |import kotlin.Int + |import kotlin.String + | + |public class StringToInteger() : Function<String, Int> by Function ({ text -> text.toIntOrNull() ?: + | 0 }), Runnable by Runnable ({ Logger.debug("Hello world") }) + | + """.trimMargin() + + assertThat(toString(type)).isEqualTo(expect) + } + + @Test fun testNoSuchParameterDelegate() { + assertThrows<IllegalArgumentException> { + TypeSpec.classBuilder("Taco") + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("other", String::class) + .build(), + ) + .addSuperinterface(KFunction::class, "notOther") + .build() + }.hasMessageThat().isEqualTo("no such constructor parameter 'notOther' to delegate to for type 'Taco'") + } + + @Test fun failAddParamDelegateWhenNullConstructor() { + assertThrows<IllegalArgumentException> { + TypeSpec.classBuilder("Taco") + .addSuperinterface(Runnable::class, "etc") + .build() + }.hasMessageThat().isEqualTo("delegating to constructor parameter requires not-null constructor") + } + + @Test fun testAddedDelegateByParamName() { + val type = TypeSpec.classBuilder("Taco") + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("superString", Function::class) + .build(), + ) + .addSuperinterface(Function::class, "superString") + .build() + + assertThat(toString(type)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Function + | + |public class Taco( + | superString: Function, + |) : Function by superString + | + """.trimMargin(), + ) + } + + @Test fun failOnAddExistingDelegateType() { + assertThrows<IllegalArgumentException> { + TypeSpec.classBuilder("Taco") + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("superString", Function::class) + .build(), + ) + .addSuperinterface(Function::class, CodeBlock.of("{ print(Hello) }")) + .addSuperinterface(Function::class, "superString") + .build() + fail() + }.hasMessageThat().isEqualTo( + "'Taco' can not delegate to kotlin.Function " + + "by superString with existing declaration by { print(Hello) }", + ) + } + + @Test fun testDelegateIfaceWithOtherParamTypeName() { + val entity = ClassName(tacosPackage, "Entity") + val entityBuilder = ClassName(tacosPackage, "EntityBuilder") + val type = TypeSpec.classBuilder("EntityBuilder") + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter( + ParameterSpec.builder( + "argBuilder", + ClassName(tacosPackage, "Payload") + .parameterizedBy(entityBuilder, entity), + ) + .defaultValue("Payload.create()") + .build(), + ) + .build(), + ) + .addSuperinterface( + ClassName(tacosPackage, "TypeBuilder") + .parameterizedBy(entityBuilder, entity), + "argBuilder", + ) + .build() + + assertThat(toString(type)).isEqualTo( + """ + |package com.squareup.tacos + | + |public class EntityBuilder( + | argBuilder: Payload<EntityBuilder, Entity> = Payload.create(), + |) : TypeBuilder<EntityBuilder, Entity> by argBuilder + | + """.trimMargin(), + ) + } + + @Test fun externalClassFunctionHasNoBody() { + val typeSpec = TypeSpec.classBuilder("Foo") + .addModifiers(KModifier.EXTERNAL) + .addFunction(FunSpec.builder("bar").addModifiers(KModifier.EXTERNAL).build()) + .build() + + assertThat(toString(typeSpec)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + | + |public external class Foo { + | public fun bar(): Unit + |} + | + """.trimMargin(), + ) + } + + @Test fun externalInterfaceWithMembers() { + val typeSpec = TypeSpec.interfaceBuilder("Foo") + .addModifiers(KModifier.EXTERNAL) + .addProperty(PropertySpec.builder("baz", String::class).addModifiers(KModifier.EXTERNAL).build()) + .addFunction(FunSpec.builder("bar").addModifiers(KModifier.EXTERNAL).build()) + .build() + + assertThat(toString(typeSpec)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.Unit + | + |public external interface Foo { + | public val baz: String + | + | public fun bar(): Unit + |} + | + """.trimMargin(), + ) + } + + @Test fun externalObjectWithMembers() { + val typeSpec = TypeSpec.objectBuilder("Foo") + .addModifiers(KModifier.EXTERNAL) + .addProperty(PropertySpec.builder("baz", String::class).addModifiers(KModifier.EXTERNAL).build()) + .addFunction(FunSpec.builder("bar").addModifiers(KModifier.EXTERNAL).build()) + .build() + + assertThat(toString(typeSpec)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.Unit + | + |public external object Foo { + | public val baz: String + | + | public fun bar(): Unit + |} + | + """.trimMargin(), + ) + } + + @Test fun externalClassWithNestedTypes() { + val typeSpec = TypeSpec.classBuilder("Foo") + .addModifiers(KModifier.EXTERNAL) + .addType( + TypeSpec.classBuilder("Nested1") + .addModifiers(KModifier.EXTERNAL) + .addType( + TypeSpec.objectBuilder("Nested2") + .addModifiers(KModifier.EXTERNAL) + .addFunction(FunSpec.builder("bar").addModifiers(KModifier.EXTERNAL).build()) + .build(), + ) + .addFunction(FunSpec.builder("baz").addModifiers(KModifier.EXTERNAL).build()) + .build(), + ) + .addType( + TypeSpec.companionObjectBuilder() + .addModifiers(KModifier.EXTERNAL) + .addFunction(FunSpec.builder("qux").addModifiers(KModifier.EXTERNAL).build()) + .build(), + ) + .build() + + assertThat(toString(typeSpec)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + | + |public external class Foo { + | public class Nested1 { + | public fun baz(): Unit + | + | public object Nested2 { + | public fun bar(): Unit + | } + | } + | + | public companion object { + | public fun qux(): Unit + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun isEnum() { + val enum = TypeSpec.enumBuilder("Topping") + .addEnumConstant("CHEESE") + .build() + assertThat(enum.isEnum).isTrue() + } + + @Test fun isAnnotation() { + val annotation = TypeSpec.annotationBuilder("Taco") + .build() + assertThat(annotation.isAnnotation).isTrue() + } + + @Test fun escapePunctuationInTypeName() { + assertThat(TypeSpec.classBuilder("With-Hyphen").build().toString()).isEqualTo( + """ + |public class `With-Hyphen` + | + """.trimMargin(), + ) + } + + @Test fun multipleCompanionObjects() { + assertThrows<IllegalArgumentException> { + TypeSpec.classBuilder("Taco") + .addTypes( + listOf( + TypeSpec.companionObjectBuilder() + .build(), + TypeSpec.companionObjectBuilder() + .build(), + ), + ) + .build() + } + } + + @Test fun objectKindIsCompanion() { + val companionObject = TypeSpec.companionObjectBuilder() + .build() + assertThat(companionObject.isCompanion).isTrue() + } + + @Test fun typeNamesCollision() { + val sqlTaco = ClassName("java.sql", "Taco") + val source = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.classBuilder("Taco") + .addModifiers(DATA) + .addProperty( + PropertySpec.builder("madeFreshDatabaseDate", sqlTaco) + .initializer("madeFreshDatabaseDate") + .build(), + ) + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("madeFreshDatabaseDate", sqlTaco) + .addParameter("fooNt", INT) + .build(), + ) + .addFunction( + FunSpec.constructorBuilder() + .addParameter("anotherTaco", ClassName("com.squareup.tacos", "Taco")) + .callThisConstructor(CodeBlock.of("%T.defaultInstance(), 0", sqlTaco)) + .build(), + ) + .build(), + ) + .build() + assertThat(source.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Int + | + |public data class Taco( + | public val madeFreshDatabaseDate: java.sql.Taco, + | fooNt: Int, + |) { + | public constructor(anotherTaco: Taco) : this(java.sql.Taco.defaultInstance(), 0) + |} + | + """.trimMargin(), + ) + } + + @Test fun modifyAnnotations() { + val builder = TypeSpec.classBuilder("Taco") + .addAnnotation( + AnnotationSpec.builder(JvmName::class.asClassName()) + .addMember("name = %S", "jvmWord") + .build(), + ) + + val javaWord = AnnotationSpec.builder(JvmName::class.asClassName()) + .addMember("name = %S", "javaWord") + .build() + builder.annotationSpecs.clear() + builder.annotationSpecs.add(javaWord) + + assertThat(builder.build().annotationSpecs).containsExactly(javaWord) + } + + @Test fun modifyTypeVariableNames() { + val builder = TypeSpec.classBuilder("Taco") + .addTypeVariable(TypeVariableName("V")) + + val tVar = TypeVariableName("T") + builder.typeVariables.clear() + builder.typeVariables.add(tVar) + + assertThat(builder.build().typeVariables).containsExactly(tVar) + } + + @Test fun modifyFunctions() { + val builder = TypeSpec.classBuilder("Taco") + .addFunction(FunSpec.builder("topping").build()) + + val seasoning = FunSpec.builder("seasoning").build() + builder.funSpecs.clear() + builder.funSpecs.add(seasoning) + + assertThat(builder.build().funSpecs).containsExactly(seasoning) + } + + @Test fun modifyTypeSpecs() { + val builder = TypeSpec.classBuilder("Taco") + .addType(TypeSpec.classBuilder("Topping").build()) + + val seasoning = TypeSpec.classBuilder("Seasoning").build() + builder.typeSpecs.clear() + builder.typeSpecs.add(seasoning) + + assertThat(builder.build().typeSpecs).containsExactly(seasoning) + } + + @Test fun modifySuperinterfaces() { + val builder = TypeSpec.classBuilder("Taco") + .addSuperinterface(List::class) + + builder.superinterfaces.clear() + builder.superinterfaces[Set::class.asTypeName()] = CodeBlock.EMPTY + + assertThat(builder.build().superinterfaces) + .containsExactlyEntriesIn(mapOf(Set::class.asTypeName() to CodeBlock.EMPTY)) + } + + @Test fun modifyProperties() { + val builder = TypeSpec.classBuilder("Taco") + .addProperty(PropertySpec.builder("topping", String::class.asClassName()).build()) + + val seasoning = PropertySpec.builder("seasoning", String::class.asClassName()).build() + builder.propertySpecs.clear() + builder.propertySpecs.add(seasoning) + + assertThat(builder.build().propertySpecs).containsExactly(seasoning) + } + + @Test fun modifyEnumConstants() { + val builder = TypeSpec.enumBuilder("Taco") + .addEnumConstant("TOPPING") + + builder.enumConstants.clear() + builder.enumConstants["SEASONING"] = TypeSpec.anonymousClassBuilder().build() + + assertThat(builder.build().enumConstants) + .containsExactlyEntriesIn(mapOf("SEASONING" to TypeSpec.anonymousClassBuilder().build())) + } + + @Test fun modifySuperclassConstructorParams() { + val builder = TypeSpec.classBuilder("Taco") + .addSuperclassConstructorParameter(CodeBlock.of("seasoning = %S", "mild")) + + val seasoning = CodeBlock.of("seasoning = %S", "spicy") + builder.superclassConstructorParameters.clear() + builder.superclassConstructorParameters.add(seasoning) + + assertThat(builder.build().superclassConstructorParameters).containsExactly(seasoning) + } + + // https://github.com/square/kotlinpoet/issues/565 + @Test fun markerEnum() { + val spec = TypeSpec.enumBuilder("Topping") + .build() + assertThat(spec.toString()).isEqualTo( + """ + |public enum class Topping + | + """.trimMargin(), + ) + } + + // https://github.com/square/kotlinpoet/issues/586 + @Test fun classKdocWithoutTags() { + val typeSpec = TypeSpec.classBuilder("Foo") + .addKdoc("blah blah") + .build() + assertThat(typeSpec.toString()).isEqualTo( + """ + |/** + | * blah blah + | */ + |public class Foo + | + """.trimMargin(), + ) + } + + @Test fun classWithPropertyKdoc() { + val typeSpec = TypeSpec.classBuilder("Foo") + .addProperty( + PropertySpec.builder("bar", String::class) + .addKdoc("The bar for your foo") + .build(), + ) + .build() + assertThat(typeSpec.toString()).isEqualTo( + """ + |public class Foo { + | /** + | * The bar for your foo + | */ + | public val bar: kotlin.String + |} + | + """.trimMargin(), + ) + } + + // https://github.com/square/kotlinpoet/issues/563 + @Test fun kdocFormatting() { + val typeSpec = TypeSpec.classBuilder("MyType") + .addKdoc("This is a thing for stuff.") + .addProperty( + PropertySpec.builder("first", INT) + .initializer("first") + .build(), + ) + .addProperty( + PropertySpec.builder("second", INT) + .initializer("second") + .build(), + ) + .addProperty( + PropertySpec.builder("third", INT) + .initializer("third") + .build(), + ) + .primaryConstructor( + FunSpec.constructorBuilder() + .addKdoc("Construct a thing!") + .addParameter( + ParameterSpec.builder("first", INT) + .addKdoc("the first thing") + .build(), + ) + .addParameter( + ParameterSpec.builder("second", INT) + .addKdoc("the second thing") + .build(), + ) + .addParameter( + ParameterSpec.builder("third", INT) + .addKdoc("the third thing") + .build(), + ) + .build(), + ) + .build() + assertThat(typeSpec.toString()).isEqualTo( + """ + |/** + | * This is a thing for stuff. + | */ + |public class MyType( + | /** + | * the first thing + | */ + | public val first: kotlin.Int, + | /** + | * the second thing + | */ + | public val second: kotlin.Int, + | /** + | * the third thing + | */ + | public val third: kotlin.Int, + |) + | + """.trimMargin(), + ) + } + + @Test fun primaryConstructorWithOneParameterKdocFormatting() { + val typeSpec = TypeSpec.classBuilder("MyType") + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter( + ParameterSpec.builder("first", INT) + .addKdoc("the first thing") + .build(), + ) + .build(), + ) + .build() + assertThat(typeSpec.toString()).isEqualTo( + """ + |public class MyType( + | /** + | * the first thing + | */ + | first: kotlin.Int, + |) + | + """.trimMargin(), + ) + } + + // https://github.com/square/kotlinpoet/issues/594 + @Test fun longComment() { + val taco = TypeSpec.classBuilder("Taco") + .addFunction( + FunSpec.builder("getAnswer") + .returns(Int::class) + .addComment( + "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do " + + "eiusmod tempor incididunt ut labore et dolore magna aliqua.", + ) + .addStatement("return 42") + .build(), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Int + | + |public class Taco { + | public fun getAnswer(): Int { + | // Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. + | return 42 + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun originatingElementsIncludesThoseOfNestedTypes() { + val outerElement = FakeElement() + val innerElement = FakeElement() + val outer = TypeSpec.classBuilder("Outer") + .addOriginatingElement(outerElement) + .addType( + TypeSpec.classBuilder("Inner") + .addOriginatingElement(innerElement) + .build(), + ) + .build() + assertThat(outer.originatingElements).containsExactly(outerElement, innerElement) + } + + // https://github.com/square/kotlinpoet/issues/698 + @Test fun escapeEnumConstants() { + val enum = TypeSpec.enumBuilder("MyEnum") + .addEnumConstant("test test") + .addEnumConstant("0constants") + .build() + assertThat(enum.toString()).isEqualTo( + """ + |public enum class MyEnum { + | `test test`, + | `0constants`, + |} + | + """.trimMargin(), + ) + } + + @Test fun initOrdering_first() { + val type = TypeSpec.classBuilder("MyClass") + .addInitializerBlock(CodeBlock.builder().build()) + .addProperty("tacos", Int::class) + .build() + + //language=kotlin + assertThat(toString(type)).isEqualTo( + """ + package com.squareup.tacos + + import kotlin.Int + + public class MyClass { + init { + } + + public val tacos: Int + } + + """.trimIndent(), + ) + } + + @Test fun initOrdering_middle() { + val type = TypeSpec.classBuilder("MyClass") + .addProperty("tacos1", Int::class) + .addInitializerBlock(CodeBlock.builder().build()) + .addProperty("tacos2", Int::class) + .build() + + //language=kotlin + assertThat(toString(type)).isEqualTo( + """ + package com.squareup.tacos + + import kotlin.Int + + public class MyClass { + public val tacos1: Int + + init { + } + + public val tacos2: Int + } + + """.trimIndent(), + ) + } + + @Test fun initOrdering_last() { + val type = TypeSpec.classBuilder("MyClass") + .addProperty("tacos", Int::class) + .addInitializerBlock(CodeBlock.builder().build()) + .build() + + //language=kotlin + assertThat(toString(type)).isEqualTo( + """ + package com.squareup.tacos + + import kotlin.Int + + public class MyClass { + public val tacos: Int + + init { + } + } + + """.trimIndent(), + ) + } + + @Test fun initOrdering_constructorParamsExludedAfterIndex() { + val type = TypeSpec.classBuilder("MyClass") + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("tacos1", Int::class) + .addParameter("tacos2", Int::class) + .build(), + ) + .addProperty( + PropertySpec.builder("tacos1", Int::class) + .initializer("tacos1") + .build(), + ) + .addInitializerBlock(CodeBlock.builder().build()) + .addProperty( + PropertySpec.builder("tacos2", Int::class) + .initializer("tacos2") + .build(), + ) + .build() + + //language=kotlin + assertThat(toString(type)).isEqualTo( + """ + package com.squareup.tacos + + import kotlin.Int + + public class MyClass( + public val tacos1: Int, + tacos2: Int, + ) { + init { + } + + public val tacos2: Int = tacos2 + } + + """.trimIndent(), + ) + } + + // https://github.com/square/kotlinpoet/issues/843 + @Test fun kdocWithParametersWithoutClassKdoc() { + val taco = TypeSpec.classBuilder("Taco") + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter( + ParameterSpec.builder("mild", Boolean::class) + .addKdoc(CodeBlock.of("%L", "Whether the taco is mild (ew) or crunchy (ye).\n")) + .build(), + ) + .build(), + ) + .addProperty( + PropertySpec.builder("mild", Boolean::class) + .addKdoc("No one likes mild tacos.") + .initializer("mild") + .build(), + ) + .build() + assertThat(toString(taco)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Boolean + | + |/** + | * @param mild Whether the taco is mild (ew) or crunchy (ye). + | */ + |public class Taco( + | /** + | * No one likes mild tacos. + | */ + | public val mild: Boolean, + |) + | + """.trimMargin(), + ) + } + + // https://github.com/square/kotlinpoet/issues/848 + @Test fun escapeEnumConstantNames() { + val enum = TypeSpec + .enumBuilder("MyEnum") + .addEnumConstant("object") + .build() + assertThat(toString(enum)).isEqualTo( + """ + |package com.squareup.tacos + | + |public enum class MyEnum { + | `object`, + |} + | + """.trimMargin(), + ) + } + + // https://youtrack.jetbrains.com/issue/KT-52315 + @Test fun escapeHeaderAndImplAsEnumConstantNames() { + val primaryConstructor = FunSpec.constructorBuilder() + .addParameter("int", Int::class) + .build() + val enum = TypeSpec + .enumBuilder("MyEnum") + .primaryConstructor(primaryConstructor) + .addEnumConstant( + "header", + TypeSpec.anonymousClassBuilder() + .addSuperclassConstructorParameter("%L", 1) + .build(), + ) + .addEnumConstant( + "impl", + TypeSpec.anonymousClassBuilder() + .addSuperclassConstructorParameter("%L", 2) + .build(), + ) + .build() + assertThat(toString(enum)).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Int + | + |public enum class MyEnum( + | int: Int, + |) { + | `header`(1), + | `impl`(2), + |} + | + """.trimMargin(), + ) + } + + @Test fun escapeClassNames() { + val type = TypeSpec.classBuilder("fun").build() + assertThat(type.toString()).isEqualTo( + """ + |public class `fun` + | + """.trimMargin(), + ) + } + + @Test fun escapeInnerClassName() { + val tacoType = ClassName("com.squareup.tacos", "Taco", "object") + val funSpec = FunSpec.builder("printTaco") + .addParameter("taco", tacoType) + .addStatement("print(taco)") + .build() + assertThat(funSpec.toString()).isEqualTo( + """ + |public fun printTaco(taco: com.squareup.tacos.Taco.`object`): kotlin.Unit { + | print(taco) + |} + | + """.trimMargin(), + ) + } + + @Test fun escapeAllowedCharacters() { + val typeSpec = TypeSpec.classBuilder("A\$B") + .build() + assertThat(typeSpec.toString()).isEqualTo("public class `A\$B`\n") + } + + // https://github.com/square/kotlinpoet/issues/1011 + @Test fun abstractInterfaceMembers() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.interfaceBuilder("Taco") + .addProperty("foo", String::class, ABSTRACT) + .addProperty( + PropertySpec.builder("fooWithDefault", String::class) + .initializer("%S", "defaultValue") + .build(), + ) + .addFunction( + FunSpec.builder("bar") + .addModifiers(ABSTRACT) + .returns(String::class) + .build(), + ) + .addFunction( + FunSpec.builder("barWithDefault") + .build(), + ) + .build(), + ) + .build() + // language=kotlin + assertThat(file.toString()).isEqualTo( + """ + package com.squareup.tacos + + import kotlin.String + import kotlin.Unit + + public interface Taco { + public val foo: String + + public val fooWithDefault: String = "defaultValue" + + public fun bar(): String + + public fun barWithDefault(): Unit { + } + } + + """.trimIndent(), + ) + } + + @Test fun emptyConstructorGenerated() { + val taco = TypeSpec.classBuilder("Taco") + .primaryConstructor(FunSpec.constructorBuilder().build()) + .build() + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addType(taco) + .build() + assertThat(file.toString()).isEqualTo( + """ + package com.squareup.tacos + + public class Taco() + + """.trimIndent(), + ) + } + + // Regression test for https://github.com/square/kotlinpoet/issues/1176 + @Test fun `templates in class delegation blocks should be imported too`() { + val taco = TypeSpec.classBuilder("TacoShim") + .addSuperinterface( + ClassName("test", "Taco"), + CodeBlock.of("%T", ClassName("test", "RealTaco")), + ) + .build() + val file = FileSpec.builder("com.squareup.tacos", "Tacos") + .addType(taco) + .build() + assertThat(file.toString()).isEqualTo( + """ + package com.squareup.tacos + + import test.RealTaco + import test.Taco + + public class TacoShim : Taco by RealTaco + + """.trimIndent(), + ) + } + + // https://github.com/square/kotlinpoet/issues/1183 + @Test fun `forbidden enum constant names`() { + var exception = assertFailsWith<IllegalArgumentException> { + TypeSpec.enumBuilder("Topping") + .addEnumConstant("name") + } + assertThat(exception.message).isEqualTo( + "constant with name \"name\" conflicts with a supertype member with the same name", + ) + + @Suppress("RemoveExplicitTypeArguments") + exception = assertFailsWith<IllegalArgumentException> { + TypeSpec.enumBuilder("Topping") + .addEnumConstant("ordinal") + } + assertThat(exception.message).isEqualTo( + "constant with name \"ordinal\" conflicts with a supertype member with the same name", + ) + } + + // https://github.com/square/kotlinpoet/issues/1183 + @Test fun `forbidden enum property names`() { + var exception = assertFailsWith<IllegalArgumentException> { + TypeSpec.enumBuilder("Topping") + .addProperty("name", String::class) + } + assertThat(exception.message).isEqualTo( + "name is a final supertype member and can't be redeclared or overridden", + ) + + @Suppress("RemoveExplicitTypeArguments") + exception = assertFailsWith<IllegalArgumentException> { + TypeSpec.enumBuilder("Topping") + .addProperty("ordinal", String::class) + } + assertThat(exception.message).isEqualTo( + "ordinal is a final supertype member and can't be redeclared or overridden", + ) + } + + // https://github.com/square/kotlinpoet/issues/1234 + @Test fun `enum constants are resolved`() { + val file = FileSpec.builder("com.example", "test") + .addType( + TypeSpec.enumBuilder("Foo") + .addProperty( + PropertySpec.builder("rawValue", String::class) + .initializer("%S", "") + .build(), + ) + .addEnumConstant("String") + .build(), + ) + .build() + + assertThat(file.toString()).isEqualTo( + """ + package com.example + + public enum class Foo { + String, + ; + + public val rawValue: kotlin.String = "" + } + + """.trimIndent(), + ) + } + + // https://github.com/square/kotlinpoet/issues/1035 + @Test fun dataClassWithKeywordProperty() { + val parameter = ParameterSpec.builder("data", STRING).build() + val typeSpec = TypeSpec.classBuilder("Example") + .addModifiers(DATA) + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter(parameter) + .build(), + ) + .addProperty( + PropertySpec.builder(parameter.name, STRING) + .initializer("%N", parameter) + .build(), + ) + .build() + assertThat(typeSpec.toString()).isEqualTo( + """ + public data class Example( + public val `data`: kotlin.String, + ) + + """.trimIndent(), + ) + } + + @Test fun contextReceiver() { + val typeSpec = TypeSpec.classBuilder("Example") + .contextReceivers(STRING) + .build() + + assertThat(typeSpec.toString()).isEqualTo( + """ + |context(kotlin.String) + |public class Example + | + """.trimMargin(), + ) + } + + @Test fun contextReceiver_mustBeClass() { + val t = assertFailsWith<IllegalStateException> { + TypeSpec.interfaceBuilder("Example") + .contextReceivers(STRING) + } + assertThat(t).hasMessageThat().contains("contextReceivers can only be applied on simple classes") + } + + companion object { + private const val donutsPackage = "com.squareup.donuts" + } +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/TypeVariableNameTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/TypeVariableNameTest.kt new file mode 100644 index 00000000..bca1a7cb --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/TypeVariableNameTest.kt @@ -0,0 +1,254 @@ +/* + * Copyright (C) 2017 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.truth.Truth.assertThat +import com.squareup.kotlinpoet.TypeVariableName.Companion.NULLABLE_ANY_LIST +import java.io.Serializable +import kotlin.test.Test + +class TypeVariableNameTest { + @Test fun nullableAnyIsImplicitBound() { + val typeVariableName = TypeVariableName("T") + assertThat(typeVariableName.bounds).containsExactly(NULLABLE_ANY) + } + + @Test fun oneTypeVariableNoBounds() { + val funSpec = FunSpec.builder("foo") + .addTypeVariable(TypeVariableName("T")) + .returns(TypeVariableName("T").copy(nullable = true)) + .addStatement("return null") + .build() + assertThat(funSpec.toString()).isEqualTo( + """ + |public fun <T> foo(): T? = null + | + """.trimMargin(), + ) + } + + @Test fun twoTypeVariablesNoBounds() { + val funSpec = FunSpec.builder("foo") + .addTypeVariable(TypeVariableName("T")) + .addTypeVariable(TypeVariableName("U")) + .returns(TypeVariableName("T").copy(nullable = true)) + .addStatement("return null") + .build() + assertThat(funSpec.toString()).isEqualTo( + """ + |public fun <T, U> foo(): T? = null + | + """.trimMargin(), + ) + } + + @Test fun oneTypeVariableOneBound() { + val funSpec = FunSpec.builder("foo") + .addTypeVariable(TypeVariableName("T", Serializable::class)) + .returns(TypeVariableName("T").copy(nullable = true)) + .addStatement("return null") + .build() + assertThat(funSpec.toString()).isEqualTo( + """ + |public fun <T : java.io.Serializable> foo(): T? = null + | + """.trimMargin(), + ) + } + + @Test fun twoTypeVariablesOneBoundEach() { + val funSpec = FunSpec.builder("foo") + .addTypeVariable(TypeVariableName("T", Serializable::class)) + .addTypeVariable(TypeVariableName("U", Runnable::class)) + .returns(TypeVariableName("T").copy(nullable = true)) + .addStatement("return null") + .build() + assertThat(funSpec.toString()).isEqualTo( + """ + |public fun <T : java.io.Serializable, U : java.lang.Runnable> foo(): T? = null + | + """.trimMargin(), + ) + } + + @Test fun oneTypeVariableTwoBounds() { + val funSpec = FunSpec.builder("foo") + .addTypeVariable(TypeVariableName("T", Serializable::class, Runnable::class)) + .returns(TypeVariableName("T").copy(nullable = true)) + .addStatement("return null") + .build() + assertThat(funSpec.toString()).isEqualTo( + """ + |public fun <T> foo(): T? where T : java.io.Serializable, T : java.lang.Runnable = null + | + """.trimMargin(), + ) + } + + @Test fun twoTypeVariablesTwoBoundsEach() { + val funSpec = FunSpec.builder("foo") + .addTypeVariable(TypeVariableName("T", Serializable::class, Runnable::class)) + .addTypeVariable(TypeVariableName("U", Comparator::class, Cloneable::class)) + .returns(TypeVariableName("T").copy(nullable = true)) + .addStatement("return null") + .build() + assertThat(funSpec.toString()).isEqualTo( + "public fun <T, U> foo(): " + + "T? where T : java.io.Serializable, T : java.lang.Runnable, " + + "U : java.util.Comparator, U : kotlin.Cloneable = null\n", + ) + } + + @Test fun threeTypeVariables() { + val funSpec = FunSpec.builder("foo") + .addTypeVariable(TypeVariableName("T", Serializable::class, Runnable::class)) + .addTypeVariable(TypeVariableName("U", Cloneable::class)) + .addTypeVariable(TypeVariableName("V")) + .returns(TypeVariableName("T").copy(nullable = true)) + .addStatement("return null") + .build() + assertThat(funSpec.toString()).isEqualTo( + "public fun <T, U : kotlin.Cloneable, V> foo(): " + + "T? where T : java.io.Serializable, T : java.lang.Runnable = null\n", + ) + } + + @Test fun addingBoundsRemovesImplicitBound() { + val typeSpec = TypeSpec.classBuilder("Taco") + .addTypeVariable(TypeVariableName("T").copy(bounds = listOf(Number::class.asTypeName()))) + .build() + assertThat(typeSpec.toString()).isEqualTo( + """ + |public class Taco<T : kotlin.Number> + | + """.trimMargin(), + ) + } + + @Test fun inVariance() { + val typeSpec = TypeSpec.classBuilder("Taco") + .addTypeVariable(TypeVariableName("E", Number::class, variance = KModifier.IN)) + .build() + assertThat(typeSpec.toString()).isEqualTo( + """ + |public class Taco<in E : kotlin.Number> + | + """.trimMargin(), + ) + } + + @Test fun outVariance() { + val typeSpec = TypeSpec.classBuilder("Taco") + .addTypeVariable(TypeVariableName("E", Number::class, variance = KModifier.OUT)) + .build() + assertThat(typeSpec.toString()).isEqualTo( + """ + |public class Taco<out E : kotlin.Number> + | + """.trimMargin(), + ) + } + + @Test fun invalidVariance() { + assertThrows<IllegalArgumentException> { + TypeVariableName("E", KModifier.FINAL) + } + } + + @Test fun reified() { + val funSpec = FunSpec.builder("printMembers") + .addModifiers(KModifier.INLINE) + .addTypeVariable(TypeVariableName("T").copy(reified = true)) + .addStatement("println(T::class.members)") + .build() + assertThat(funSpec.toString()).isEqualTo( + """ + |public inline fun <reified T> printMembers(): kotlin.Unit { + | println(T::class.members) + |} + | + """.trimMargin(), + ) + } + + @Test fun anyBoundsIsLegal() { + val typeSpec = TypeSpec.classBuilder("Taco") + .addTypeVariable(TypeVariableName("E", ANY)) + .build() + assertThat(typeSpec.toString()).isEqualTo( + """ + |public class Taco<E : kotlin.Any> + | + """.trimMargin(), + ) + } + + @Test fun filterOutNullableAnyBounds() { + val typeSpec = TypeSpec.classBuilder("Taco") + .addTypeVariable(TypeVariableName("E", NULLABLE_ANY)) + .build() + assertThat(typeSpec.toString()).isEqualTo( + """ + |public class Taco<E> + | + """.trimMargin(), + ) + } + + @Test fun emptyBoundsShouldDefaultToAnyNullable() { + val typeVariable = TypeVariableName("E", bounds = *emptyArray<TypeName>()) + val typeSpec = TypeSpec.classBuilder("Taco") + .addTypeVariable(typeVariable) + .build() + assertThat(typeVariable.bounds).isEqualTo(NULLABLE_ANY_LIST) + assertThat(typeSpec.toString()).isEqualTo( + """ + |public class Taco<E> + | + """.trimMargin(), + ) + } + + @Test fun noBoundsShouldDefaultToAnyNullable() { + val typeVariable = TypeVariableName("E") + val typeSpec = TypeSpec.classBuilder("Taco") + .addTypeVariable(typeVariable) + .build() + assertThat(typeVariable.bounds).isEqualTo(NULLABLE_ANY_LIST) + assertThat(typeSpec.toString()).isEqualTo( + """ + |public class Taco<E> + | + """.trimMargin(), + ) + } + + @Test fun genericClassNoBoundsShouldDefaultToAnyNullable() { + val typeVariable = TypeVariableName.get(GenericClass::class.java.typeParameters[0]) + val typeSpec = TypeSpec.classBuilder("Taco") + .addTypeVariable(typeVariable) + .build() + assertThat(typeVariable.bounds).isEqualTo(NULLABLE_ANY_LIST) + assertThat(typeSpec.toString()).isEqualTo( + """ + |public class Taco<T> + | + """.trimMargin(), + ) + } + + class GenericClass<T> +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/TypesEclipseTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/TypesEclipseTest.kt new file mode 100644 index 00000000..a3ae52b5 --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/TypesEclipseTest.kt @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2014 Google, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.base.Charsets.UTF_8 +import com.google.common.collect.ImmutableSet +import org.eclipse.jdt.internal.compiler.tool.EclipseCompiler +import org.junit.Rule +import org.junit.rules.TestRule +import org.junit.runner.Description +import org.junit.runners.JUnit4 +import org.junit.runners.model.Statement +import java.util.Locale +import java.util.concurrent.atomic.AtomicReference +import javax.annotation.processing.AbstractProcessor +import javax.annotation.processing.ProcessingEnvironment +import javax.annotation.processing.Processor +import javax.annotation.processing.RoundEnvironment +import javax.lang.model.SourceVersion +import javax.lang.model.element.TypeElement +import javax.lang.model.util.Elements +import javax.lang.model.util.Types +import javax.tools.DiagnosticCollector +import javax.tools.JavaFileObject +import kotlin.test.Ignore + +@Ignore("Not clear this test is useful to retain in the Kotlin world") +class TypesEclipseTest : AbstractTypesTest() { + /** + * A [JUnit4] [Rule] that executes tests such that a instances of [Elements] and [Types] are + * available during execution. + * + * To use this rule in a test, just add the following field: + * + * ```java + * public CompilationRule compilationRule = new CompilationRule(); + * ``` + * + * @author Gregory Kick + */ + class CompilationRule : TestRule { + private var elements: Elements? = null + private var types: Types? = null + + override fun apply(base: Statement, description: Description): Statement { + return object : Statement() { + override fun evaluate() { + val thrown = AtomicReference<Throwable>() + val successful = compile( + listOf(object : AbstractProcessor() { + override fun getSupportedSourceVersion() = SourceVersion.latest() + + override fun getSupportedAnnotationTypes() = setOf("*") + + @Synchronized override fun init(processingEnv: ProcessingEnvironment) { + super.init(processingEnv) + elements = processingEnv.elementUtils + types = processingEnv.typeUtils + } + + override fun process( + annotations: Set<TypeElement>, + roundEnv: RoundEnvironment + ): Boolean { + // just run the test on the last round after compilation is over + if (roundEnv.processingOver()) { + try { + base.evaluate() + } catch (e: Throwable) { + thrown.set(e) + } + } + return false + } + }) + ) + check(successful) + val t = thrown.get() + if (t != null) { + throw t + } + } + } + } + + /** + * Returns the [Elements] instance associated with the current execution of the rule. + * + * @throws IllegalStateException if this method is invoked outside the execution of the rule. + */ + fun getElements() = elements!! + + /** + * Returns the [Types] instance associated with the current execution of the rule. + * + * @throws IllegalStateException if this method is invoked outside the execution of the rule. + */ + fun getTypes() = types!! + + private fun compile(processors: Iterable<Processor>): Boolean { + val compiler = EclipseCompiler() + val diagnosticCollector = DiagnosticCollector<JavaFileObject>() + val fileManager = compiler.getStandardFileManager( + diagnosticCollector, Locale.getDefault(), UTF_8 + ) + val task = compiler.getTask( + null, + fileManager, + diagnosticCollector, + ImmutableSet.of(), + ImmutableSet.of(TypesEclipseTest::class.java.canonicalName), + ImmutableSet.of() + ) + task.setProcessors(processors) + return task.call()!! + } + } + + @JvmField @Rule val compilation = CompilationRule() + + override val elements: Elements + get() = compilation.getElements() + + override val types: Types + get() = compilation.getTypes() +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/TypesTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/TypesTest.kt new file mode 100644 index 00000000..43bdbb7c --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/TypesTest.kt @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2014 Google, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.testing.compile.CompilationRule +import org.junit.Rule +import javax.lang.model.util.Elements +import javax.lang.model.util.Types +import kotlin.test.Ignore + +@Ignore("Not clear this test is useful to retain in the Kotlin world") +class TypesTest : AbstractTypesTest() { + @JvmField @Rule val compilation = CompilationRule() + + override val elements: Elements + get() = compilation.elements + + override val types: Types + get() = compilation.types +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/UtilTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/UtilTest.kt new file mode 100644 index 00000000..de3650b6 --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/UtilTest.kt @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2016 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import kotlin.test.assertEquals + +class UtilTest { + @Test fun characterLiteral() { + assertEquals("a", characterLiteralWithoutSingleQuotes('a')) + assertEquals("b", characterLiteralWithoutSingleQuotes('b')) + assertEquals("c", characterLiteralWithoutSingleQuotes('c')) + assertEquals("%", characterLiteralWithoutSingleQuotes('%')) + // common escapes + assertEquals("\\b", characterLiteralWithoutSingleQuotes('\b')) + assertEquals("\\t", characterLiteralWithoutSingleQuotes('\t')) + assertEquals("\\n", characterLiteralWithoutSingleQuotes('\n')) + assertEquals("\\u000c", characterLiteralWithoutSingleQuotes('\u000c')) + assertEquals("\\r", characterLiteralWithoutSingleQuotes('\r')) + assertEquals("\"", characterLiteralWithoutSingleQuotes('"')) + assertEquals("\\'", characterLiteralWithoutSingleQuotes('\'')) + assertEquals("\\\\", characterLiteralWithoutSingleQuotes('\\')) + // octal escapes + assertEquals("\\u0000", characterLiteralWithoutSingleQuotes('\u0000')) + assertEquals("\\u0007", characterLiteralWithoutSingleQuotes('\u0007')) + assertEquals("?", characterLiteralWithoutSingleQuotes('\u003f')) + assertEquals("\\u007f", characterLiteralWithoutSingleQuotes('\u007f')) + assertEquals("¿", characterLiteralWithoutSingleQuotes('\u00bf')) + assertEquals("ÿ", characterLiteralWithoutSingleQuotes('\u00ff')) + // unicode escapes + assertEquals("\\u0000", characterLiteralWithoutSingleQuotes('\u0000')) + assertEquals("\\u0001", characterLiteralWithoutSingleQuotes('\u0001')) + assertEquals("\\u0002", characterLiteralWithoutSingleQuotes('\u0002')) + assertEquals("€", characterLiteralWithoutSingleQuotes('\u20AC')) + assertEquals("☃", characterLiteralWithoutSingleQuotes('\u2603')) + assertEquals("♠", characterLiteralWithoutSingleQuotes('\u2660')) + assertEquals("♣", characterLiteralWithoutSingleQuotes('\u2663')) + assertEquals("♥", characterLiteralWithoutSingleQuotes('\u2665')) + assertEquals("♦", characterLiteralWithoutSingleQuotes('\u2666')) + assertEquals("✵", characterLiteralWithoutSingleQuotes('\u2735')) + assertEquals("✺", characterLiteralWithoutSingleQuotes('\u273A')) + assertEquals("/", characterLiteralWithoutSingleQuotes('\uFF0F')) + } + + @Test fun stringLiteral() { + stringLiteral("abc") + stringLiteral("♦♥♠♣") + stringLiteral("€\\t@\\t\${\'\$\'}", "€\t@\t$") + assertThat(stringLiteralWithQuotes("abc();\ndef();")) + .isEqualTo("\"\"\"\n|abc();\n|def();\n\"\"\".trimMargin()") + stringLiteral("This is \\\"quoted\\\"!", "This is \"quoted\"!") + stringLiteral("e^{i\\\\pi}+1=0", "e^{i\\pi}+1=0") + assertThat(stringLiteralWithQuotes("abc();\ndef();", isConstantContext = true)) + .isEqualTo("\"abc();\\ndef();\"") + } + + @Test fun legalIdentifiers() { + assertThat("foo".isIdentifier).isTrue() + assertThat("bAr1".isIdentifier).isTrue() + assertThat("1".isIdentifier).isFalse() + assertThat("♦♥♠♣".isIdentifier).isFalse() + assertThat("`♦♥♠♣`".isIdentifier).isTrue() + assertThat("` ♣ !`".isIdentifier).isTrue() + assertThat("€".isIdentifier).isFalse() + assertThat("`€`".isIdentifier).isTrue() + assertThat("`1`".isIdentifier).isTrue() + assertThat("```".isIdentifier).isFalse() + assertThat("``".isIdentifier).isFalse() + assertThat("\n".isIdentifier).isFalse() + assertThat("`\n`".isIdentifier).isFalse() + assertThat("\r".isIdentifier).isFalse() + assertThat("`\r`".isIdentifier).isFalse() + assertThat("when".isIdentifier).isTrue() + assertThat("fun".isIdentifier).isTrue() + assertThat("".isIdentifier).isFalse() + } + + @Test fun escapeNonJavaIdentifiers() { + assertThat("8startWithNumber".escapeIfNecessary()).isEqualTo("`8startWithNumber`") + assertThat("with-hyphen".escapeIfNecessary()).isEqualTo("`with-hyphen`") + assertThat("with space".escapeIfNecessary()).isEqualTo("`with·space`") + assertThat("with_unicode_punctuation\u2026".escapeIfNecessary()).isEqualTo("`with_unicode_punctuation\u2026`") + } + + @Test fun escapeSpaceInName() { + val generated = FileSpec.builder("a", "b") + .addFunction( + FunSpec.builder("foo").apply { + addParameter("aaa bbb", typeNameOf<(Int) -> String>()) + val arg = mutableListOf<String>() + addStatement( + StringBuilder().apply { + repeat(10) { + append("%N($it) + ") + arg += "aaa bbb" + } + append("%N(100)") + arg += "aaa bbb" + }.toString(), + *arg.toTypedArray(), + ) + }.build(), + ) + .build() + .toString() + + val expectedOutput = """ + package a + + import kotlin.Function1 + import kotlin.Int + import kotlin.String + import kotlin.Unit + + public fun foo(`aaa bbb`: Function1<Int, String>): Unit { + `aaa bbb`(0) + `aaa bbb`(1) + `aaa bbb`(2) + `aaa bbb`(3) + `aaa bbb`(4) + `aaa bbb`(5) + + `aaa bbb`(6) + `aaa bbb`(7) + `aaa bbb`(8) + `aaa bbb`(9) + `aaa bbb`(100) + } + + """.trimIndent() + + assertThat(generated).isEqualTo(expectedOutput) + } + + @Test fun escapeMultipleTimes() { + assertThat("A-\$B".escapeIfNecessary()).isEqualTo("`A-\$B`") + } + + @Test fun escapeEscaped() { + assertThat("`A`".escapeIfNecessary()).isEqualTo("`A`") + } + + private fun stringLiteral(string: String) = stringLiteral(string, string) + + private fun stringLiteral(expected: String, value: String) = + assertEquals("\"$expected\"", stringLiteralWithQuotes(value)) +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/ValueTypeSpecTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/ValueTypeSpecTest.kt new file mode 100644 index 00000000..e6efa3fa --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/ValueTypeSpecTest.kt @@ -0,0 +1,256 @@ +/* + * Copyright (C) 2021 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet + +import com.google.common.truth.Truth.assertThat +import com.squareup.kotlinpoet.KModifier.INLINE +import com.squareup.kotlinpoet.KModifier.PRIVATE +import org.junit.Test +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Parameterized::class) +class ValueTypeSpecTest(private val useValue: Boolean) { + + companion object { + @JvmStatic + @Parameterized.Parameters(name = "value={0}") + fun data(): Collection<Array<Any>> { + return listOf( + arrayOf(true), + arrayOf(false), + ) + } + } + + private val modifier = if (useValue) KModifier.VALUE else INLINE + private val modifierString = modifier.keyword + + private fun classBuilder() = if (useValue) { + TypeSpec.valueClassBuilder("Guacamole") + } else { + TypeSpec.classBuilder("Guacamole") + .addModifiers(modifier) + } + + @Test fun validInlineClass() { + val guacamole = classBuilder() + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("avacado", String::class) + .build(), + ) + .addProperty( + PropertySpec.builder("avacado", String::class) + .initializer("avacado") + .build(), + ) + .build() + + assertThat(guacamole.toString()).isEqualTo( + """ + |public $modifierString class Guacamole( + | public val avacado: kotlin.String, + |) + | + """.trimMargin(), + ) + } + + @Test fun inlineClassWithInitBlock() { + val guacamole = classBuilder() + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("avacado", String::class) + .build(), + ) + .addProperty( + PropertySpec.builder("avacado", String::class) + .initializer("avacado") + .build(), + ) + .addInitializerBlock(CodeBlock.EMPTY) + .build() + + assertThat(guacamole.toString()).isEqualTo( + """ + |public $modifierString class Guacamole( + | public val avacado: kotlin.String, + |) { + | init { + | } + |} + | + """.trimMargin(), + ) + } + + class InlineSuperClass + + @Test fun inlineClassWithSuperClass() { + assertThrows<IllegalStateException> { + classBuilder() + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("avocado", String::class) + .build(), + ) + .addProperty( + PropertySpec.builder("avocado", String::class) + .initializer("avocado") + .build(), + ) + .superclass(InlineSuperClass::class) + .build() + }.hasMessageThat().isEqualTo("value/inline classes cannot have super classes") + } + + interface InlineSuperInterface + + @Test fun inlineClassInheritsFromInterface() { + val guacamole = classBuilder() + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("avocado", String::class) + .build(), + ) + .addProperty( + PropertySpec.builder("avocado", String::class) + .initializer("avocado") + .build(), + ) + .addSuperinterface(InlineSuperInterface::class) + .build() + + assertThat(guacamole.toString()).isEqualTo( + """ + |public $modifierString class Guacamole( + | public val avocado: kotlin.String, + |) : com.squareup.kotlinpoet.ValueTypeSpecTest.InlineSuperInterface + | + """.trimMargin(), + ) + } + + @Test fun inlineClassWithoutBackingProperty() { + assertThrows<IllegalArgumentException> { + classBuilder() + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("avocado", String::class) + .build(), + ) + .addProperty("garlic", String::class) + .build() + }.hasMessageThat().isEqualTo("value/inline classes must have a single read-only (val) property parameter.") + } + + @Test fun inlineClassWithoutProperties() { + assertThrows<IllegalStateException> { + classBuilder() + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("avocado", String::class) + .build(), + ) + .build() + }.hasMessageThat().isEqualTo("value/inline classes must have at least 1 property") + } + + @Test fun inlineClassWithMutableProperties() { + assertThrows<IllegalStateException> { + classBuilder() + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("avocado", String::class) + .build(), + ) + .addProperty( + PropertySpec.builder("avocado", String::class) + .initializer("avocado") + .mutable() + .build(), + ) + .build() + }.hasMessageThat().isEqualTo("value/inline classes must have a single read-only (val) property parameter.") + } + + @Test + fun inlineClassWithPrivateConstructor() { + val guacamole = classBuilder() + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("avocado", String::class) + .addModifiers(PRIVATE) + .build(), + ) + .addProperty( + PropertySpec.builder("avocado", String::class) + .initializer("avocado") + .build(), + ) + .build() + + assertThat(guacamole.toString()).isEqualTo( + """ + |public $modifierString class Guacamole private constructor( + | public val avocado: kotlin.String, + |) + | + """.trimMargin(), + ) + } + + @Test fun inlineEnumClass() { + val guacamole = TypeSpec.enumBuilder("Foo") + .addModifiers(modifier) + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("x", Int::class) + .build(), + ) + .addEnumConstant( + "A", + TypeSpec.anonymousClassBuilder() + .addSuperclassConstructorParameter("%L", 1) + .build(), + ) + .addEnumConstant( + "B", + TypeSpec.anonymousClassBuilder() + .addSuperclassConstructorParameter("%L", 2) + .build(), + ) + .addProperty( + PropertySpec.builder("x", Int::class) + .initializer("x") + .build(), + ) + .build() + assertThat(guacamole.toString()).isEqualTo( + """ + |public enum $modifierString class Foo( + | public val x: kotlin.Int, + |) { + | A(1), + | B(2), + | ; + |} + | + """.trimMargin(), + ) + } +} diff --git a/kotlinpoet/src/test/java/com/squareup/kotlinpoet/jvm/JvmAnnotationsTest.kt b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/jvm/JvmAnnotationsTest.kt new file mode 100644 index 00000000..f0b68a10 --- /dev/null +++ b/kotlinpoet/src/test/java/com/squareup/kotlinpoet/jvm/JvmAnnotationsTest.kt @@ -0,0 +1,1215 @@ +/* + * Copyright (C) 2018 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.squareup.kotlinpoet.jvm + +import com.google.common.truth.Truth.assertThat +import com.squareup.kotlinpoet.ClassName +import com.squareup.kotlinpoet.FileSpec +import com.squareup.kotlinpoet.FunSpec +import com.squareup.kotlinpoet.KModifier.DATA +import com.squareup.kotlinpoet.ParameterSpec +import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy +import com.squareup.kotlinpoet.PropertySpec +import com.squareup.kotlinpoet.STRING +import com.squareup.kotlinpoet.TypeSpec +import com.squareup.kotlinpoet.asClassName +import com.squareup.kotlinpoet.asTypeName +import com.squareup.kotlinpoet.assertThrows +import java.io.IOException +import kotlin.test.Test + +class JvmAnnotationsTest { + + @Test fun jvmField() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.classBuilder("Taco") + .addProperty( + PropertySpec.builder("foo", String::class) + .jvmField() + .initializer("%S", "foo") + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.jvm.JvmField + | + |public class Taco { + | @JvmField + | public val foo: String = "foo" + |} + | + """.trimMargin(), + ) + } + + @Test fun jvmFieldConstructorParameter() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.classBuilder("Taco") + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("foo", String::class) + .build(), + ) + .addProperty( + PropertySpec.builder("foo", String::class) + .jvmField() + .initializer("foo") + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.jvm.JvmField + | + |public class Taco( + | @JvmField + | public val foo: String, + |) + | + """.trimMargin(), + ) + } + + @Test fun jvmStaticProperty() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.classBuilder("Taco") + .addType( + TypeSpec.companionObjectBuilder() + .addProperty( + PropertySpec.builder("foo", String::class) + .jvmStatic() + .initializer("%S", "foo") + .build(), + ) + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.jvm.JvmStatic + | + |public class Taco { + | public companion object { + | @JvmStatic + | public val foo: String = "foo" + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun jvmStaticFunction() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.classBuilder("Taco") + .addType( + TypeSpec.companionObjectBuilder() + .addFunction( + FunSpec.builder("foo") + .jvmStatic() + .addStatement("return %S", "foo") + .returns(String::class) + .build(), + ) + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.jvm.JvmStatic + | + |public class Taco { + | public companion object { + | @JvmStatic + | public fun foo(): String = "foo" + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun jvmStaticGetter() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.classBuilder("Taco") + .addType( + TypeSpec.companionObjectBuilder() + .addProperty( + PropertySpec.builder("foo", String::class) + .getter( + FunSpec.getterBuilder() + .jvmStatic() + .addStatement("return %S", "foo") + .build(), + ) + .build(), + ) + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.jvm.JvmStatic + | + |public class Taco { + | public companion object { + | public val foo: String + | @JvmStatic + | get() = "foo" + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun jvmStaticSetter() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.classBuilder("Taco") + .addType( + TypeSpec.companionObjectBuilder() + .addProperty( + PropertySpec.builder("foo", String::class.asTypeName()) + .mutable() + .setter( + FunSpec.setterBuilder() + .jvmStatic() + .addParameter("value", String::class) + .build(), + ) + .initializer("%S", "foo") + .build(), + ) + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.jvm.JvmStatic + | + |public class Taco { + | public companion object { + | public var foo: String = "foo" + | @JvmStatic + | set(`value`) { + | } + | } + |} + | + """.trimMargin(), + ) + } + + @Test fun jvmStaticForbiddenOnConstructor() { + assertThrows<IllegalStateException> { + FunSpec.constructorBuilder() + .jvmStatic() + }.hasMessageThat().isEqualTo("Can't apply @JvmStatic to a constructor!") + } + + @Test fun throwsFunction() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addFunction( + FunSpec.builder("foo") + .throws(IOException::class, IllegalArgumentException::class) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.io.IOException + |import java.lang.IllegalArgumentException + |import kotlin.Unit + |import kotlin.jvm.Throws + | + |@Throws( + | IOException::class, + | IllegalArgumentException::class, + |) + |public fun foo(): Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun throwsFunctionCustomException() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addFunction( + FunSpec.builder("foo") + .throws(ClassName("com.squareup.tacos", "IllegalTacoException")) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + |import kotlin.jvm.Throws + | + |@Throws(IllegalTacoException::class) + |public fun foo(): Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun throwsPrimaryConstructor() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.classBuilder("Taco") + .primaryConstructor( + FunSpec.constructorBuilder() + .throws(IOException::class) + .addParameter("foo", String::class) + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.io.IOException + |import kotlin.String + |import kotlin.jvm.Throws + | + |public class Taco @Throws(IOException::class) constructor( + | foo: String, + |) + | + """.trimMargin(), + ) + } + + @Test fun throwsGetter() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addProperty( + PropertySpec.builder("foo", String::class) + .getter( + FunSpec.getterBuilder() + .throws(IOException::class) + .addStatement("return %S", "foo") + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.io.IOException + |import kotlin.String + |import kotlin.jvm.Throws + | + |public val foo: String + | @Throws(IOException::class) + | get() = "foo" + | + """.trimMargin(), + ) + } + + @Test fun throwsSetter() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addProperty( + PropertySpec.builder("foo", String::class) + .mutable() + .setter( + FunSpec.setterBuilder() + .throws(IOException::class) + .addParameter("value", String::class) + .addStatement("print(%S)", "foo") + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import java.io.IOException + |import kotlin.String + |import kotlin.jvm.Throws + | + |public var foo: String + | @Throws(IOException::class) + | set(`value`) { + | print("foo") + | } + | + """.trimMargin(), + ) + } + + @Test fun jvmOverloadsFunction() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addFunction( + FunSpec.builder("foo") + .jvmOverloads() + .addParameter("bar", Int::class) + .addParameter( + ParameterSpec.builder("baz", String::class) + .defaultValue("%S", "baz") + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Int + |import kotlin.String + |import kotlin.Unit + |import kotlin.jvm.JvmOverloads + | + |@JvmOverloads + |public fun foo(bar: Int, baz: String = "baz"): Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun jvmOverloadsPrimaryConstructor() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.classBuilder("Taco") + .primaryConstructor( + FunSpec.constructorBuilder() + .jvmOverloads() + .addParameter("bar", Int::class) + .addParameter( + ParameterSpec.builder("baz", String::class) + .defaultValue("%S", "baz") + .build(), + ) + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Int + |import kotlin.String + |import kotlin.jvm.JvmOverloads + | + |public class Taco @JvmOverloads constructor( + | bar: Int, + | baz: String = "baz", + |) + | + """.trimMargin(), + ) + } + + @Test fun jvmOverloadsOnGetterForbidden() { + assertThrows<IllegalStateException> { + FunSpec.getterBuilder() + .jvmOverloads() + }.hasMessageThat().isEqualTo("Can't apply @JvmOverloads to a getter!") + } + + @Test fun jvmOverloadsOnSetterForbidden() { + assertThrows<IllegalStateException> { + FunSpec.setterBuilder() + .jvmOverloads() + }.hasMessageThat().isEqualTo("Can't apply @JvmOverloads to a setter!") + } + + @Test fun jvmNameFile() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .jvmName("TacoUtils") + .addProperty( + PropertySpec.builder("foo", String::class) + .initializer("%S", "foo") + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |@file:JvmName("TacoUtils") + | + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.jvm.JvmName + | + |public val foo: String = "foo" + | + """.trimMargin(), + ) + } + + @Test fun jvmNameFunction() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addFunction( + FunSpec.builder("foo") + .jvmName("getFoo") + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + |import kotlin.jvm.JvmName + | + |@JvmName("getFoo") + |public fun foo(): Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun jvmNameGetter() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addProperty( + PropertySpec.builder("foo", String::class) + .getter( + FunSpec.getterBuilder() + .jvmName("foo") + .addStatement("return %S", "foo") + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.jvm.JvmName + | + |public val foo: String + | @JvmName("foo") + | get() = "foo" + | + """.trimMargin(), + ) + } + + @Test fun jvmNameSetter() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addProperty( + PropertySpec.builder("foo", String::class.asTypeName()) + .mutable() + .initializer("%S", "foo") + .setter( + FunSpec.setterBuilder() + .jvmName("foo") + .addParameter("value", String::class) + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.jvm.JvmName + | + |public var foo: String = "foo" + | @JvmName("foo") + | set(`value`) { + | } + | + """.trimMargin(), + ) + } + + @Test fun jvmNameForbiddenOnConstructor() { + assertThrows<IllegalStateException> { + FunSpec.constructorBuilder() + .jvmName("notAConstructor") + }.hasMessageThat().isEqualTo("Can't apply @JvmName to a constructor!") + } + + @Test fun jvmMultifileClass() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .jvmMultifileClass() + .addProperty( + PropertySpec.builder("foo", String::class) + .initializer("%S", "foo") + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |@file:JvmMultifileClass + | + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.jvm.JvmMultifileClass + | + |public val foo: String = "foo" + | + """.trimMargin(), + ) + } + + @Test fun jvmSuppressWildcardsClass() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.classBuilder("Taco") + .jvmSuppressWildcards() + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.jvm.JvmSuppressWildcards + | + |@JvmSuppressWildcards + |public class Taco + | + """.trimMargin(), + ) + } + + @Test fun jvmSuppressWildcardsFunction() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addFunction( + FunSpec.builder("foo") + .jvmSuppressWildcards(suppress = false) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + |import kotlin.jvm.JvmSuppressWildcards + | + |@JvmSuppressWildcards(suppress = false) + |public fun foo(): Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun jvmSuppressWildcardsOnConstructorForbidden() { + assertThrows<IllegalStateException> { + FunSpec.constructorBuilder() + .jvmSuppressWildcards() + }.hasMessageThat().isEqualTo("Can't apply @JvmSuppressWildcards to a constructor!") + } + + @Test fun jvmSuppressWildcardsOnGetterForbidden() { + assertThrows<IllegalStateException> { + FunSpec.getterBuilder() + .jvmSuppressWildcards() + }.hasMessageThat().isEqualTo("Can't apply @JvmSuppressWildcards to a getter!") + } + + @Test fun jvmSuppressWildcardsOnSetterForbidden() { + assertThrows<IllegalStateException> { + FunSpec.setterBuilder() + .jvmSuppressWildcards() + }.hasMessageThat().isEqualTo("Can't apply @JvmSuppressWildcards to a setter!") + } + + @Test fun jvmSuppressWildcardsProperty() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addProperty( + PropertySpec.builder("foo", String::class) + .jvmSuppressWildcards(suppress = false) + .initializer("%S", "foo") + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.jvm.JvmSuppressWildcards + | + |@JvmSuppressWildcards(suppress = false) + |public val foo: String = "foo" + | + """.trimMargin(), + ) + } + + @Test fun jvmSuppressWildcardsType() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addFunction( + FunSpec.builder("foo") + .addParameter( + "a", + List::class.asClassName() + .parameterizedBy(Int::class.asTypeName().jvmSuppressWildcards()), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Int + |import kotlin.Unit + |import kotlin.collections.List + |import kotlin.jvm.JvmSuppressWildcards + | + |public fun foo(a: List<@JvmSuppressWildcards Int>): Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun jvmWildcardType() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addFunction( + FunSpec.builder("foo") + .addParameter( + "a", + List::class.asClassName() + .parameterizedBy(Int::class.asTypeName().jvmWildcard()), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Int + |import kotlin.Unit + |import kotlin.collections.List + |import kotlin.jvm.JvmWildcard + | + |public fun foo(a: List<@JvmWildcard Int>): Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun synchronizedFunction() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addFunction( + FunSpec.builder("foo") + .synchronized() + .addStatement("return %S", "foo") + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.jvm.Synchronized + | + |@Synchronized + |public fun foo() = "foo" + | + """.trimMargin(), + ) + } + + @Test fun synchronizedGetter() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addProperty( + PropertySpec.builder("foo", String::class) + .getter( + FunSpec.getterBuilder() + .synchronized() + .addStatement("return %S", "foo") + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.jvm.Synchronized + | + |public val foo: String + | @Synchronized + | get() = "foo" + | + """.trimMargin(), + ) + } + + @Test fun synchronizedSetter() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addProperty( + PropertySpec.builder("foo", String::class.asTypeName()) + .mutable() + .initializer("%S", "foo") + .setter( + FunSpec.setterBuilder() + .synchronized() + .addParameter("value", String::class) + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.jvm.Synchronized + | + |public var foo: String = "foo" + | @Synchronized + | set(`value`) { + | } + | + """.trimMargin(), + ) + } + + @Test fun synchronizedOnConstructorForbidden() { + assertThrows<IllegalStateException> { + FunSpec.constructorBuilder() + .synchronized() + }.hasMessageThat().isEqualTo("Can't apply @Synchronized to a constructor!") + } + + @Test fun transient() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.classBuilder("Taco") + .addProperty( + PropertySpec.builder("foo", String::class) + .transient() + .initializer("%S", "foo") + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.jvm.Transient + | + |public class Taco { + | @Transient + | public val foo: String = "foo" + |} + | + """.trimMargin(), + ) + } + + @Test fun transientConstructorParameter() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.classBuilder("Taco") + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("foo", String::class) + .build(), + ) + .addProperty( + PropertySpec.builder("foo", String::class) + .transient() + .initializer("foo") + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.jvm.Transient + | + |public class Taco( + | @Transient + | public val foo: String, + |) + | + """.trimMargin(), + ) + } + + @Test fun volatile() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.classBuilder("Taco") + .addProperty( + PropertySpec.builder("foo", String::class) + .volatile() + .initializer("%S", "foo") + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.jvm.Volatile + | + |public class Taco { + | @Volatile + | public val foo: String = "foo" + |} + | + """.trimMargin(), + ) + } + + @Test fun volatileConstructorParameter() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.classBuilder("Taco") + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("foo", String::class) + .build(), + ) + .addProperty( + PropertySpec.builder("foo", String::class) + .volatile() + .initializer("foo") + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.jvm.Volatile + | + |public class Taco( + | @Volatile + | public val foo: String, + |) + | + """.trimMargin(), + ) + } + + @Test fun strictfpFunction() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addFunction( + FunSpec.builder("foo") + .strictfp() + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.Unit + |import kotlin.jvm.Strictfp + | + |@Strictfp + |public fun foo(): Unit { + |} + | + """.trimMargin(), + ) + } + + @Test fun strictfpPrimaryConstructor() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.classBuilder("Taco") + .primaryConstructor( + FunSpec.constructorBuilder() + .strictfp() + .addParameter("foo", String::class) + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.jvm.Strictfp + | + |public class Taco @Strictfp constructor( + | foo: String, + |) + | + """.trimMargin(), + ) + } + + @Test fun strictfpGetter() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addProperty( + PropertySpec.builder("foo", String::class) + .getter( + FunSpec.getterBuilder() + .strictfp() + .addStatement("return %S", "foo") + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.jvm.Strictfp + | + |public val foo: String + | @Strictfp + | get() = "foo" + | + """.trimMargin(), + ) + } + + @Test fun strictfpSetter() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addProperty( + PropertySpec.builder("foo", String::class) + .mutable() + .setter( + FunSpec.setterBuilder() + .strictfp() + .addParameter("value", String::class) + .addStatement("print(%S)", "foo") + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.jvm.Strictfp + | + |public var foo: String + | @Strictfp + | set(`value`) { + | print("foo") + | } + | + """.trimMargin(), + ) + } + + @Test fun jvmDefaultProperty() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.interfaceBuilder("Taco") + .addProperty( + PropertySpec.builder("foo", String::class) + .jvmDefault() + .initializer("%S", "foo") + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.jvm.JvmDefault + | + |public interface Taco { + | @JvmDefault + | public val foo: String = "foo" + |} + | + """.trimMargin(), + ) + } + + @Test fun jvmDefaultFunction() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.interfaceBuilder("Taco") + .addFunction( + FunSpec.builder("foo") + .jvmDefault() + .returns(String::class) + .addStatement("return %S", "foo") + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.jvm.JvmDefault + | + |public interface Taco { + | @JvmDefault + | public fun foo(): String = "foo" + |} + | + """.trimMargin(), + ) + } + + @Test fun jvmInlineClass() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.valueClassBuilder("Taco") + .jvmInline() + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("value", STRING) + .build(), + ) + .addProperty( + PropertySpec.builder("value", STRING) + .initializer("value") + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.jvm.JvmInline + | + |@JvmInline + |public value class Taco( + | public val `value`: String, + |) + | + """.trimMargin(), + ) + } + + @Test fun jvmRecordClass() { + val file = FileSpec.builder("com.squareup.tacos", "Taco") + .addType( + TypeSpec.classBuilder("Taco") + .jvmRecord() + .addModifiers(DATA) + .primaryConstructor( + FunSpec.constructorBuilder() + .addParameter("value", STRING) + .build(), + ) + .addProperty( + PropertySpec.builder("value", STRING) + .initializer("value") + .build(), + ) + .build(), + ) + .build() + assertThat(file.toString()).isEqualTo( + """ + |package com.squareup.tacos + | + |import kotlin.String + |import kotlin.jvm.JvmRecord + | + |@JvmRecord + |public data class Taco( + | public val `value`: String, + |) + | + """.trimMargin(), + ) + } +} diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 00000000..74caf790 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,69 @@ +# pip install mkdocs mkdocs-material +# mkdocs serve +# mkdocs gh-deploy + +site_name: KotlinPoet +repo_name: KotlinPoet +repo_url: https://github.com/square/kotlinpoet +site_description: "A Kotlin API for generating .kt source files" +site_author: Square, Inc. +remote_branch: gh-pages + +copyright: 'Copyright © 2015 Square, Inc.' + +theme: + name: 'material' + logo: 'images/icon-square.png' + favicon: 'images/icon-square.png' + palette: + - media: '(prefers-color-scheme: light)' + scheme: default + primary: 'cyan' + accent: 'deep-purple' + toggle: + icon: material/weather-night + name: Switch to dark mode + - media: '(prefers-color-scheme: dark)' + scheme: slate + primary: 'black' + accent: 'blue-grey' + toggle: + icon: material/weather-sunny + name: Switch to light mode + +extra_css: + - 'css/app.css' + +markdown_extensions: + - smarty + - codehilite: + guess_lang: false + - footnotes + - meta + - toc: + permalink: true + - pymdownx.betterem: + smart_enable: all + - pymdownx.caret + - pymdownx.inlinehilite + - pymdownx.magiclink + - pymdownx.smartsymbols + - pymdownx.superfences + - pymdownx.emoji + - tables + - admonition + +nav: + - 'Overview': + - 'KotlinPoet': index.md + - 'Interop - JavaPoet': interop-javapoet.md + - 'Interop - kotlinx-metadata': interop-kotlinx-metadata.md + - 'Interop - KSP': interop-ksp.md + - 'API': + - 'kotlinpoet': 1.x/kotlinpoet/index.html + - 'interop-javapoet': 1.x/interop-javapoet/index.html + - 'interop-kotlinx-metadata': 1.x/interop-kotlinx-metadata/index.html + - 'interop-ksp': 1.x/interop-ksp/index.html + - 'Stack Overflow ⏏': https://stackoverflow.com/questions/tagged/kotlinpoet?sort=active + - 'Change Log': changelog.md + - 'Contributing': contributing.md diff --git a/renovate.json b/renovate.json new file mode 100644 index 00000000..7ebea239 --- /dev/null +++ b/renovate.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://docs.renovatebot.com/renovate-schema.json", + "extends": [ + "config:base" + ], + "packageRules": [ + { + "matchManagers": ["pip_requirements"], + "automerge": true + } + ] +} diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 00000000..3e23cc20 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2019 Square, Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +pluginManagement { + repositories { + mavenCentral() + gradlePluginPortal() + } +} + +include( + ":kotlinpoet", + ":interop:javapoet", + ":interop:kotlinx-metadata", + ":interop:ksp", + ":interop:ksp:test-processor", +) |