├── .github └── workflows │ └── ci.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── RELEASING.md ├── build.gradle.kts ├── core ├── build.gradle.kts ├── gradle.properties └── src │ ├── main │ └── kotlin │ │ ├── SynchronizedToolProvider.kt │ │ └── com │ │ └── tschuchort │ │ └── compiletesting │ │ ├── AbstractKotlinCompilation.kt │ │ ├── CompilationResult.kt │ │ ├── DefaultPropertyDelegate.kt │ │ ├── DiagnosticMessage.kt │ │ ├── DiagnosticsMessageCollector.kt │ │ ├── HostEnvironment.kt │ │ ├── JavacUtils.kt │ │ ├── KaptComponentRegistrar.kt │ │ ├── KotlinCompilation.kt │ │ ├── KotlinJsCompilation.kt │ │ ├── MainCommandLineProcessor.kt │ │ ├── MainComponentRegistrar.kt │ │ ├── MutliMessageCollector.kt │ │ ├── PrecursorTool.kt │ │ ├── SourceFile.kt │ │ ├── StreamUtils.kt │ │ ├── Utils.kt │ │ └── kapt │ │ └── util.kt │ └── test │ ├── java │ └── com │ │ └── tschuchort │ │ └── compiletesting │ │ └── JavaTestProcessor.java │ ├── kotlin │ └── com │ │ └── tschuchort │ │ └── compiletesting │ │ ├── CompilerPluginsTest.kt │ │ ├── FakeCompilerPluginRegistrar.kt │ │ ├── JavacUtilsTest.kt │ │ ├── KotlinCompilationTests.kt │ │ ├── KotlinJsCompilationTests.kt │ │ ├── KotlinTestProcessor.kt │ │ ├── Mockito.kt │ │ ├── StreamUtilTests.kt │ │ └── TestUtils.kt │ └── resources │ └── mockito-extensions │ └── org.mockito.plugins.MockMaker ├── gradle.properties ├── gradle ├── libs.versions.toml └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── ksp ├── build.gradle.kts ├── gradle.properties └── src │ ├── main │ └── kotlin │ │ └── com │ │ └── tschuchort │ │ └── compiletesting │ │ ├── Ksp.kt │ │ ├── Ksp2.kt │ │ ├── KspTool.kt │ │ └── TestKSPLogger.kt │ └── test │ └── kotlin │ └── com │ └── tschuchort │ └── compiletesting │ ├── AbstractTestSymbolProcessor.kt │ ├── KspTest.kt │ └── TestClasses.kt ├── release.sh ├── renovate.json └── settings.gradle.kts /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | # Only run push on main 5 | push: 6 | branches: 7 | - main 8 | paths-ignore: 9 | - '**/*.md' 10 | # Always run on PRs 11 | pull_request: 12 | branches: [ main ] 13 | 14 | concurrency: 15 | group: 'ci-${{ github.event.merge_group.head_ref || github.head_ref }}-${{ github.workflow }}' 16 | cancel-in-progress: true 17 | 18 | jobs: 19 | build: 20 | name: "${{ matrix.platform }}" 21 | runs-on: ${{ matrix.platform }} 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | platform: [ 'windows-latest', 'ubuntu-latest', 'macos-latest' ] 26 | steps: 27 | - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 28 | 29 | - uses: gradle/actions/wrapper-validation@v4 30 | 31 | - name: Set up JDK 32 | uses: actions/setup-java@v4 33 | with: 34 | distribution: 'zulu' 35 | java-version: '22' 36 | 37 | - name: Check 38 | run: ./gradlew check 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by .ignore support plugin (hsz.mobi) 2 | ### Xcode template 3 | # Xcode 4 | # 5 | # gitignore contributors: remember to update Global/Xcode.gitignore, Objective-C.gitignore & Swift.gitignore 6 | 7 | ## User settings 8 | xcuserdata/ 9 | 10 | ## compatibility with Xcode 8 and earlier (ignoring not required starting Xcode 9) 11 | *.xcscmblueprint 12 | *.xccheckout 13 | 14 | ## compatibility with Xcode 3 and earlier (ignoring not required starting Xcode 4) 15 | build/ 16 | DerivedData/ 17 | *.moved-aside 18 | *.pbxuser 19 | !default.pbxuser 20 | *.mode1v3 21 | !default.mode1v3 22 | *.mode2v3 23 | !default.mode2v3 24 | *.perspectivev3 25 | !default.perspectivev3 26 | ### SublimeText template 27 | # Cache files for Sublime Text 28 | *.tmlanguage.cache 29 | *.tmPreferences.cache 30 | *.stTheme.cache 31 | 32 | # Workspace files are user-specific 33 | *.sublime-workspace 34 | 35 | # Project files should be checked into the repository, unless a significant 36 | # proportion of contributors will probably not be using Sublime Text 37 | # *.sublime-project 38 | 39 | # SFTP configuration file 40 | sftp-config.json 41 | 42 | # Package control specific files 43 | Package Control.last-run 44 | Package Control.ca-list 45 | Package Control.ca-bundle 46 | Package Control.system-ca-bundle 47 | Package Control.cache/ 48 | Package Control.ca-certs/ 49 | Package Control.merged-ca-bundle 50 | Package Control.user-ca-bundle 51 | oscrypto-ca-bundle.crt 52 | bh_unicode_properties.cache 53 | 54 | # Sublime-github package stores a github token in this file 55 | # https://packagecontrol.io/packages/sublime-github 56 | GitHub.sublime-settings 57 | ### Vim template 58 | # Swap 59 | [._]*.s[a-v][a-z] 60 | [._]*.sw[a-p] 61 | [._]s[a-v][a-z] 62 | [._]sw[a-p] 63 | 64 | # Session 65 | Session.vim 66 | 67 | # Temporary 68 | .netrwhist 69 | *~ 70 | # Auto-generated tag files 71 | tags 72 | ### JetBrains template 73 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 74 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 75 | 76 | # User-specific stuff: 77 | .idea/**/workspace.xml 78 | .idea/**/tasks.xml 79 | .idea/dictionaries 80 | .idea/inspectionProfiles 81 | 82 | # Sensitive or high-churn files: 83 | .idea/**/dataSources/ 84 | .idea/**/dataSources.ids 85 | .idea/**/dataSources.xml 86 | .idea/**/dataSources.local.xml 87 | .idea/**/sqlDataSources.xml 88 | .idea/**/dynamic.xml 89 | .idea/**/uiDesigner.xml 90 | .idea/misc.xml 91 | .idea/jarRepositories.xml 92 | .idea/compiler.xml 93 | 94 | # Gradle: 95 | .idea/**/gradle.xml 96 | .idea/**/libraries 97 | .gradle/ 98 | .kotlin/ 99 | 100 | # CMake 101 | cmake-build-debug/ 102 | cmake-build-release/ 103 | 104 | # Mongo Explorer plugin: 105 | .idea/**/mongoSettings.xml 106 | 107 | ## File-based project format: 108 | *.iws 109 | 110 | ## Plugin-specific files: 111 | 112 | # IntelliJ 113 | out/ 114 | 115 | *.iml 116 | .idea/modules.xml 117 | 118 | # mpeltonen/sbt-idea plugin 119 | .idea_modules/ 120 | 121 | # JIRA plugin 122 | atlassian-ide-plugin.xml 123 | 124 | # Cursive Clojure plugin 125 | .idea/replstate.xml 126 | 127 | # Crashlytics plugin (for Android Studio and IntelliJ) 128 | com_crashlytics_export_strings.xml 129 | crashlytics.properties 130 | crashlytics-build.properties 131 | fabric.properties 132 | ### Java template 133 | # Compiled class file 134 | *.class 135 | 136 | # Log file 137 | *.log 138 | 139 | # BlueJ files 140 | *.ctxt 141 | 142 | # Mobile Tools for Java (J2ME) 143 | .mtj.tmp/ 144 | 145 | # Package Files # 146 | *.jar 147 | *.war 148 | *.ear 149 | *.zip 150 | *.tar.gz 151 | *.rar 152 | 153 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 154 | hs_err_pid* 155 | ### VisualStudioCode template 156 | .vscode/* 157 | !.vscode/settings.json 158 | !.vscode/tasks.json 159 | !.vscode/launch.json 160 | !.vscode/extensions.json 161 | ### Windows template 162 | # Windows thumbnail cache files 163 | Thumbs.db 164 | ehthumbs.db 165 | ehthumbs_vista.db 166 | 167 | # Dump file 168 | *.stackdump 169 | 170 | # Folder config file 171 | [Dd]esktop.ini 172 | 173 | # Recycle Bin used on file shares 174 | $RECYCLE.BIN/ 175 | 176 | # Windows Installer files 177 | *.cab 178 | *.msi 179 | *.msm 180 | *.msp 181 | 182 | # Windows shortcuts 183 | *.lnk 184 | ### macOS template 185 | # General 186 | .DS_Store 187 | .AppleDouble 188 | .LSOverride 189 | 190 | # Icon must end with two \r 191 | Icon 192 | 193 | # Thumbnails 194 | ._* 195 | 196 | # Files that might appear in the root of a volume 197 | .DocumentRevisions-V100 198 | .fseventsd 199 | .Spotlight-V100 200 | .TemporaryItems 201 | .Trashes 202 | .VolumeIcon.icns 203 | .com.apple.timemachine.donotpresent 204 | 205 | # Directories potentially created on remote AFP share 206 | .AppleDB 207 | .AppleDesktop 208 | Network Trash Folder 209 | Temporary Items 210 | .apdisk 211 | ### TortoiseGit template 212 | # Project-level settings 213 | /.tgitconfig 214 | ### Kotlin template 215 | # Compiled class file 216 | *.class 217 | 218 | # Log file 219 | *.log 220 | 221 | # BlueJ files 222 | *.ctxt 223 | 224 | # Mobile Tools for Java (J2ME) 225 | .mtj.tmp/ 226 | 227 | # Package Files # 228 | *.jar 229 | *.war 230 | *.ear 231 | *.zip 232 | *.tar.gz 233 | *.rar 234 | 235 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 236 | hs_err_pid* 237 | 238 | !gradle/wrapper/gradle-wrapper.jar 239 | 240 | ksp/build 241 | ksp/.idea 242 | 243 | .idea/ -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | **Unreleased** 5 | -------------- 6 | 7 | 0.7.1 8 | ----- 9 | 10 | _2025-05-05_ 11 | 12 | - Remove references to `useOldBackend`, which is removed in Kotlin `2.2.0`. 13 | 14 | 0.7.0 15 | ----- 16 | 17 | _2024-11-28_ 18 | 19 | - Remove `irOnly` option from `KotlinJsCompilation`. 20 | - Default to the current language/api version if one isn't specified in KSP2 invocations. 21 | - Update to Kotlin `2.1.0`. 22 | - Update to KSP `2.1.0-1.0.29`. 23 | 24 | 0.6.0 25 | ----- 26 | 27 | _2024-11-11_ 28 | 29 | - **Enhancement**: Cleanup old sources between compilations. 30 | - Update to Kotlin `2.0.21`. 31 | - Update to KSP `2.0.21-1.0.27`. Note that this is now the minimum version of KSP support. 32 | - Update classgraph to `4.8.177`. 33 | - Update Okio to `3.9.1`. 34 | 35 | Special thanks to [@ansman](https://github.com/ansman) for contributing to this release! 36 | 37 | 0.5.1 38 | ----- 39 | 40 | _2024-06-05_ 41 | 42 | - **New**: Capture diagnostics with a severity level. This allows the output to be more easily filtered after the fact. 43 | 44 | Special thanks to [@evant](https://github.com/evant) for contributing to this release! 45 | 46 | 0.5.0 47 | ----- 48 | 49 | _2024-06-05_ 50 | 51 | - Update to Kotlin `2.0.0`. 52 | - Update to KSP `2.0.0-1.0.22`. 53 | - Change `supportsK2` to true by default. 54 | - Change `disableStandardScript` to true by default. This doesn't seem to work reliably in K2 testing. 55 | - Update kapt class location references. 56 | - Support Kapt4 (AKA kapt.k2). 57 | - Support KSP2. 58 | - Introduce a new `KotlinCompilation.useKsp()` API to simplify KSP configuration. 59 | - Update to ClassGraph `4.8.173`. 60 | 61 | Note that in order to test Kapt 3 or KSP 1, you must now also set `languageVersion` to `1.9` in your `KotlinCompilation` configuration. 62 | 63 | 0.4.1 64 | ----- 65 | 66 | _2024-03-25_ 67 | 68 | - **Fix**: Fix decoding of classloader resources. 69 | - Update to Kotlin `1.9.23`. 70 | - Update to KSP `1.9.2301.0.19`. 71 | - Update to classgraph `4.8.168`. 72 | - Update to Okio `3.9.0`. 73 | 74 | Special thanks to [@jbarr21](https://github.com/jbarr21) for contributing to this release! 75 | 76 | 0.4.0 77 | ----- 78 | 79 | _2023-10-31_ 80 | 81 | - **Enhancement**: Create parent directories of `SourceFile` in compilations. 82 | - Update to Kotlin `1.9.20`. 83 | - Update to KSP `1.9.20-1.0.13`. 84 | - Update to ClassGraph `4.8.162`. 85 | - Update to Okio `3.6.0`. 86 | 87 | Special thanks to [@BraisGabin](https://github.com/BraisGabin) for contributing to this release! 88 | 89 | 0.3.2 90 | ----- 91 | 92 | _2023-08-01_ 93 | 94 | - **Fix**: Include KSP-generated Java files in java compilation. This is particularly useful for KSP processors that generate Java code. 95 | - **Enhancement**: Print full diagnostic messages when javac compilation fails, not just the cause. The cause message alone was often not very helpful. 96 | 97 | 0.3.1 98 | ----- 99 | 100 | _2023-07-22_ 101 | 102 | - **Fix**: Set required `languageVersionSettings` property in `KspOptions`. 103 | - Update to KSP `1.9.0-1.0.12`. 104 | - Update to Okio `3.4.0`. 105 | 106 | 0.3.0 107 | ----- 108 | 109 | _2023-07-06_ 110 | 111 | - **New**: Refactor results into common `CompilationResult` hierarchy. 112 | - **Fix**: Missing UTF-8 encoding of logs resulting in unknown chars. 113 | - **Fix**: Set resources path when compilerPluginRegistrars not empty. 114 | - `useIR` is now enabled by default. 115 | - Update to Kotlin `1.9.0`. 116 | - Update to KSP `1.9.0-1.0.11`. 117 | 118 | Special thanks to [@SimonMarquis](https://github.com/SimonMarquis) and [@bennyhuo](https://github.com/bennyhuo) for contributing to this release! 119 | 120 | 0.2.1 121 | ----- 122 | 123 | _2023-01-09_ 124 | 125 | Happy new year! 126 | 127 | - **New**: Expose the API to pass flags to KAPT. This is necessary in order to use KAPT's new JVM IR support. 128 | 129 | 0.2.0 130 | ----- 131 | 132 | _2022-12-28_ 133 | 134 | - Deprecate `KotlinCompilation.singleModule` option as it no longer exists in kotlinc. 135 | - Propagate `@ExperimentalCompilerApi` annotations 136 | - `KotlinJsCompilation.irOnly` and `KotlinJsCompilation.irProduceJs` now default to true and are the only supported options. 137 | - Expose new `KotlinCompilation.compilerPluginRegistrars` property for adding `CompilerPluginRegistrar` instances (the new entrypoint API for compiler plugins) 138 | ```kotlin 139 | KotlinCompilation().apply { 140 | compilerPluginRegistrars = listOf(MyCompilerPluginRegistrar()) 141 | } 142 | ``` 143 | - Deprecate `KotlinCompilation.compilerPlugins` in favor of `KotlinCompilation.componentRegistrars`. The latter is also deprecated, but this is at least a clearer name. 144 | ```diff 145 | KotlinCompilation().apply { 146 | - compilerPlugins = listOf(MyComponentRegistrar()) 147 | + componentRegistrars = listOf(MyComponentRegistrar()) 148 | } 149 | ``` 150 | - Don't try to set removed kotlinc args. If they're removed, they're removed forever. This library will just track latest kotlin releases with its own. 151 | - Dependency updates: 152 | ``` 153 | Kotlin (and its associated artifacts) 1.8.0 154 | KSP 1.8.0 155 | Classgraph: 4.8.153 156 | ``` 157 | 158 | Special thanks to [@bnorm](https://github.com/bnorm) for contributing to this release. 159 | 160 | 0.1.0 161 | ----- 162 | 163 | _2022-12-01_ 164 | 165 | Initial release. Changes from the original repo are as follows 166 | 167 | Base commit: https://github.com/tschuchortdev/kotlin-compile-testing/commit/4f394fe485a0d6e0ed438dd9ce140b172b1bd746 168 | 169 | - **New**: Add `supportsK2` option to `KotlinCompilation` to allow testing the new K2 compiler. 170 | - Update to Kotlin `1.7.22`. 171 | - Update to KSP `1.7.22-1.0.8`. 172 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kotlin Compile Testing 2 | 3 | A library for in-process compilation of Kotlin and Java code, in the spirit of [Google Compile Testing](https://github.com/google/compile-testing). For example, you can use this library to test your annotation processor or compiler plugin. 4 | 5 | **NOTE** This project is a fork of the original [tschuchortdev/kotlin-compile-testing](https://github.com/tschuchortdev/kotlin-compile-testing), which itself started as a fork of [Moshi](https://github.com/square/moshi)'s compile test infra. The goal of this fork is to better track the latest Kotlin releases. 6 | 7 | ## Use Cases 8 | 9 | - Compile Kotlin and Java code in tests 10 | - Test annotation processors 11 | - Test compiler plugins 12 | - Test Kotlin code generation 13 | 14 | ## Example 15 | 16 | Create sources 17 | 18 | ```Kotlin 19 | class TestEnvClass {} 20 | 21 | @Test 22 | fun `test my annotation processor`() { 23 | val kotlinSource = SourceFile.kotlin( 24 | "KClass.kt", """ 25 | class KClass { 26 | fun foo() { 27 | // Classes from the test environment are visible to the compiled sources 28 | val testEnvClass = TestEnvClass() 29 | } 30 | } 31 | """ 32 | ) 33 | 34 | val javaSource = SourceFile.java( 35 | "JClass.java", """ 36 | public class JClass { 37 | public void bar() { 38 | // compiled Kotlin classes are visible to Java sources 39 | KClass kClass = new KClass(); 40 | } 41 | } 42 | """ 43 | ) 44 | } 45 | ``` 46 | 47 | Configure compilation 48 | ```Kotlin 49 | val result = KotlinCompilation().apply { 50 | sources = listOf(kotlinSource, javaSource) 51 | 52 | // pass your own instance of an annotation processor 53 | annotationProcessors = listOf(MyAnnotationProcessor()) 54 | 55 | // pass your own instance of a compiler plugin 56 | compilerPlugins = listOf(MyComponentRegistrar()) 57 | commandLineProcessors = listOf(MyCommandlineProcessor()) 58 | 59 | inheritClassPath = true 60 | messageOutputStream = System.out // see diagnostics in real time 61 | }.compile() 62 | ``` 63 | 64 | Assert results 65 | ```Kotlin 66 | assertThat(result.exitCode).isEqualTo(ExitCode.OK) 67 | 68 | // Test diagnostic output of compiler 69 | assertThat(result.messages).contains("My annotation processor was called") 70 | 71 | // Load compiled classes and inspect generated code through reflection 72 | val kClazz = result.classLoader.loadClass("KClass") 73 | assertThat(kClazz).hasDeclaredMethods("foo") 74 | ``` 75 | 76 | ## Features 77 | - Mixed-source sets: Compile Kotlin and Java source files in a single run 78 | - Annotation processing: 79 | - Run annotation processors on Kotlin and Java sources 80 | - Generate Kotlin and Java sources 81 | - Both Kotlin and Java sources have access to the generated sources 82 | - Provide your own instances of annotation processors directly to the compiler instead of letting the compiler create them with a service locator 83 | - Debug annotation processors: Since the compilation runs in the same process as your application, you can easily debug it instead of having to attach your IDE's debugger manually to the compilation process 84 | - Inherit classpath: Compiled sources have access to classes in your application 85 | - Project Jigsaw compatible: Kotlin-Compile-Testing works with JDK 8 as well as JDK 9 and later 86 | - JDK-crosscompilation: Provide your own JDK to compile the code against, instead of using the host application's JDK. This allows you to easily test your code on all JDK versions 87 | - Find dependencies automatically on the host classpath 88 | 89 | ## Installation 90 | 91 | The package is available on Maven Central. 92 | 93 | Add dependency to your module's `build.gradle` file: 94 | 95 | ```Groovy 96 | dependencies { 97 | // ... 98 | testImplementation("dev.zacsweers.kctfork:core:>") 99 | } 100 | ``` 101 | 102 | ## Compatibility 103 | 104 | Kotlin-Compile-Testing is compatible with all _local_ compiler versions. It does not matter what compiler you use to compile your project. 105 | 106 | However, if your project or any of its dependencies depend directly on compiler artifacts such as `kotlin-compiler-embeddable` or `kotlin-annotation-processing-embeddable` then they have to be the same version as the one used by Kotlin-Compile-Testing or there will be a transitive dependency conflict. 107 | 108 | Because the internal APIs of the Kotlin compiler often change between versions, we can only support one `kotlin-compiler-embeddable` version at a time. 109 | 110 | ## Kotlin Symbol Processing API Support 111 | [Kotlin Symbol Processing (KSP)](https://goo.gle/ksp) is a new annotation processing pipeline that builds on top of the 112 | plugin architecture of the Kotlin Compiler, instead of delegating to javac as `kapt` does. 113 | 114 | To test KSP processors, you need to use the KSP dependency: 115 | 116 | ```Groovy 117 | dependencies { 118 | testImplementation("dev.zacsweers.kctfork:ksp:>") 119 | } 120 | ``` 121 | 122 | This module adds a new function to the `KotlinCompilation` to specify KSP processors: 123 | 124 | ```Kotlin 125 | class MySymbolProcessorProvider : SymbolProcessorProvider { 126 | // implementation of the SymbolProcessorProvider from the KSP API 127 | } 128 | val compilation = KotlinCompilation().apply { 129 | sources = listOf(source) 130 | symbolProcessorProviders = listOf(MySymbolProcessorProvider()) 131 | } 132 | val result = compilation.compile() 133 | ``` 134 | All code generated by the KSP processor will be written into the `KotlinCompilation.kspSourcesDir` directory. 135 | 136 | ## Java 16 compatibility 137 | 138 | With the release of Java 16 the access control of the new Jigsaw module system is starting to be enforced by the JVM. Unfortunately, this impacts kotlin-compile-testing because KAPT still tries to access classes of javac that are not exported by the jdk.compiler module, leading to errors such as: 139 | ``` 140 | java.lang.IllegalAccessError: class org.jetbrains.kotlin.kapt3.base.KaptContext (in unnamed module @0x43b6aa9d) cannot access class com.sun.tools.javac.util.Context (in module jdk.compiler) because module jdk.compiler does not export com.sun.tools.javac.util to unnamed module @0x43b6aa9d 141 | ``` 142 | To mitigate this problem, you have to add the following code to your module's `build.gradle` file: 143 | ```groovy 144 | if (JavaVersion.current() >= JavaVersion.VERSION_16) { 145 | test { 146 | jvmArgs( 147 | "--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED", 148 | "--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED", 149 | "--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED", 150 | "--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED", 151 | "--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED", 152 | "--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED", 153 | "--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED", 154 | "--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED", 155 | "--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", 156 | "--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED", 157 | ) 158 | } 159 | } 160 | ``` 161 | 162 | or for Kotlin DSL 163 | 164 | ```kotlin 165 | if (JavaVersion.current() >= JavaVersion.VERSION_16) { 166 | tasks.withType().configureEach { 167 | jvmArgs( 168 | "--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED", 169 | "--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED", 170 | "--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED", 171 | "--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED", 172 | "--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED", 173 | "--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED", 174 | "--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED", 175 | "--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED", 176 | "--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", 177 | "--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED", 178 | ) 179 | } 180 | } 181 | ``` 182 | Since the kotlin compilation tests run in the same process as the test runner, these options have to be added manually and can not be set automatically by the kotlin-compile-testing library. 183 | 184 | ## License 185 | 186 | Copyright (C) 2021 Thilo Schuchort 187 | 188 | Licensed under the Mozilla Public License 2.0 189 | 190 | For custom license agreements contact me directly 191 | -------------------------------------------------------------------------------- /RELEASING.md: -------------------------------------------------------------------------------- 1 | Releasing 2 | ========= 3 | 4 | 1. Update the `CHANGELOG.md` for the impending release. 5 | 2. Run `./release.sh `. 6 | 3. Publish the release on the repo's releases tab. 7 | -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | import com.vanniktech.maven.publish.MavenPublishBaseExtension 2 | import org.jetbrains.dokka.gradle.DokkaExtension 3 | import org.jetbrains.dokka.gradle.DokkaTask 4 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 5 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 6 | 7 | plugins { 8 | alias(libs.plugins.kotlin.jvm) apply false 9 | alias(libs.plugins.dokka) 10 | alias(libs.plugins.mavenPublish) apply false 11 | } 12 | 13 | dokka { 14 | dokkaPublications.html { 15 | outputDirectory.set(rootDir.resolve("docs/api/0.x")) 16 | includes.from(project.layout.projectDirectory.file("README.md")) 17 | } 18 | } 19 | 20 | subprojects { 21 | pluginManager.withPlugin("java") { 22 | configure { toolchain { languageVersion.set(JavaLanguageVersion.of(22)) } } 23 | 24 | tasks.withType().configureEach { options.release.set(8) } 25 | } 26 | 27 | pluginManager.withPlugin("org.jetbrains.kotlin.jvm") { 28 | tasks.withType().configureEach { 29 | compilerOptions { 30 | jvmTarget.set(JvmTarget.JVM_1_8) 31 | progressiveMode.set(true) 32 | } 33 | } 34 | } 35 | 36 | if (JavaVersion.current() >= JavaVersion.VERSION_16) { 37 | tasks.withType().configureEach { 38 | jvmArgs( 39 | "--add-opens=jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED", 40 | "--add-opens=jdk.compiler/com.sun.tools.javac.code=ALL-UNNAMED", 41 | "--add-opens=jdk.compiler/com.sun.tools.javac.comp=ALL-UNNAMED", 42 | "--add-opens=jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED", 43 | "--add-opens=jdk.compiler/com.sun.tools.javac.jvm=ALL-UNNAMED", 44 | "--add-opens=jdk.compiler/com.sun.tools.javac.main=ALL-UNNAMED", 45 | "--add-opens=jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED", 46 | "--add-opens=jdk.compiler/com.sun.tools.javac.processing=ALL-UNNAMED", 47 | "--add-opens=jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED", 48 | "--add-opens=jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED", 49 | ) 50 | } 51 | } 52 | 53 | pluginManager.withPlugin("com.vanniktech.maven.publish") { 54 | apply(plugin = "org.jetbrains.dokka") 55 | 56 | configure { 57 | dokkaPublicationDirectory.set(layout.buildDirectory.dir("dokkaDir")) 58 | dokkaSourceSets.configureEach { 59 | skipDeprecated.set(true) 60 | } 61 | } 62 | 63 | configure { 64 | publishToMavenCentral(automaticRelease = true) 65 | signAllPublications() 66 | } 67 | } 68 | } 69 | 70 | dependencies { 71 | dokka(projects.core) 72 | dokka(projects.ksp) 73 | } 74 | -------------------------------------------------------------------------------- /core/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | kotlin("jvm") 5 | kotlin("kapt") 6 | alias(libs.plugins.ksp) 7 | alias(libs.plugins.buildconfig) 8 | alias(libs.plugins.mavenPublish) 9 | } 10 | 11 | buildConfig { 12 | className.set("BuildConfig") 13 | packageName.set("com.tschuchort.compiletesting") 14 | sourceSets { 15 | test { 16 | buildConfigField("String", "KOTLIN_VERSION", "\"${libs.versions.kotlin.get()}\"") 17 | } 18 | } 19 | } 20 | 21 | dependencies { 22 | ksp(libs.autoService.ksp) 23 | 24 | implementation(libs.autoService) 25 | implementation(libs.okio) 26 | implementation(libs.classgraph) 27 | 28 | // These dependencies are only needed as a "sample" compiler plugin to test that 29 | // running compiler plugins passed via the pluginClasspath CLI option works 30 | testRuntimeOnly(libs.kotlin.scriptingCompiler) 31 | testRuntimeOnly(libs.intellij.core) 32 | testRuntimeOnly(libs.intellij.util) 33 | 34 | api(libs.kotlin.compilerEmbeddable) 35 | api(libs.kotlin.annotationProcessingEmbeddable) 36 | api(libs.kotlin.kapt4) 37 | 38 | testImplementation(libs.kotlinpoet) 39 | testImplementation(libs.javapoet) 40 | testImplementation(libs.kotlin.junit) 41 | testImplementation(libs.mockito) 42 | testImplementation(libs.mockitoKotlin) 43 | testImplementation(libs.assertJ) 44 | } 45 | 46 | tasks.withType().configureEach { 47 | val isTest = name.contains("test", ignoreCase = true) 48 | compilerOptions { 49 | if (isTest) { 50 | optIn.add("org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi") 51 | } 52 | } 53 | } 54 | 55 | tasks.withType().configureEach { 56 | duplicatesStrategy = DuplicatesStrategy.EXCLUDE 57 | } 58 | -------------------------------------------------------------------------------- /core/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=core 2 | POM_NAME=Kotlin Compile Testing (Core) 3 | POM_DESCRIPTION=Kotlin Compile Testing (Core) -------------------------------------------------------------------------------- /core/src/main/kotlin/SynchronizedToolProvider.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018-present Facebook, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); you may 5 | * not use this file except in compliance with the License. You may obtain 6 | * a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 12 | * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 | * License for the specific language governing permissions and limitations 14 | * under the License. 15 | */ 16 | 17 | package com.facebook.buck.jvm.java.javax 18 | 19 | import com.tschuchort.compiletesting.isJdk9OrLater 20 | 21 | import java.lang.reflect.InvocationTargetException 22 | import java.lang.reflect.Method 23 | import javax.tools.JavaCompiler 24 | import javax.tools.ToolProvider 25 | 26 | 27 | /** 28 | * ToolProvider has no synchronization internally, so if we don't synchronize from the outside we 29 | * could wind up loading the compiler classes multiple times from different class loaders. 30 | */ 31 | internal object SynchronizedToolProvider { 32 | private var getPlatformClassLoaderMethod: Method? = null 33 | 34 | val systemJavaCompiler: JavaCompiler 35 | get() { 36 | val compiler = synchronized(ToolProvider::class.java) { 37 | ToolProvider.getSystemJavaCompiler() 38 | } 39 | 40 | check(compiler != null) { "System java compiler is null! Are you running without JDK?" } 41 | return compiler 42 | } 43 | 44 | // The compiler classes are loaded using the platform class loader in Java 9+. 45 | val systemToolClassLoader: ClassLoader 46 | get() { 47 | if (isJdk9OrLater()) { 48 | try { 49 | return getPlatformClassLoaderMethod!!.invoke(null) as ClassLoader 50 | } catch (e: IllegalAccessException) { 51 | throw RuntimeException(e) 52 | } catch (e: InvocationTargetException) { 53 | throw RuntimeException(e) 54 | } 55 | 56 | } 57 | 58 | val classLoader: ClassLoader 59 | synchronized(ToolProvider::class.java) { 60 | classLoader = ToolProvider.getSystemToolClassLoader() 61 | } 62 | return classLoader 63 | } 64 | 65 | init { 66 | if (isJdk9OrLater()) { 67 | try { 68 | getPlatformClassLoaderMethod = ClassLoader::class.java.getMethod("getPlatformClassLoader") 69 | } catch (e: NoSuchMethodException) { 70 | throw RuntimeException(e) 71 | } 72 | 73 | } 74 | } 75 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/com/tschuchort/compiletesting/AbstractKotlinCompilation.kt: -------------------------------------------------------------------------------- 1 | package com.tschuchort.compiletesting 2 | 3 | import java.io.File 4 | import java.io.OutputStream 5 | import java.io.PrintStream 6 | import java.net.URI 7 | import java.net.URL 8 | import java.nio.file.Files 9 | import java.nio.file.Path 10 | import java.nio.file.Paths 11 | import okio.Buffer 12 | import org.jetbrains.kotlin.cli.common.CLICompiler 13 | import org.jetbrains.kotlin.cli.common.ExitCode 14 | import org.jetbrains.kotlin.cli.common.arguments.CommonCompilerArguments 15 | import org.jetbrains.kotlin.cli.common.arguments.parseCommandLineArguments 16 | import org.jetbrains.kotlin.cli.common.arguments.validateArguments 17 | import org.jetbrains.kotlin.cli.common.messages.MessageCollector 18 | import org.jetbrains.kotlin.cli.common.messages.MessageRenderer 19 | import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector 20 | import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor 21 | import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar 22 | import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar 23 | import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi 24 | import org.jetbrains.kotlin.config.Services 25 | import org.jetbrains.kotlin.kapt3.base.KaptOptions 26 | import org.jetbrains.kotlin.util.ServiceLoaderLite 27 | 28 | /** 29 | * Base compilation class for sharing common compiler arguments and 30 | * functionality. Should not be used outside of this library as it is an 31 | * implementation detail. 32 | */ 33 | @ExperimentalCompilerApi 34 | abstract class AbstractKotlinCompilation internal constructor() { 35 | /** Working directory for the compilation */ 36 | var workingDir: File by default { 37 | val path = Files.createTempDirectory("Kotlin-Compilation") 38 | log("Created temporary working directory at ${path.toAbsolutePath()}") 39 | return@default path.toFile() 40 | } 41 | 42 | /** 43 | * Paths to directories or .jar files that contain classes 44 | * to be made available in the compilation (i.e. added to 45 | * the classpath) 46 | */ 47 | var classpaths: List = emptyList() 48 | 49 | /** 50 | * Paths to plugins to be made available in the compilation 51 | */ 52 | var pluginClasspaths: List = emptyList() 53 | 54 | @Deprecated( 55 | "Use componentRegistrars instead", 56 | ReplaceWith("componentRegistrars"), 57 | DeprecationLevel.ERROR 58 | ) 59 | var compilerPlugins: List = emptyList() 60 | 61 | /** 62 | * Legacy [ComponentRegistrar] plugins that should be added to the compilation. 63 | */ 64 | @Deprecated("Migrate to ComponentPluginRegistrar and use compilerPluginRegistrars instead") 65 | var componentRegistrars: List = emptyList() 66 | 67 | /** 68 | * [CompilerPluginRegistrars][CompilerPluginRegistrar] that should be added to the compilation. 69 | */ 70 | var compilerPluginRegistrars: List = emptyList() 71 | 72 | /** 73 | * Commandline processors for compiler plugins that should be added to the compilation 74 | */ 75 | var commandLineProcessors: List = emptyList() 76 | 77 | /** Source files to be compiled */ 78 | var sources: List = emptyList() 79 | 80 | /** Print verbose logging info */ 81 | var verbose: Boolean = true 82 | 83 | /** 84 | * Helpful information (if [verbose] = true) and the compiler 85 | * system output will be written to this stream 86 | */ 87 | var messageOutputStream: OutputStream = System.out 88 | 89 | /** Inherit classpath from calling process */ 90 | var inheritClassPath: Boolean = false 91 | 92 | /** Suppress all warnings */ 93 | var suppressWarnings: Boolean = false 94 | 95 | /** All warnings should be treated as errors */ 96 | var allWarningsAsErrors: Boolean = false 97 | 98 | /** Report locations of files generated by the compiler */ 99 | var reportOutputFiles: Boolean by default { verbose } 100 | 101 | /** Report on performance of the compilation */ 102 | var reportPerformance: Boolean = false 103 | 104 | var languageVersion: String? = null 105 | var apiVersion: String? = null 106 | 107 | /** Enable experimental multiplatform support */ 108 | var multiplatform: Boolean = false 109 | 110 | /** Do not check presence of 'actual' modifier in multi-platform projects */ 111 | var noCheckActual: Boolean = false 112 | 113 | /** Enable usages of API that requires opt-in with an opt-in requirement marker with the given fully qualified name */ 114 | var optIn: List? = null 115 | 116 | /** Additional string arguments to the Kotlin compiler */ 117 | var kotlincArguments: List = emptyList() 118 | 119 | /** Options to be passed to compiler plugins: -P plugin::=*/ 120 | var pluginOptions: List = emptyList() 121 | 122 | /** 123 | * Path to the kotlin-stdlib-common.jar 124 | * If none is given, it will be searched for in the host 125 | * process' classpaths 126 | */ 127 | var kotlinStdLibCommonJar: File? by default { 128 | HostEnvironment.kotlinStdLibCommonJar 129 | } 130 | 131 | /** Enable support for the new K2 compiler. */ 132 | var supportsK2 = true 133 | 134 | /** Disables compiler scripting support. */ 135 | var disableStandardScript = true 136 | 137 | /** Tools that need to run before the compilation. */ 138 | val precursorTools: MutableMap = mutableMapOf() 139 | 140 | protected val extraGeneratedSources = mutableListOf() 141 | 142 | /** Registers extra directories with generated sources, such as sources generated by KSP. */ 143 | fun registerGeneratedSourcesDir(dir: File) { 144 | extraGeneratedSources.add(dir) 145 | } 146 | 147 | // Directory for input source files 148 | protected val sourcesDir get() = workingDir.resolve("sources") 149 | 150 | protected fun commonArguments(args: A, configuration: (args: A) -> Unit): A { 151 | args.pluginClasspaths = pluginClasspaths.map(File::getAbsolutePath).toTypedArray() 152 | 153 | args.verbose = verbose 154 | 155 | args.suppressWarnings = suppressWarnings 156 | args.allWarningsAsErrors = allWarningsAsErrors 157 | args.reportOutputFiles = reportOutputFiles 158 | args.reportPerf = reportPerformance 159 | args.multiPlatform = multiplatform 160 | args.noCheckActual = noCheckActual 161 | args.optIn = optIn?.toTypedArray() 162 | 163 | if (languageVersion != null) { 164 | args.languageVersion = this.languageVersion 165 | } 166 | 167 | if (apiVersion != null) { 168 | args.apiVersion = this.apiVersion 169 | } 170 | 171 | configuration(args) 172 | 173 | /** 174 | * It's not possible to pass dynamic [CommandLineProcessor] instances directly to the [K2JSCompiler] 175 | * because the compiler discovers them on the classpath through a service locator, so we need to apply 176 | * the same trick as with [ComponentRegistrar]s: We put our own static [CommandLineProcessor] on the 177 | * classpath which in turn calls the user's dynamic [CommandLineProcessor] instances. 178 | */ 179 | MainCommandLineProcessor.threadLocalParameters.set( 180 | MainCommandLineProcessor.ThreadLocalParameters(commandLineProcessors) 181 | ) 182 | 183 | /** 184 | * Our [MainCommandLineProcessor] only has access to the CLI options that belong to its own plugin ID. 185 | * So in order to be able to access CLI options that are meant for other [CommandLineProcessor]s we 186 | * wrap these CLI options, send them to our own plugin ID and later unwrap them again to forward them 187 | * to the correct [CommandLineProcessor]. 188 | */ 189 | args.pluginOptions = pluginOptions.map { (pluginId, optionName, optionValue) -> 190 | "plugin:${MainCommandLineProcessor.pluginId}:${MainCommandLineProcessor.encodeForeignOptionName(pluginId, optionName)}=$optionValue" 191 | }.toTypedArray() 192 | 193 | /* Parse extra CLI arguments that are given as strings so users can specify arguments that are not yet 194 | implemented here as well-typed properties. */ 195 | parseCommandLineArguments(kotlincArguments, args) 196 | 197 | validateArguments(args.errors)?.let { 198 | throw IllegalArgumentException("Errors parsing kotlinc CLI arguments:\n$it") 199 | } 200 | 201 | return args 202 | } 203 | 204 | /** Performs the compilation step to compile Kotlin source files */ 205 | protected fun compileKotlin(sources: List, compiler: CLICompiler, arguments: A): KotlinCompilation.ExitCode { 206 | if (this is KotlinCompilation) { 207 | // Execute precursor tools first 208 | for (tool in precursorTools) { 209 | val exitCode = try { 210 | tool.value.execute(this, internalMessageStream, sources) 211 | } catch (t: Throwable) { 212 | t.message?.let { internalMessageStream.println(it) } 213 | t.printStackTrace(internalMessageStream) 214 | KotlinCompilation.ExitCode.INTERNAL_ERROR 215 | } 216 | if (exitCode != KotlinCompilation.ExitCode.OK) { 217 | return exitCode 218 | } 219 | } 220 | } 221 | 222 | // Update sources to include any generated in a precursor tool 223 | val generatedSourceFiles = extraGeneratedSources.flatMap(File::listFilesRecursively).filter(File::hasKotlinFileExtension) 224 | .map { it.absoluteFile } 225 | val finalSources = sources + generatedSourceFiles 226 | 227 | /** 228 | * Here the list of compiler plugins is set 229 | * 230 | * To avoid that the annotation processors are executed twice, 231 | * the list is set to empty 232 | */ 233 | MainComponentRegistrar.threadLocalParameters.set( 234 | MainComponentRegistrar.ThreadLocalParameters( 235 | listOf(), 236 | KaptOptions.Builder(), 237 | componentRegistrars, 238 | compilerPluginRegistrars, 239 | supportsK2 240 | ) 241 | ) 242 | 243 | // in this step also include source files generated by kapt in the previous step 244 | val args = arguments.also { args -> 245 | args.freeArgs = 246 | finalSources.map(File::getAbsolutePath).distinct() + if (finalSources.none(File::hasKotlinFileExtension)) { 247 | /* __HACK__: The Kotlin compiler expects at least one Kotlin source file or it will crash, 248 | so we trick the compiler by just including an empty .kt-File. We need the compiler to run 249 | even if there are no Kotlin files because some compiler plugins may also process Java files. */ 250 | listOf(SourceFile.new("emptyKotlinFile.kt", "").writeTo(sourcesDir).absolutePath) 251 | } else { 252 | emptyList() 253 | } 254 | args.pluginClasspaths = (args.pluginClasspaths ?: emptyArray()) + 255 | /** The resources path contains the MainComponentRegistrar and MainCommandLineProcessor which will 256 | be found by the Kotlin compiler's service loader. We add it only when the user has actually given 257 | us ComponentRegistrar instances to be loaded by the MainComponentRegistrar because the experimental 258 | K2 compiler doesn't support plugins yet. This way, users of K2 can prevent MainComponentRegistrar 259 | from being loaded and crashing K2 by setting both [componentRegistrars] and [commandLineProcessors] to 260 | the emptyList. */ 261 | if ( 262 | componentRegistrars.isNotEmpty() || 263 | compilerPluginRegistrars.isNotEmpty() || 264 | commandLineProcessors.isNotEmpty() 265 | ) arrayOf(getResourcesPath()) 266 | else emptyArray() 267 | } 268 | 269 | val compilerMessageCollector = PrintingMessageCollector( 270 | internalMessageStream, MessageRenderer.GRADLE_STYLE, verbose 271 | ) 272 | 273 | return convertKotlinExitCode( 274 | compiler.exec(compilerMessageCollector, Services.EMPTY, args) 275 | ) 276 | } 277 | 278 | protected fun getResourcesPath(): String { 279 | return this::class.java.classLoader.getResources(resourceName) 280 | .asSequence() 281 | .mapNotNull { url -> urlToResourcePath(url) } 282 | .find { resourcesPath -> 283 | ServiceLoaderLite.findImplementations(ComponentRegistrar::class.java, listOf(resourcesPath.toFile())) 284 | .any { implementation -> implementation == MainComponentRegistrar::class.java.name } 285 | }?.toString() ?: throw AssertionError("Could not get path to ComponentRegistrar service from META-INF") 286 | } 287 | 288 | /** Maps a URL resource for a class from a JAR or file to an absolute Path on disk */ 289 | internal fun urlToResourcePath(url: URL): Path? { 290 | val uri = url.toURI() 291 | val uriPath = when (uri.scheme) { 292 | "jar" -> uri.rawSchemeSpecificPart.removeSuffix("!/$resourceName") 293 | "file" -> uri.toString().removeSuffix("/$resourceName") 294 | else -> return null 295 | } 296 | return Paths.get(URI.create(uriPath)).toAbsolutePath() 297 | } 298 | 299 | /** Searches compiler log for known errors that are hard to debug for the user */ 300 | protected fun searchSystemOutForKnownErrors(compilerSystemOut: String) { 301 | if (compilerSystemOut.contains("No enum constant com.sun.tools.javac.main.Option.BOOT_CLASS_PATH")) { 302 | warn( 303 | "${this::class.simpleName} has detected that the compiler output contains an error message that may be " + 304 | "caused by including a tools.jar file together with a JDK of version 9 or later. " + 305 | if (inheritClassPath) 306 | "Make sure that no tools.jar (or unwanted JDK) is in the inherited classpath" 307 | else "" 308 | ) 309 | } 310 | 311 | if (compilerSystemOut.contains("Unable to find package java.")) { 312 | warn( 313 | "${this::class.simpleName} has detected that the compiler output contains an error message " + 314 | "that may be caused by a missing JDK. This can happen if jdkHome=null and inheritClassPath=false." 315 | ) 316 | } 317 | } 318 | 319 | protected val hostClasspaths by lazy { HostEnvironment.classpath } 320 | 321 | /* This internal buffer and stream is used so it can be easily converted to a string 322 | that is put into the [Result] object, in addition to printing immediately to the user's 323 | stream. */ 324 | protected val internalMessageBuffer = Buffer() 325 | protected val internalMessageStream = PrintStream( 326 | TeeOutputStream( 327 | object : OutputStream() { 328 | override fun write(b: Int) = messageOutputStream.write(b) 329 | override fun write(b: ByteArray) = messageOutputStream.write(b) 330 | override fun write(b: ByteArray, off: Int, len: Int) = messageOutputStream.write(b, off, len) 331 | override fun flush() = messageOutputStream.flush() 332 | override fun close() = messageOutputStream.close() 333 | }, 334 | internalMessageBuffer.outputStream() 335 | ), 336 | /* autoFlush = */ false, 337 | /* encoding = */ "UTF-8", 338 | ) 339 | private val _diagnostics: MutableList = mutableListOf() 340 | protected val diagnostics: List get() = _diagnostics 341 | 342 | protected fun createMessageCollector(stepName: String): MessageCollector { 343 | val diagnosticsMessageCollector = DiagnosticsMessageCollector(stepName, verbose, _diagnostics) 344 | val printMessageCollector = PrintingMessageCollector( 345 | internalMessageStream, 346 | MessageRenderer.GRADLE_STYLE, 347 | verbose 348 | ) 349 | return MultiMessageCollector(diagnosticsMessageCollector, printMessageCollector) 350 | } 351 | 352 | protected fun log(s: String) { 353 | if (verbose) { 354 | internalMessageStream.println("logging: $s") 355 | } 356 | } 357 | 358 | protected fun warn(s: String) { 359 | internalMessageStream.println("warning: $s") 360 | } 361 | protected fun error(s: String) { 362 | internalMessageStream.println("error: $s") 363 | } 364 | 365 | internal fun createMessageCollectorAccess(stepName: String): MessageCollector = createMessageCollector(stepName) 366 | 367 | private val resourceName = "META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar" 368 | } 369 | 370 | @ExperimentalCompilerApi 371 | internal fun convertKotlinExitCode(code: ExitCode) = when (code) { 372 | ExitCode.OK -> KotlinCompilation.ExitCode.OK 373 | ExitCode.OOM_ERROR -> throw OutOfMemoryError("Kotlin compiler ran out of memory") 374 | ExitCode.INTERNAL_ERROR -> KotlinCompilation.ExitCode.INTERNAL_ERROR 375 | ExitCode.COMPILATION_ERROR -> KotlinCompilation.ExitCode.COMPILATION_ERROR 376 | ExitCode.SCRIPT_EXECUTION_ERROR -> KotlinCompilation.ExitCode.SCRIPT_EXECUTION_ERROR 377 | } 378 | -------------------------------------------------------------------------------- /core/src/main/kotlin/com/tschuchort/compiletesting/CompilationResult.kt: -------------------------------------------------------------------------------- 1 | package com.tschuchort.compiletesting 2 | 3 | import java.io.File 4 | import java.net.URLClassLoader 5 | import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi 6 | 7 | /** Result of the compilation. */ 8 | @ExperimentalCompilerApi 9 | sealed interface CompilationResult { 10 | /** The exit code of the compilation. */ 11 | val exitCode: KotlinCompilation.ExitCode 12 | /** Messages that were printed by the compilation. */ 13 | val messages: String 14 | /** Messages with captured diagnostic severity. */ 15 | val diagnosticMessages: List 16 | /** The directory where compiled files will be output to. */ 17 | val outputDirectory: File 18 | /** Messages filtered by the given severities */ 19 | fun messagesWithSeverity(vararg severities: DiagnosticSeverity): String = 20 | diagnosticMessages.filter { it.severity in severities }.joinToString("\n") 21 | } 22 | 23 | @ExperimentalCompilerApi 24 | class JsCompilationResult( 25 | override val exitCode: KotlinCompilation.ExitCode, 26 | override val messages: String, 27 | override val diagnosticMessages: List, 28 | private val compilation: KotlinJsCompilation, 29 | ) : CompilationResult { 30 | override val outputDirectory: File 31 | get() = compilation.outputDir 32 | 33 | /** JS files output by the compilation. */ 34 | val jsFiles: List = outputDirectory.listFilesRecursively() 35 | } 36 | 37 | /** Result of the compilation */ 38 | @ExperimentalCompilerApi 39 | class JvmCompilationResult( 40 | override val exitCode: KotlinCompilation.ExitCode, 41 | override val messages: String, 42 | override val diagnosticMessages: List, 43 | private val compilation: KotlinCompilation, 44 | ) : CompilationResult { 45 | override val outputDirectory: File 46 | get() = compilation.classesDir 47 | 48 | /** ClassLoader to load the compiled classes */ 49 | val classLoader = 50 | URLClassLoader( 51 | // Include the original classpaths and the output directory to be able to load classes from 52 | // dependencies. 53 | compilation.classpaths.plus(outputDirectory).map { it.toURI().toURL() }.toTypedArray(), 54 | this::class.java.classLoader 55 | ) 56 | 57 | /** Compiled classes and resources output by the compilation. */ 58 | val compiledClassAndResourceFiles: List = outputDirectory.listFilesRecursively() 59 | 60 | /** 61 | * Intermediate source and resource files generated by the annotation processor that will be 62 | * compiled in the next steps. 63 | */ 64 | val sourcesGeneratedByAnnotationProcessor: List = 65 | compilation.kaptSourceDir.listFilesRecursively() + 66 | compilation.kaptKotlinGeneratedDir.listFilesRecursively() 67 | 68 | /** Stub files generated by kapt */ 69 | val generatedStubFiles: List = compilation.kaptStubsDir.listFilesRecursively() 70 | 71 | /** 72 | * The class, resource and intermediate source files generated during the compilation. Does not 73 | * include stub files and kapt incremental data. 74 | */ 75 | val generatedFiles: Collection = 76 | sourcesGeneratedByAnnotationProcessor + compiledClassAndResourceFiles + generatedStubFiles 77 | } 78 | -------------------------------------------------------------------------------- /core/src/main/kotlin/com/tschuchort/compiletesting/DefaultPropertyDelegate.kt: -------------------------------------------------------------------------------- 1 | package com.tschuchort.compiletesting 2 | 3 | import kotlin.properties.ReadWriteProperty 4 | import kotlin.reflect.KProperty 5 | 6 | @Suppress("MemberVisibilityCanBePrivate") 7 | internal class DefaultPropertyDelegate(private val createDefault: () -> T) : ReadWriteProperty { 8 | val hasDefaultValue 9 | @Synchronized get() = (value == DEFAULT) 10 | 11 | private var value: Any? = DEFAULT 12 | val defaultValue by lazy { createDefault() } 13 | 14 | @Synchronized 15 | override operator fun getValue(thisRef: R, property: KProperty<*>): T { 16 | @Suppress("UNCHECKED_CAST") 17 | return if(hasDefaultValue) 18 | defaultValue 19 | else 20 | value as T 21 | } 22 | 23 | @Synchronized 24 | override operator fun setValue(thisRef: R, property: KProperty<*>, value: T) { 25 | this.value = value 26 | } 27 | 28 | companion object { 29 | private object DEFAULT 30 | } 31 | } 32 | 33 | internal fun default(createDefault: () -> T) = DefaultPropertyDelegate(createDefault) -------------------------------------------------------------------------------- /core/src/main/kotlin/com/tschuchort/compiletesting/DiagnosticMessage.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.tschuchort.compiletesting 17 | 18 | import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity 19 | 20 | public enum class DiagnosticSeverity { 21 | ERROR, 22 | WARNING, 23 | INFO, 24 | LOGGING, 25 | } 26 | 27 | /** 28 | * Holder for diagnostics messages 29 | */ 30 | public data class DiagnosticMessage( 31 | val severity: DiagnosticSeverity, 32 | val message: String, 33 | ) 34 | 35 | internal fun CompilerMessageSeverity.toSeverity() = when (this) { 36 | CompilerMessageSeverity.EXCEPTION, 37 | CompilerMessageSeverity.ERROR -> DiagnosticSeverity.ERROR 38 | CompilerMessageSeverity.STRONG_WARNING, 39 | CompilerMessageSeverity.WARNING -> DiagnosticSeverity.WARNING 40 | CompilerMessageSeverity.INFO -> DiagnosticSeverity.INFO 41 | CompilerMessageSeverity.LOGGING, 42 | CompilerMessageSeverity.OUTPUT -> DiagnosticSeverity.LOGGING 43 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/com/tschuchort/compiletesting/DiagnosticsMessageCollector.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.tschuchort.compiletesting 17 | 18 | import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity 19 | import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation 20 | import org.jetbrains.kotlin.cli.common.messages.MessageCollector 21 | 22 | /** 23 | * Custom message collector for Kotlin compilation that collects messages into 24 | * [DiagnosticMessage] objects. 25 | */ 26 | internal class DiagnosticsMessageCollector( 27 | private val stepName: String, 28 | private val verbose: Boolean, 29 | private val diagnostics: MutableList, 30 | ) : MessageCollector { 31 | 32 | override fun clear() { 33 | diagnostics.clear() 34 | } 35 | 36 | /** 37 | * Returns `true` if this collector has any warning messages. 38 | */ 39 | fun hasWarnings() = diagnostics.any { 40 | it.severity == DiagnosticSeverity.WARNING 41 | } 42 | 43 | override fun hasErrors(): Boolean { 44 | return diagnostics.any { 45 | it.severity == DiagnosticSeverity.ERROR 46 | } 47 | } 48 | 49 | override fun report( 50 | severity: CompilerMessageSeverity, 51 | message: String, 52 | location: CompilerMessageSourceLocation? 53 | ) { 54 | if (!verbose && CompilerMessageSeverity.VERBOSE.contains(severity)) return 55 | 56 | val severity = 57 | if (stepName == "kapt" && getJavaVersion() >= 17) { 58 | // Workaround for KT-54030 59 | message.getSeverityFromPrefix() ?: severity.toSeverity() 60 | } else { 61 | severity.toSeverity() 62 | } 63 | doReport(severity, message) 64 | } 65 | 66 | private fun doReport( 67 | severity: DiagnosticSeverity, 68 | message: String, 69 | ) { 70 | if (message == KSP_ADDITIONAL_ERROR_MESSAGE) { 71 | // ignore this as it will impact error counts. 72 | return 73 | } 74 | // Strip kapt/ksp prefixes 75 | val strippedMessage = message.stripPrefixes() 76 | diagnostics.add( 77 | DiagnosticMessage( 78 | severity = severity, 79 | message = strippedMessage, 80 | ) 81 | ) 82 | } 83 | 84 | /** 85 | * Removes prefixes added by kapt / ksp from the message 86 | */ 87 | private fun String.stripPrefixes(): String { 88 | return stripKind().stripKspPrefix() 89 | } 90 | 91 | /** 92 | * KAPT prepends the message kind to the message, we'll remove it here. 93 | */ 94 | private fun String.stripKind(): String { 95 | val firstLine = lineSequence().firstOrNull() ?: return this 96 | val match = KIND_REGEX.find(firstLine) ?: return this 97 | return substring(match.range.last + 1) 98 | } 99 | 100 | /** 101 | * KSP prepends ksp to each message, we'll strip it here. 102 | */ 103 | private fun String.stripKspPrefix(): String { 104 | val firstLine = lineSequence().firstOrNull() ?: return this 105 | val match = KSP_PREFIX_REGEX.find(firstLine) ?: return this 106 | return substring(match.range.last + 1) 107 | } 108 | 109 | private fun String.getSeverityFromPrefix(): DiagnosticSeverity? { 110 | val kindMatch = 111 | // The (\w+) for the kind prefix is is the 4th capture group 112 | KAPT_LOCATION_AND_KIND_REGEX.find(this)?.groupValues?.getOrNull(4) 113 | // The (\w+) is the 1st capture group 114 | ?: KIND_REGEX.find(this)?.groupValues?.getOrNull(1) 115 | ?: return null 116 | return if (kindMatch.equals("error", ignoreCase = true)) { 117 | DiagnosticSeverity.ERROR 118 | } else if (kindMatch.equals("warning", ignoreCase = true)) { 119 | DiagnosticSeverity.WARNING 120 | } else if (kindMatch.equals("note", ignoreCase = true)) { 121 | DiagnosticSeverity.INFO 122 | } else { 123 | null 124 | } 125 | } 126 | 127 | private fun getJavaVersion(): Int = 128 | System.getProperty("java.specification.version")?.substringAfter('.')?.toIntOrNull() ?: 6 129 | companion object { 130 | // example: foo/bar/Subject.kt:2: warning: the real message 131 | private val KAPT_LOCATION_AND_KIND_REGEX = """^(.*\.(kt|java)):(\d+): (\w+): """.toRegex() 132 | // detect things like "Note: " to be stripped from the message. 133 | // We could limit this to known diagnostic kinds (instead of matching \w:) but it is always 134 | // added so not really necessary until we hit a parser bug :) 135 | // example: "error: the real message" 136 | private val KIND_REGEX = """^(\w+): """.toRegex() 137 | // example: "[ksp] the real message" 138 | private val KSP_PREFIX_REGEX = """^\[ksp] """.toRegex() 139 | // KSP always prints an additional error if any other error occurred. 140 | // We drop that additional message to provide a more consistent error count with KAPT/javac. 141 | private const val KSP_ADDITIONAL_ERROR_MESSAGE = 142 | "Error occurred in KSP, check log for detail" 143 | } 144 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/com/tschuchort/compiletesting/HostEnvironment.kt: -------------------------------------------------------------------------------- 1 | package com.tschuchort.compiletesting 2 | 3 | import io.github.classgraph.ClassGraph 4 | import java.io.File 5 | 6 | /** 7 | * Utility object to provide everything we might discover from the host environment. 8 | */ 9 | internal object HostEnvironment { 10 | val classpath by lazy { 11 | getHostClasspaths() 12 | } 13 | 14 | val kotlinStdLibJar: File? by lazy { 15 | findInClasspath(kotlinDependencyRegex("(kotlin-stdlib|kotlin-runtime)")) 16 | } 17 | 18 | val kotlinStdLibCommonJar: File? by lazy { 19 | findInClasspath(kotlinDependencyRegex("kotlin-stdlib-common")) 20 | } 21 | 22 | val kotlinStdLibJdkJar: File? by lazy { 23 | findInClasspath(kotlinDependencyRegex("kotlin-stdlib-jdk[0-9]+")) 24 | } 25 | 26 | val kotlinStdLibJsJar: File? by default { 27 | findInClasspath(kotlinDependencyRegex("kotlin-stdlib-js")) 28 | } 29 | 30 | val kotlinReflectJar: File? by lazy { 31 | findInClasspath(kotlinDependencyRegex("kotlin-reflect")) 32 | } 33 | 34 | val kotlinScriptRuntimeJar: File? by lazy { 35 | findInClasspath(kotlinDependencyRegex("kotlin-script-runtime")) 36 | } 37 | 38 | val toolsJar: File? by lazy { 39 | findInClasspath(Regex("tools.jar")) 40 | } 41 | 42 | private fun kotlinDependencyRegex(prefix: String): Regex { 43 | return Regex("$prefix(-[0-9]+\\.[0-9]+(\\.[0-9]+)?)([-0-9a-zA-Z]+)?\\.jar") 44 | } 45 | 46 | /** Tries to find a file matching the given [regex] in the host process' classpath */ 47 | private fun findInClasspath(regex: Regex): File? { 48 | val jarFile = classpath.firstOrNull { classpath -> 49 | classpath.name.matches(regex) 50 | //TODO("check that jar file actually contains the right classes") 51 | } 52 | return jarFile 53 | } 54 | 55 | /** Returns the files on the classloader's classpath and modulepath */ 56 | private fun getHostClasspaths(): List { 57 | val classGraph = ClassGraph() 58 | .enableSystemJarsAndModules() 59 | .removeTemporaryFilesAfterScan() 60 | 61 | val classpaths = classGraph.classpathFiles 62 | val modules = classGraph.modules.mapNotNull { it.locationFile } 63 | 64 | return (classpaths + modules).distinctBy(File::getAbsolutePath) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /core/src/main/kotlin/com/tschuchort/compiletesting/JavacUtils.kt: -------------------------------------------------------------------------------- 1 | package com.tschuchort.compiletesting 2 | 3 | import java.io.ByteArrayInputStream 4 | import java.io.File 5 | import java.io.InputStream 6 | import java.io.Reader 7 | import java.io.StringReader 8 | import java.net.URI 9 | import java.nio.charset.Charset 10 | import javax.tools.JavaCompiler 11 | import javax.tools.JavaFileObject 12 | import javax.tools.SimpleJavaFileObject 13 | import okio.Buffer 14 | 15 | /** 16 | * A [JavaFileObject] created from a source [File]. 17 | * 18 | * Used for interfacing with javac ([JavaCompiler]). 19 | */ 20 | internal class FileJavaFileObject(val sourceFile: File, val charset: Charset = Charset.defaultCharset()) 21 | : SimpleJavaFileObject(sourceFile.toURI(), 22 | deduceKind(sourceFile.toURI()) 23 | ) { 24 | 25 | init { 26 | require(sourceFile.isFile) 27 | require(sourceFile.canRead()) 28 | } 29 | 30 | companion object { 31 | private fun deduceKind(uri: URI): JavaFileObject.Kind 32 | = JavaFileObject.Kind.values().firstOrNull { 33 | uri.path.endsWith(it.extension, ignoreCase = true) 34 | } ?: JavaFileObject.Kind.OTHER 35 | } 36 | 37 | override fun openInputStream(): InputStream = sourceFile.inputStream() 38 | 39 | override fun getCharContent(ignoreEncodingErrors: Boolean): CharSequence 40 | = sourceFile.readText(charset) 41 | } 42 | 43 | /** 44 | * A [JavaFileObject] created from a [String]. 45 | * 46 | * Used for interfacing with javac ([JavaCompiler]). 47 | */ 48 | internal class StringJavaFileObject(className: String, private val contents: String) 49 | : SimpleJavaFileObject( 50 | URI.create("string:///" + className.replace('.', '/') + JavaFileObject.Kind.SOURCE.extension), 51 | JavaFileObject.Kind.SOURCE 52 | ){ 53 | private var _lastModified = System.currentTimeMillis() 54 | 55 | override fun getCharContent(ignoreEncodingErrors: Boolean): CharSequence = contents 56 | 57 | override fun openInputStream(): InputStream 58 | = ByteArrayInputStream(contents.toByteArray(Charset.defaultCharset())) 59 | 60 | override fun openReader(ignoreEncodingErrors: Boolean): Reader = StringReader(contents) 61 | 62 | override fun getLastModified(): Long = _lastModified 63 | } 64 | 65 | /** 66 | * Gets the version string of a javac executable that can be started using the 67 | * given [javacCommand] via [Runtime.exec]. 68 | */ 69 | internal fun getJavacVersionString(javacCommand: String): String { 70 | val javacProc = ProcessBuilder(listOf(javacCommand, "-version")) 71 | .redirectErrorStream(true) 72 | .start() 73 | 74 | val buffer = Buffer() 75 | 76 | javacProc.inputStream.copyTo(buffer.outputStream()) 77 | javacProc.waitFor() 78 | 79 | val output = buffer.readUtf8() 80 | 81 | return parseVersionString(output) ?: throw IllegalStateException( 82 | "Command '$javacCommand -version' did not print expected output. " + 83 | "Output was: '$output'" 84 | ) 85 | } 86 | 87 | internal fun parseVersionString(output: String) = 88 | Regex("javac (.*)?[\\s\\S]*").find(output)?.destructured?.component1() 89 | 90 | internal fun isJavac9OrLater(javacVersionString: String): Boolean { 91 | try { 92 | val (majorv, minorv, patchv, otherv) = Regex("([0-9]*)(?:\\.([0-9]*))?(?:\\.([0-9]*))?(.*)") 93 | .matchEntire(javacVersionString)?.destructured 94 | ?: throw IllegalArgumentException("Could not match version regex") 95 | 96 | check(majorv.isNotBlank()) { "Major version can not be blank" } 97 | 98 | if (majorv.toInt() == 1) 99 | check(minorv.isNotBlank()) { "Minor version can not be blank if major version is 1" } 100 | 101 | return (majorv.toInt() == 1 && minorv.toInt() >= 9) // old versioning scheme: 1.8.x 102 | || (majorv.toInt() >= 9) // new versioning scheme: 10.x.x 103 | } 104 | catch (t: Throwable) { 105 | throw IllegalArgumentException("Could not parse javac version string: '$javacVersionString'", t) 106 | } 107 | } 108 | 109 | /** Finds the tools.jar given a path to a JDK 8 or earlier */ 110 | internal fun findToolsJarFromJdk(jdkHome: File): File { 111 | return jdkHome.resolve("../lib/tools.jar").existsOrNull() 112 | ?: jdkHome.resolve("lib/tools.jar").existsOrNull() 113 | ?: jdkHome.resolve("tools.jar").existsOrNull() 114 | ?: throw IllegalStateException("Can not find tools.jar from JDK with path ${jdkHome.absolutePath}") 115 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/com/tschuchort/compiletesting/KaptComponentRegistrar.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2016 JetBrains s.r.o. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tschuchort.compiletesting 18 | 19 | import java.io.File 20 | import org.jetbrains.kotlin.analyzer.AnalysisResult 21 | import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys 22 | import org.jetbrains.kotlin.cli.common.messages.MessageRenderer 23 | import org.jetbrains.kotlin.cli.common.messages.PrintingMessageCollector 24 | import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot 25 | import org.jetbrains.kotlin.cli.jvm.config.JvmClasspathRoot 26 | import org.jetbrains.kotlin.com.intellij.mock.MockProject 27 | import org.jetbrains.kotlin.com.intellij.openapi.project.Project 28 | import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar 29 | import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi 30 | import org.jetbrains.kotlin.config.CommonConfigurationKeys 31 | import org.jetbrains.kotlin.config.CompilerConfiguration 32 | import org.jetbrains.kotlin.config.JVMConfigurationKeys 33 | import org.jetbrains.kotlin.container.ComponentProvider 34 | import org.jetbrains.kotlin.context.ProjectContext 35 | import org.jetbrains.kotlin.descriptors.ModuleDescriptor 36 | import org.jetbrains.kotlin.extensions.StorageComponentContainerContributor 37 | import org.jetbrains.kotlin.kapt3.AbstractKapt3Extension 38 | import org.jetbrains.kotlin.kapt3.Kapt3ComponentRegistrar 39 | import org.jetbrains.kotlin.kapt3.base.AptMode 40 | import org.jetbrains.kotlin.kapt3.base.Kapt 41 | import org.jetbrains.kotlin.kapt3.base.KaptFlag 42 | import org.jetbrains.kotlin.kapt3.base.KaptOptions 43 | import org.jetbrains.kotlin.kapt3.base.LoadedProcessors 44 | import org.jetbrains.kotlin.kapt3.base.incremental.IncrementalProcessor 45 | import org.jetbrains.kotlin.kapt3.base.logString 46 | import org.jetbrains.kotlin.kapt3.base.util.KaptLogger 47 | import org.jetbrains.kotlin.kapt3.util.MessageCollectorBackedKaptLogger 48 | import org.jetbrains.kotlin.psi.KtFile 49 | import org.jetbrains.kotlin.resolve.BindingTrace 50 | import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension 51 | 52 | @ExperimentalCompilerApi 53 | internal class KaptComponentRegistrar( 54 | private val processors: List, 55 | private val kaptOptions: KaptOptions.Builder 56 | ) : ComponentRegistrar { 57 | 58 | override fun registerProjectComponents(project: MockProject, configuration: CompilerConfiguration) { 59 | if (processors.isEmpty()) 60 | return 61 | 62 | val contentRoots = configuration[CLIConfigurationKeys.CONTENT_ROOTS] ?: emptyList() 63 | 64 | val optionsBuilder = kaptOptions.apply { 65 | projectBaseDir = project.basePath?.let(::File) 66 | compileClasspath.addAll(contentRoots.filterIsInstance().map { it.file }) 67 | javaSourceRoots.addAll(contentRoots.filterIsInstance().map { it.file }) 68 | classesOutputDir = classesOutputDir ?: configuration.get(JVMConfigurationKeys.OUTPUT_DIRECTORY) 69 | } 70 | 71 | val messageCollector = configuration.get(CLIConfigurationKeys.MESSAGE_COLLECTOR_KEY) 72 | ?: PrintingMessageCollector(System.err, MessageRenderer.PLAIN_FULL_PATHS, optionsBuilder.flags.contains(KaptFlag.VERBOSE)) 73 | 74 | val logger = MessageCollectorBackedKaptLogger( 75 | optionsBuilder.flags.contains(KaptFlag.VERBOSE), 76 | optionsBuilder.flags.contains(KaptFlag.INFO_AS_WARNINGS), 77 | messageCollector 78 | ) 79 | 80 | if (!optionsBuilder.checkOptions(project, logger, configuration)) { 81 | return 82 | } 83 | 84 | val options = optionsBuilder.build() 85 | 86 | options.sourcesOutputDir.mkdirs() 87 | 88 | if (options[KaptFlag.VERBOSE]) { 89 | logger.info(options.logString()) 90 | } 91 | 92 | val kapt3AnalysisCompletedHandlerExtension = 93 | object : AbstractKapt3Extension(options, logger, configuration) { 94 | override fun loadProcessors() = LoadedProcessors( 95 | processors = processors, 96 | classLoader = this::class.java.classLoader 97 | ) 98 | } 99 | 100 | AnalysisHandlerExtension.registerExtension(project, kapt3AnalysisCompletedHandlerExtension) 101 | StorageComponentContainerContributor.registerExtension( 102 | project = project, 103 | extension = Kapt3ComponentRegistrar.KaptComponentContributor(kapt3AnalysisCompletedHandlerExtension) 104 | ) 105 | } 106 | 107 | private fun KaptOptions.Builder.checkOptions(project: MockProject, logger: KaptLogger, configuration: CompilerConfiguration): Boolean { 108 | fun abortAnalysis() = AnalysisHandlerExtension.registerExtension(project, AbortAnalysisHandlerExtension()) 109 | 110 | if (classesOutputDir == null) { 111 | if (configuration.get(JVMConfigurationKeys.OUTPUT_JAR) != null) { 112 | logger.error("Kapt does not support specifying JAR file outputs. Please specify the classes output directory explicitly.") 113 | abortAnalysis() 114 | return false 115 | } 116 | else { 117 | classesOutputDir = configuration.get(JVMConfigurationKeys.OUTPUT_DIRECTORY) 118 | } 119 | } 120 | 121 | if (sourcesOutputDir == null || classesOutputDir == null || stubsOutputDir == null) { 122 | if (mode != AptMode.WITH_COMPILATION) { 123 | val nonExistentOptionName = when { 124 | sourcesOutputDir == null -> "Sources output directory" 125 | classesOutputDir == null -> "Classes output directory" 126 | stubsOutputDir == null -> "Stubs output directory" 127 | else -> throw IllegalStateException() 128 | } 129 | val moduleName = configuration.get(CommonConfigurationKeys.MODULE_NAME) 130 | ?: configuration.get(JVMConfigurationKeys.MODULES).orEmpty().joinToString() 131 | 132 | logger.warn("$nonExistentOptionName is not specified for $moduleName, skipping annotation processing") 133 | abortAnalysis() 134 | } 135 | return false 136 | } 137 | 138 | if (!Kapt.checkJavacComponentsAccess(logger)) { 139 | abortAnalysis() 140 | return false 141 | } 142 | 143 | return true 144 | } 145 | 146 | /* This extension simply disables both code analysis and code generation. 147 | * When aptOnly is true, and any of required kapt options was not passed, we just abort compilation by providing this extension. 148 | * */ 149 | private class AbortAnalysisHandlerExtension : AnalysisHandlerExtension { 150 | override fun doAnalysis( 151 | project: Project, 152 | module: ModuleDescriptor, 153 | projectContext: ProjectContext, 154 | files: Collection, 155 | bindingTrace: BindingTrace, 156 | componentProvider: ComponentProvider 157 | ): AnalysisResult? { 158 | return AnalysisResult.success(bindingTrace.bindingContext, module, shouldGenerateCode = false) 159 | } 160 | 161 | override fun analysisCompleted( 162 | project: Project, 163 | module: ModuleDescriptor, 164 | bindingTrace: BindingTrace, 165 | files: Collection 166 | ): AnalysisResult? { 167 | return AnalysisResult.success(bindingTrace.bindingContext, module, shouldGenerateCode = false) 168 | } 169 | } 170 | 171 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/com/tschuchort/compiletesting/KotlinCompilation.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2018 Square, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.tschuchort.compiletesting 18 | 19 | import com.facebook.buck.jvm.java.javax.SynchronizedToolProvider 20 | import com.tschuchort.compiletesting.kapt.toPluginOptions 21 | import java.io.File 22 | import java.io.OutputStreamWriter 23 | import java.nio.file.Path 24 | import javax.annotation.processing.Processor 25 | import javax.tools.Diagnostic 26 | import javax.tools.DiagnosticCollector 27 | import javax.tools.JavaFileObject 28 | import org.jetbrains.kotlin.cli.common.arguments.K2JVMCompilerArguments 29 | import org.jetbrains.kotlin.cli.jvm.K2JVMCompiler 30 | import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi 31 | import org.jetbrains.kotlin.config.JVMAssertionsMode 32 | import org.jetbrains.kotlin.config.JvmDefaultMode 33 | import org.jetbrains.kotlin.config.JvmTarget 34 | import org.jetbrains.kotlin.config.Services 35 | import org.jetbrains.kotlin.kapt3.base.AptMode 36 | import org.jetbrains.kotlin.kapt3.base.KaptFlag 37 | import org.jetbrains.kotlin.kapt3.base.KaptOptions 38 | import org.jetbrains.kotlin.kapt3.base.incremental.DeclaredProcType 39 | import org.jetbrains.kotlin.kapt3.base.incremental.IncrementalProcessor 40 | import org.jetbrains.kotlin.kapt3.util.MessageCollectorBackedKaptLogger 41 | import org.jetbrains.kotlin.kapt4.Kapt4CompilerPluginRegistrar 42 | 43 | data class PluginOption( 44 | val pluginId: PluginId, 45 | val optionName: OptionName, 46 | val optionValue: OptionValue, 47 | ) 48 | 49 | typealias PluginId = String 50 | 51 | typealias OptionName = String 52 | 53 | typealias OptionValue = String 54 | 55 | @ExperimentalCompilerApi 56 | @Suppress("MemberVisibilityCanBePrivate") 57 | class KotlinCompilation : AbstractKotlinCompilation() { 58 | /** Arbitrary arguments to be passed to kapt */ 59 | var kaptArgs: MutableMap = mutableMapOf() 60 | 61 | /** Arbitrary flags to be passed to kapt */ 62 | var kaptFlags: MutableSet = mutableSetOf() 63 | 64 | /** Enables the new Kapt 4 impl for K2 support. */ 65 | var useKapt4: Boolean? = null 66 | 67 | /** Annotation processors to be passed to kapt */ 68 | var annotationProcessors: List = emptyList() 69 | 70 | /** Include Kotlin runtime in to resulting .jar */ 71 | var includeRuntime: Boolean = false 72 | 73 | /** Make kapt correct error types */ 74 | var correctErrorTypes: Boolean = true 75 | 76 | /** Name of the generated .kotlin_module file */ 77 | var moduleName: String? = null 78 | 79 | /** Target version of the generated JVM bytecode */ 80 | var jvmTarget: String = JvmTarget.DEFAULT.description 81 | 82 | /** Generate metadata for Java 1.8 reflection on method parameters */ 83 | var javaParameters: Boolean = false 84 | 85 | /** Paths where to find Java 9+ modules */ 86 | var javaModulePath: Path? = null 87 | 88 | /** 89 | * Root modules to resolve in addition to the initial modules, or all modules on the module path 90 | * if is ALL-MODULE-PATH 91 | */ 92 | var additionalJavaModules: MutableList = mutableListOf() 93 | 94 | /** Don't generate not-null assertions for arguments of platform types */ 95 | var noCallAssertions: Boolean = false 96 | 97 | /** Don't generate not-null assertion for extension receiver arguments of platform types */ 98 | var noReceiverAssertions: Boolean = false 99 | 100 | /** Don't generate not-null assertions on parameters of methods accessible from Java */ 101 | var noParamAssertions: Boolean = false 102 | 103 | /** Generate nullability assertions for non-null Java expressions */ 104 | @Deprecated("Removed in Kotlinc, this does nothing now.") 105 | var strictJavaNullabilityAssertions: Boolean? = null 106 | 107 | /** Disable optimizations */ 108 | var noOptimize: Boolean = false 109 | 110 | /** 111 | * Normalize constructor calls (disable: don't normalize; enable: normalize), default is 'disable' 112 | * in language version 1.2 and below, 'enable' since language version 1.3 113 | * 114 | * {disable|enable} 115 | */ 116 | @Deprecated("Removed in Kotlinc, this does nothing now.") 117 | var constructorCallNormalizationMode: String? = null 118 | 119 | /** Assert calls behaviour {always-enable|always-disable|jvm|legacy} */ 120 | var assertionsMode: String? = JVMAssertionsMode.DEFAULT.description 121 | 122 | /** Path to the .xml build file to compile */ 123 | var buildFile: File? = null 124 | 125 | /** Compile multifile classes as a hierarchy of parts and facade */ 126 | var inheritMultifileParts: Boolean = false 127 | 128 | /** Use type table in metadata serialization */ 129 | var useTypeTable: Boolean = false 130 | 131 | /** Allow Kotlin runtime libraries of incompatible versions in the classpath */ 132 | @Deprecated("Removed in Kotlinc, this does nothing now.") 133 | var skipRuntimeVersionCheck: Boolean? = null 134 | 135 | /** Combine modules for source files and binary dependencies into a single module */ 136 | @Deprecated("Removed in Kotlinc, this does nothing now.") var singleModule: Boolean = false 137 | 138 | /** Suppress the \"cannot access built-in declaration\" error (useful with -no-stdlib) */ 139 | var suppressMissingBuiltinsError: Boolean = false 140 | 141 | /** Script resolver environment in key-value pairs (the value could be quoted and escaped) */ 142 | var scriptResolverEnvironment: MutableMap = mutableMapOf() 143 | 144 | /** Java compiler arguments */ 145 | var javacArguments: MutableList = mutableListOf() 146 | 147 | /** Package prefix for Java files */ 148 | var javaPackagePrefix: String? = null 149 | 150 | /** 151 | * Specify behavior for Checker Framework compatqual annotations (NullableDecl/NonNullDecl). 152 | * Default value is 'enable' 153 | */ 154 | var supportCompatqualCheckerFrameworkAnnotations: String? = null 155 | 156 | /** 157 | * Do not throw NPE on explicit 'equals' call for null receiver of platform boxed primitive type 158 | */ 159 | @Deprecated("Removed in Kotlinc, this does nothing now.") 160 | var noExceptionOnExplicitEqualsForBoxedNull: Boolean? = null 161 | 162 | /** 163 | * Allow to use '@JvmDefault' annotation for JVM default method support. 164 | * {disable|enable|compatibility} 165 | */ 166 | var jvmDefault: String = JvmDefaultMode.DISABLE.description 167 | 168 | /** Generate metadata with strict version semantics (see kdoc on Metadata.extraInt) */ 169 | var strictMetadataVersionSemantics: Boolean = false 170 | 171 | /** 172 | * Transform '(' and ')' in method names to some other character sequence. This mode can BREAK 173 | * BINARY COMPATIBILITY and is only supposed to be used as a workaround of an issue in the ASM 174 | * bytecode framework. See KT-29475 for more details 175 | */ 176 | var sanitizeParentheses: Boolean = false 177 | 178 | /** Paths to output directories for friend modules (whose internals should be visible) */ 179 | var friendPaths: List = emptyList() 180 | 181 | /** 182 | * Path to the JDK to be used 183 | * 184 | * If null, no JDK will be used with kotlinc (option -no-jdk) and the system java compiler will be 185 | * used with empty bootclasspath (on JDK8) or --system none (on JDK9+). This can be useful if all 186 | * the JDK classes you need are already on the (inherited) classpath. 187 | */ 188 | var jdkHome: File? by default { processJdkHome } 189 | 190 | /** 191 | * Path to the kotlin-stdlib.jar If none is given, it will be searched for in the host process' 192 | * classpaths 193 | */ 194 | var kotlinStdLibJar: File? by default { HostEnvironment.kotlinStdLibJar } 195 | 196 | /** 197 | * Path to the kotlin-stdlib-jdk*.jar If none is given, it will be searched for in the host 198 | * process' classpaths 199 | */ 200 | var kotlinStdLibJdkJar: File? by default { HostEnvironment.kotlinStdLibJdkJar } 201 | 202 | /** 203 | * Path to the kotlin-reflect.jar If none is given, it will be searched for in the host process' 204 | * classpaths 205 | */ 206 | var kotlinReflectJar: File? by default { HostEnvironment.kotlinReflectJar } 207 | 208 | /** 209 | * Path to the kotlin-script-runtime.jar If none is given, it will be searched for in the host 210 | * process' classpaths 211 | */ 212 | var kotlinScriptRuntimeJar: File? by default { HostEnvironment.kotlinScriptRuntimeJar } 213 | 214 | /** 215 | * Path to the tools.jar file needed for kapt when using a JDK 8. 216 | * 217 | * Note: Using a tools.jar file with a JDK 9 or later leads to an internal compiler error! 218 | */ 219 | var toolsJar: File? by default { 220 | if (!isJdk9OrLater()) jdkHome?.let { findToolsJarFromJdk(it) } ?: HostEnvironment.toolsJar 221 | else null 222 | } 223 | 224 | // *.class files, Jars and resources (non-temporary) that are created by the 225 | // compilation will land here 226 | val classesDir 227 | get() = workingDir.resolve("classes") 228 | 229 | // Base directory for kapt stuff 230 | private val kaptBaseDir 231 | get() = workingDir.resolve("kapt") 232 | 233 | // Java annotation processors that are compile by kapt will put their generated files here 234 | val kaptSourceDir 235 | get() = kaptBaseDir.resolve("sources") 236 | 237 | // Output directory for Kotlin source files generated by kapt 238 | val kaptKotlinGeneratedDir 239 | get() = 240 | kaptArgs[OPTION_KAPT_KOTLIN_GENERATED]?.let { path -> 241 | require(File(path).isDirectory) { "$OPTION_KAPT_KOTLIN_GENERATED must be a directory" } 242 | File(path) 243 | } ?: File(kaptBaseDir, "kotlinGenerated") 244 | 245 | val kaptStubsDir 246 | get() = kaptBaseDir.resolve("stubs") 247 | 248 | val kaptIncrementalDataDir 249 | get() = kaptBaseDir.resolve("incrementalData") 250 | 251 | /** ExitCode of the entire Kotlin compilation process */ 252 | enum class ExitCode { 253 | OK, 254 | INTERNAL_ERROR, 255 | COMPILATION_ERROR, 256 | SCRIPT_EXECUTION_ERROR 257 | } 258 | 259 | private fun useKapt4(): Boolean { 260 | return (useKapt4 ?: languageVersion?.startsWith("2")) == true 261 | } 262 | 263 | // setup common arguments for the two kotlinc calls 264 | private fun commonK2JVMArgs() = 265 | commonArguments(K2JVMCompilerArguments()) { args -> 266 | args.destination = classesDir.absolutePath 267 | args.classpath = commonClasspaths().joinToString(separator = File.pathSeparator) 268 | 269 | if (jdkHome != null) { 270 | args.jdkHome = jdkHome!!.absolutePath 271 | } else { 272 | log("Using option -no-jdk. Kotlinc won't look for a JDK.") 273 | args.noJdk = true 274 | } 275 | 276 | args.includeRuntime = includeRuntime 277 | 278 | // the compiler should never look for stdlib or reflect in the 279 | // kotlinHome directory (which is null anyway). We will put them 280 | // in the classpath manually if they're needed 281 | args.noStdlib = true 282 | args.noReflect = true 283 | 284 | if (moduleName != null) args.moduleName = moduleName 285 | 286 | args.jvmTarget = jvmTarget 287 | args.javaParameters = javaParameters 288 | 289 | if (javaModulePath != null) args.javaModulePath = javaModulePath!!.toString() 290 | 291 | args.additionalJavaModules = additionalJavaModules.map(File::getAbsolutePath).toTypedArray() 292 | args.noCallAssertions = noCallAssertions 293 | args.noParamAssertions = noParamAssertions 294 | args.noReceiverAssertions = noReceiverAssertions 295 | 296 | args.noOptimize = noOptimize 297 | 298 | if (assertionsMode != null) args.assertionsMode = assertionsMode 299 | 300 | if (buildFile != null) args.buildFile = buildFile!!.toString() 301 | 302 | args.inheritMultifileParts = inheritMultifileParts 303 | args.useTypeTable = useTypeTable 304 | 305 | if (javacArguments.isNotEmpty()) args.javacArguments = javacArguments.toTypedArray() 306 | 307 | if (supportCompatqualCheckerFrameworkAnnotations != null) 308 | args.supportCompatqualCheckerFrameworkAnnotations = 309 | supportCompatqualCheckerFrameworkAnnotations 310 | 311 | args.jvmDefault = jvmDefault 312 | args.strictMetadataVersionSemantics = strictMetadataVersionSemantics 313 | args.sanitizeParentheses = sanitizeParentheses 314 | 315 | if (friendPaths.isNotEmpty()) 316 | args.friendPaths = friendPaths.map(File::getAbsolutePath).toTypedArray() 317 | 318 | if (scriptResolverEnvironment.isNotEmpty()) 319 | args.scriptResolverEnvironment = 320 | scriptResolverEnvironment.map { (key, value) -> "$key=\"$value\"" }.toTypedArray() 321 | 322 | args.javaPackagePrefix = javaPackagePrefix 323 | args.suppressMissingBuiltinsError = suppressMissingBuiltinsError 324 | args.disableStandardScript = disableStandardScript 325 | } 326 | 327 | /** Performs the 1st and 2nd compilation step to generate stubs and run annotation processors */ 328 | private fun stubsAndApt(sourceFiles: List): ExitCode { 329 | if (annotationProcessors.isEmpty()) { 330 | log("No services were given. Not running kapt steps.") 331 | return ExitCode.OK 332 | } 333 | 334 | val kaptOptions = 335 | KaptOptions.Builder().also { 336 | it.stubsOutputDir = kaptStubsDir 337 | it.sourcesOutputDir = kaptSourceDir 338 | it.incrementalDataOutputDir = kaptIncrementalDataDir 339 | it.classesOutputDir = classesDir 340 | it.processingOptions.apply { 341 | putAll(kaptArgs) 342 | putIfAbsent(OPTION_KAPT_KOTLIN_GENERATED, kaptKotlinGeneratedDir.absolutePath) 343 | } 344 | 345 | it.mode = AptMode.STUBS_AND_APT 346 | 347 | it.flags.apply { 348 | addAll(kaptFlags) 349 | 350 | if (verbose) { 351 | addAll(KaptFlag.MAP_DIAGNOSTIC_LOCATIONS, KaptFlag.VERBOSE) 352 | } 353 | } 354 | } 355 | 356 | val compilerMessageCollector = createMessageCollector("kapt") 357 | 358 | val kaptLogger = MessageCollectorBackedKaptLogger(kaptOptions.build(), compilerMessageCollector) 359 | 360 | /* 361 | * The main compiler plugin (MainComponentRegistrar) 362 | * is instantiated by K2JVMCompiler using 363 | * a service locator. So we can't just pass parameters to it easily. 364 | * Instead, we need to use a thread-local global variable to pass 365 | * any parameters that change between compilations 366 | */ 367 | MainComponentRegistrar.threadLocalParameters.set( 368 | MainComponentRegistrar.ThreadLocalParameters( 369 | annotationProcessors.map { 370 | IncrementalProcessor(it, DeclaredProcType.NON_INCREMENTAL, kaptLogger) 371 | }, 372 | kaptOptions, 373 | componentRegistrars, 374 | compilerPluginRegistrars, 375 | supportsK2, 376 | ) 377 | ) 378 | 379 | val kotlinSources = sourceFiles.filter(File::hasKotlinFileExtension) 380 | val javaSources = sourceFiles.filter(File::hasJavaFileExtension) 381 | 382 | val sourcePaths = javaSources.plus(kotlinSources).map(File::getAbsolutePath).distinct() 383 | 384 | if (pluginClasspaths.isNotEmpty()) { 385 | warn("Included plugins in pluginsClasspaths will be executed twice.") 386 | } 387 | 388 | val isK2 = useKapt4() 389 | if (isK2) { 390 | this.compilerPluginRegistrars += Kapt4CompilerPluginRegistrar() 391 | this.kotlincArguments += kaptOptions.toPluginOptions() 392 | } 393 | 394 | val k2JvmArgs = 395 | commonK2JVMArgs().also { 396 | it.freeArgs = sourcePaths 397 | it.pluginClasspaths = (it.pluginClasspaths ?: emptyArray()) + arrayOf(getResourcesPath()) 398 | if (kotlinSources.isEmpty()) { 399 | it.allowNoSourceFiles = true 400 | } 401 | } 402 | 403 | return convertKotlinExitCode( 404 | K2JVMCompiler().exec(compilerMessageCollector, Services.EMPTY, k2JvmArgs) 405 | ) 406 | } 407 | 408 | /** Performs the 3rd compilation step to compile Kotlin source files */ 409 | private fun compileJvmKotlin(sourceFiles: List): ExitCode { 410 | val sources = 411 | sourceFiles + 412 | kaptKotlinGeneratedDir.listFilesRecursively() + 413 | kaptSourceDir.listFilesRecursively() 414 | 415 | return compileKotlin(sources, K2JVMCompiler(), commonK2JVMArgs()) 416 | } 417 | 418 | /** 419 | * Base javac arguments that only depend on the arguments given by the user Depending on which 420 | * compiler implementation is actually used, more arguments may be added 421 | */ 422 | private fun baseJavacArgs(isJavac9OrLater: Boolean) = 423 | mutableListOf().apply { 424 | if (verbose) { 425 | add("-verbose") 426 | add("-Xlint:path") // warn about invalid paths in CLI 427 | add("-Xlint:options") // warn about invalid options in CLI 428 | 429 | if (isJavac9OrLater) add("-Xlint:module") // warn about issues with the module system 430 | } 431 | 432 | addAll("-d", classesDir.absolutePath) 433 | 434 | add("-proc:none") // disable annotation processing 435 | 436 | if (allWarningsAsErrors) add("-Werror") 437 | 438 | addAll(javacArguments) 439 | 440 | // also add class output path to javac classpath so it can discover 441 | // already compiled Kotlin classes 442 | addAll( 443 | "-cp", 444 | (commonClasspaths() + classesDir).joinToString( 445 | File.pathSeparator, 446 | transform = File::getAbsolutePath, 447 | ), 448 | ) 449 | } 450 | 451 | /** Performs the 4th compilation step to compile Java source files */ 452 | private fun compileJava(sourceFiles: List): ExitCode { 453 | val javaSources = 454 | sourceFiles 455 | .plus(kaptSourceDir.listFilesRecursively()) 456 | .plus( 457 | extraGeneratedSources 458 | .flatMap(File::listFilesRecursively) 459 | .filter(File::hasJavaFileExtension) 460 | ) 461 | .distinct() 462 | .filterNot(File::hasKotlinFileExtension) 463 | 464 | if (javaSources.isEmpty()) return ExitCode.OK 465 | 466 | if (jdkHome != null && jdkHome!!.canonicalPath != processJdkHome.canonicalPath) { 467 | /* If a JDK home is given, try to run javac from there so it uses the same JDK 468 | as K2JVMCompiler. Changing the JDK of the system java compiler via the 469 | "--system" and "-bootclasspath" options is not so easy. 470 | If the jdkHome is the same as the current process, we still run an in process compilation because it is 471 | expensive to fork a process to compile. 472 | */ 473 | log("compiling java in a sub-process because a jdkHome is specified") 474 | val jdkBinFile = File(jdkHome, "bin") 475 | check(jdkBinFile.exists()) { "No JDK bin folder found at: ${jdkBinFile.toPath()}" } 476 | 477 | val javacCommand = jdkBinFile.absolutePath + File.separatorChar + "javac" 478 | 479 | val isJavac9OrLater = isJavac9OrLater(getJavacVersionString(javacCommand)) 480 | val javacArgs = baseJavacArgs(isJavac9OrLater) 481 | 482 | val javacProc = 483 | ProcessBuilder(listOf(javacCommand) + javacArgs + javaSources.map(File::getAbsolutePath)) 484 | .directory(workingDir) 485 | .redirectErrorStream(true) 486 | .start() 487 | 488 | javacProc.inputStream.copyTo(internalMessageStream) 489 | javacProc.errorStream.copyTo(internalMessageStream) 490 | 491 | return when (javacProc.waitFor()) { 492 | 0 -> ExitCode.OK 493 | 1 -> ExitCode.COMPILATION_ERROR 494 | else -> ExitCode.INTERNAL_ERROR 495 | } 496 | } else { 497 | /* If no JDK is given, we will use the host process' system java compiler. 498 | If it is set to `null`, we will erase the bootclasspath. The user is then on their own to somehow 499 | provide the JDK classes via the regular classpath because javac won't 500 | work at all without them */ 501 | log("jdkHome is not specified. Using system java compiler of the host process.") 502 | val isJavac9OrLater = isJdk9OrLater() 503 | val javacArgs = 504 | baseJavacArgs(isJavac9OrLater).apply { 505 | if (jdkHome == null) { 506 | log("jdkHome is set to null, removing boot classpath from java compilation") 507 | // erase bootclasspath or JDK path because no JDK was specified 508 | if (isJavac9OrLater) addAll("--system", "none") else addAll("-bootclasspath", "") 509 | } 510 | } 511 | 512 | val javac = SynchronizedToolProvider.systemJavaCompiler 513 | val javaFileManager = javac.getStandardFileManager(null, null, null) 514 | val diagnosticCollector = DiagnosticCollector() 515 | 516 | fun printDiagnostics() = 517 | diagnosticCollector.diagnostics.forEach { diag -> 518 | // Print toString() for these to get the full error message 519 | when (diag.kind) { 520 | Diagnostic.Kind.ERROR -> error(diag.toString()) 521 | Diagnostic.Kind.WARNING, 522 | Diagnostic.Kind.MANDATORY_WARNING -> warn(diag.toString()) 523 | else -> log(diag.toString()) 524 | } 525 | } 526 | 527 | try { 528 | val noErrors = 529 | javac 530 | .getTask( 531 | OutputStreamWriter(internalMessageStream), 532 | javaFileManager, 533 | diagnosticCollector, 534 | javacArgs, 535 | /* classes to be annotation processed */ null, 536 | javaSources 537 | .map { FileJavaFileObject(it) } 538 | .filter { it.kind == JavaFileObject.Kind.SOURCE }, 539 | ) 540 | .call() 541 | 542 | printDiagnostics() 543 | 544 | return if (noErrors) ExitCode.OK else ExitCode.COMPILATION_ERROR 545 | } catch (e: Exception) { 546 | if (e is RuntimeException) { 547 | printDiagnostics() 548 | error(e.toString()) 549 | return ExitCode.INTERNAL_ERROR 550 | } else throw e 551 | } 552 | } 553 | } 554 | 555 | /** Runs the compilation task */ 556 | fun compile(): JvmCompilationResult { 557 | // make sure all needed directories exist 558 | sourcesDir.deleteRecursively() 559 | sourcesDir.mkdirs() 560 | classesDir.mkdirs() 561 | kaptSourceDir.mkdirs() 562 | kaptStubsDir.mkdirs() 563 | kaptIncrementalDataDir.mkdirs() 564 | kaptKotlinGeneratedDir.mkdirs() 565 | 566 | // write given sources to working directory 567 | val sourceFiles = sources.map { it.writeTo(sourcesDir) } 568 | 569 | pluginClasspaths.forEach { filepath -> 570 | if (!filepath.exists()) { 571 | error("Plugin $filepath not found") 572 | return makeResult(ExitCode.INTERNAL_ERROR) 573 | } 574 | } 575 | 576 | /* 577 | There are 4 steps to the compilation process: 578 | 1. Generate stubs (using kotlinc with kapt plugin which does no further compilation) 579 | 2. Run apt (using kotlinc with kapt plugin which does no further compilation) 580 | 3. Run kotlinc with the normal Kotlin sources and Kotlin sources generated in step 2 581 | 4. Run javac with Java sources and the compiled Kotlin classes 582 | */ 583 | 584 | /* Work around for warning that sometimes happens: 585 | "Failed to initialize native filesystem for Windows 586 | java.lang.RuntimeException: Could not find installation home path. 587 | Please make sure bin/idea.properties is present in the installation directory" 588 | See: https://github.com/arturbosch/detekt/issues/630 589 | */ 590 | withSystemProperty("idea.use.native.fs.for.win", "false") { 591 | // step 1 and 2: generate stubs and run annotation processors 592 | try { 593 | val exitCode = stubsAndApt(sourceFiles) 594 | if (exitCode != ExitCode.OK) { 595 | return makeResult(exitCode) 596 | } 597 | } finally { 598 | MainComponentRegistrar.threadLocalParameters.remove() 599 | } 600 | 601 | // step 3: compile Kotlin files 602 | compileJvmKotlin(sourceFiles).let { exitCode -> 603 | if (exitCode != ExitCode.OK) { 604 | return makeResult(exitCode) 605 | } 606 | } 607 | } 608 | 609 | // step 4: compile Java files 610 | return makeResult(compileJava(sourceFiles)) 611 | } 612 | 613 | private fun makeResult(exitCode: ExitCode): JvmCompilationResult { 614 | val messages = internalMessageBuffer.readUtf8() 615 | 616 | if (exitCode != ExitCode.OK) searchSystemOutForKnownErrors(messages) 617 | 618 | return JvmCompilationResult(exitCode, messages, diagnostics, this) 619 | } 620 | 621 | internal fun commonClasspaths() = 622 | mutableListOf() 623 | .apply { 624 | addAll(classpaths) 625 | addAll( 626 | listOfNotNull( 627 | kotlinStdLibJar, 628 | kotlinStdLibCommonJar, 629 | kotlinStdLibJdkJar, 630 | kotlinReflectJar, 631 | kotlinScriptRuntimeJar, 632 | ) 633 | ) 634 | 635 | if (inheritClassPath) { 636 | addAll(hostClasspaths) 637 | log("Inheriting classpaths: " + hostClasspaths.joinToString(File.pathSeparator)) 638 | } 639 | } 640 | .distinct() 641 | 642 | companion object { 643 | const val OPTION_KAPT_KOTLIN_GENERATED = "kapt.kotlin.generated" 644 | } 645 | } 646 | 647 | /** 648 | * Adds the output directory of [previousResult] to the classpath of this compilation. This is a 649 | * convenience for 650 | * 651 | * ``` 652 | * this.classpaths += previousResult.outputDirectory 653 | * ``` 654 | */ 655 | @ExperimentalCompilerApi 656 | fun KotlinCompilation.addPreviousResultToClasspath( 657 | previousResult: JvmCompilationResult 658 | ): KotlinCompilation = apply { classpaths += previousResult.outputDirectory } 659 | -------------------------------------------------------------------------------- /core/src/main/kotlin/com/tschuchort/compiletesting/KotlinJsCompilation.kt: -------------------------------------------------------------------------------- 1 | package com.tschuchort.compiletesting 2 | 3 | import java.io.File 4 | import org.jetbrains.kotlin.cli.common.arguments.K2JSCompilerArguments 5 | import org.jetbrains.kotlin.cli.js.K2JSCompiler 6 | import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi 7 | 8 | @ExperimentalCompilerApi 9 | @Suppress("MemberVisibilityCanBePrivate") 10 | class KotlinJsCompilation : AbstractKotlinCompilation() { 11 | 12 | @Deprecated("It is senseless to use with IR compiler. Only for compatibility.") 13 | var outputFileName: String? = null 14 | 15 | /** 16 | * Generate unpacked KLIB into parent directory of output JS file. In combination with -meta-info 17 | * generates both IR and pre-IR versions of library. 18 | */ 19 | var irProduceKlibDir: Boolean = false 20 | 21 | /** Generate packed klib into file specified by -output. Disables pre-IR backend */ 22 | var irProduceKlibFile: Boolean = false 23 | 24 | /** Generates JS file using IR backend. Also disables pre-IR backend */ 25 | var irProduceJs: Boolean = true 26 | 27 | /** Perform experimental dead code elimination */ 28 | var irDce: Boolean = false 29 | 30 | /** Print declarations' reachability info to stdout during performing DCE */ 31 | var irDcePrintReachabilityInfo: Boolean = false 32 | 33 | /** Specify a compilation module name for IR backend */ 34 | var irModuleName: String? = null 35 | 36 | /** Base name of generated files */ 37 | var moduleName: String? = null 38 | 39 | /** 40 | * Path to the kotlin-stdlib-js.jar 41 | * If none is given, it will be searched for in the host 42 | * process' classpaths 43 | */ 44 | var kotlinStdLibJsJar: File? by default { 45 | HostEnvironment.kotlinStdLibJsJar 46 | } 47 | 48 | /** 49 | * Generate TypeScript declarations .d.ts file alongside JS file. Available in IR backend only 50 | */ 51 | var generateDts: Boolean = false 52 | 53 | // *.class files, Jars and resources (non-temporary) that are created by the 54 | // compilation will land here 55 | val outputDir get() = workingDir.resolve("output") 56 | 57 | // setup common arguments for the two kotlinc calls 58 | private fun jsArgs() = commonArguments(K2JSCompilerArguments()) { args -> 59 | // the compiler should never look for stdlib or reflect in the 60 | // kotlinHome directory (which is null anyway). We will put them 61 | // in the classpath manually if they're needed 62 | args.noStdlib = true 63 | 64 | args.moduleKind = "commonjs" 65 | outputFileName?.let { 66 | args.outputFile = File(outputDir, it).absolutePath 67 | } 68 | args.outputDir = outputDir.absolutePath 69 | args.sourceMapBaseDirs = jsClasspath().joinToString(separator = File.pathSeparator) 70 | args.libraries = listOfNotNull(kotlinStdLibJsJar).joinToString(separator = ":") 71 | 72 | args.irProduceKlibDir = irProduceKlibDir 73 | args.irProduceKlibFile = irProduceKlibFile 74 | args.irProduceJs = irProduceJs 75 | args.irDce = irDce 76 | args.irDcePrintReachabilityInfo = irDcePrintReachabilityInfo 77 | args.moduleName = moduleName 78 | args.irModuleName = irModuleName 79 | args.generateDts = generateDts 80 | } 81 | 82 | /** Runs the compilation task */ 83 | fun compile(): JsCompilationResult { 84 | // make sure all needed directories exist 85 | sourcesDir.deleteRecursively() 86 | sourcesDir.mkdirs() 87 | outputDir.mkdirs() 88 | 89 | // write given sources to working directory 90 | val sourceFiles = sources.map { it.writeTo(sourcesDir) } 91 | 92 | pluginClasspaths.forEach { filepath -> 93 | if (!filepath.exists()) { 94 | error("Plugin $filepath not found") 95 | return makeResult(KotlinCompilation.ExitCode.INTERNAL_ERROR) 96 | } 97 | } 98 | 99 | 100 | /* Work around for warning that sometimes happens: 101 | "Failed to initialize native filesystem for Windows 102 | java.lang.RuntimeException: Could not find installation home path. 103 | Please make sure bin/idea.properties is present in the installation directory" 104 | See: https://github.com/arturbosch/detekt/issues/630 105 | */ 106 | withSystemProperty("idea.use.native.fs.for.win", "false") { 107 | // step 1: compile Kotlin files 108 | return makeResult(compileKotlin(sourceFiles, K2JSCompiler(), jsArgs())) 109 | } 110 | } 111 | 112 | private fun makeResult(exitCode: KotlinCompilation.ExitCode): JsCompilationResult { 113 | val messages = internalMessageBuffer.readUtf8() 114 | 115 | if (exitCode != KotlinCompilation.ExitCode.OK) 116 | searchSystemOutForKnownErrors(messages) 117 | 118 | return JsCompilationResult(exitCode, messages, diagnostics, this) 119 | } 120 | 121 | private fun jsClasspath() = mutableListOf().apply { 122 | addAll(classpaths) 123 | addAll(listOfNotNull(kotlinStdLibCommonJar, kotlinStdLibJsJar)) 124 | 125 | if (inheritClassPath) { 126 | addAll(hostClasspaths) 127 | log("Inheriting classpaths: " + hostClasspaths.joinToString(File.pathSeparator)) 128 | } 129 | }.distinct() 130 | } 131 | -------------------------------------------------------------------------------- /core/src/main/kotlin/com/tschuchort/compiletesting/MainCommandLineProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.tschuchort.compiletesting 2 | 3 | import com.google.auto.service.AutoService 4 | import org.jetbrains.kotlin.compiler.plugin.AbstractCliOption 5 | import org.jetbrains.kotlin.compiler.plugin.CliOption 6 | import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor 7 | import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi 8 | import org.jetbrains.kotlin.config.CompilerConfiguration 9 | 10 | @ExperimentalCompilerApi 11 | @AutoService(CommandLineProcessor::class) 12 | internal class MainCommandLineProcessor : CommandLineProcessor { 13 | override val pluginId: String = Companion.pluginId 14 | 15 | override val pluginOptions: Collection 16 | get() = threadLocalParameters.get()?.pluginOptions 17 | ?: emptyList().also { 18 | // Handle unset parameters gracefully because this plugin may be accidentally called by other tools that 19 | // discover it on the classpath (for example the kotlin jupyter kernel). 20 | System.err.println("WARNING: MainCommandLineProcessor::pluginOptions accessed before thread local parameters have been set") 21 | } 22 | 23 | companion object { 24 | const val pluginId = "com.tschuchort.compiletesting.maincommandlineprocessor" 25 | 26 | /** This CommandLineProcessor is instantiated by K2JVMCompiler using 27 | * a service locator. So we can't just pass parameters to it easily. 28 | * Instead, we need to use a thread-local global variable to pass 29 | * any parameters that change between compilations 30 | */ 31 | val threadLocalParameters: ThreadLocal = ThreadLocal() 32 | 33 | private fun encode(str: String) = str //Base64.getEncoder().encodeToString(str.toByteArray()).replace('=', '%') 34 | 35 | private fun decode(str: String) = str // String(Base64.getDecoder().decode(str.replace('%', '='))) 36 | 37 | fun encodeForeignOptionName(processorPluginId: PluginId, optionName: OptionName) 38 | = encode(processorPluginId) + ":" + encode(optionName) 39 | 40 | fun decodeForeignOptionName(str: String): Pair { 41 | return Regex("(.*?):(.*)").matchEntire(str)?.groupValues?.let { (_, encodedPluginId, encodedOptionName) -> 42 | Pair(decode(encodedPluginId), decode(encodedOptionName)) 43 | } 44 | ?: error("Could not decode foreign option name: '$str'.") 45 | } 46 | } 47 | 48 | class ThreadLocalParameters(cliProcessors: List) { 49 | val cliProcessorsByPluginId: Map> 50 | = cliProcessors.groupBy(CommandLineProcessor::pluginId) 51 | 52 | val optionByProcessorAndName: Map, AbstractCliOption> 53 | = cliProcessors.flatMap { cliProcessor -> 54 | cliProcessor.pluginOptions.map { option -> 55 | Pair(Pair(cliProcessor, option.optionName), option) 56 | } 57 | }.toMap() 58 | 59 | val pluginOptions = cliProcessorsByPluginId.flatMap { (processorPluginId, cliProcessors) -> 60 | cliProcessors.flatMap { cliProcessor -> 61 | cliProcessor.pluginOptions.map { option -> 62 | CliOption( 63 | encodeForeignOptionName(processorPluginId, option.optionName), 64 | option.valueDescription, 65 | option.description, 66 | option.required, 67 | option.allowMultipleOccurrences 68 | ) 69 | } 70 | } 71 | } 72 | } 73 | 74 | override fun processOption(option: AbstractCliOption, value: String, configuration: CompilerConfiguration) { 75 | // Handle unset parameters gracefully because this plugin may be accidentally called by other tools that 76 | // discover it on the classpath (for example the kotlin jupyter kernel). 77 | if (threadLocalParameters.get() == null) { 78 | System.err.println("WARNING: MainCommandLineProcessor::processOption accessed before thread local parameters have been set") 79 | return 80 | } 81 | 82 | val (foreignPluginId, foreignOptionName) = decodeForeignOptionName(option.optionName) 83 | val params = threadLocalParameters.get() 84 | 85 | params.cliProcessorsByPluginId[foreignPluginId]?.forEach { cliProcessor -> 86 | cliProcessor.processOption( 87 | params.optionByProcessorAndName[Pair(cliProcessor, foreignOptionName)] 88 | ?: error("Could not get AbstractCliOption for option '$foreignOptionName'"), 89 | value, configuration 90 | ) 91 | } 92 | ?: error("No CommandLineProcessor given for option '$foreignOptionName' for plugin ID $foreignPluginId") 93 | } 94 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/com/tschuchort/compiletesting/MainComponentRegistrar.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2010-2016 JetBrains s.r.o. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | @file:Suppress("DEPRECATION") 17 | package com.tschuchort.compiletesting 18 | 19 | import com.google.auto.service.AutoService 20 | import org.jetbrains.kotlin.com.intellij.mock.MockProject 21 | import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar 22 | import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar 23 | import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi 24 | import org.jetbrains.kotlin.config.CommonConfigurationKeys.USE_FIR 25 | import org.jetbrains.kotlin.config.CompilerConfiguration 26 | import org.jetbrains.kotlin.kapt3.base.KaptOptions 27 | import org.jetbrains.kotlin.kapt3.base.incremental.IncrementalProcessor 28 | 29 | @ExperimentalCompilerApi 30 | @AutoService(ComponentRegistrar::class, CompilerPluginRegistrar::class) 31 | internal class MainComponentRegistrar : ComponentRegistrar, CompilerPluginRegistrar() { 32 | 33 | override val supportsK2: Boolean 34 | get() = getThreadLocalParameters("supportsK2")?.supportsK2 != false 35 | 36 | // Handle unset parameters gracefully because this plugin may be accidentally called by other tools that 37 | // discover it on the classpath (for example the kotlin jupyter kernel). 38 | private fun getThreadLocalParameters(caller: String): ThreadLocalParameters? { 39 | val params = threadLocalParameters.get() 40 | if (params == null) { 41 | System.err.println("WARNING: ${MainComponentRegistrar::class.simpleName}::$caller accessed before thread local parameters have been set") 42 | } 43 | 44 | return params 45 | } 46 | 47 | override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) { 48 | val parameters = getThreadLocalParameters("registerExtensions") ?: return 49 | 50 | parameters.compilerPluginRegistrar.forEach { pluginRegistrar -> 51 | with(pluginRegistrar) { 52 | registerExtensions(configuration) 53 | } 54 | } 55 | } 56 | 57 | // Legacy plugins 58 | override fun registerProjectComponents(project: MockProject, configuration: CompilerConfiguration) { 59 | val parameters = getThreadLocalParameters("registerProjectComponents") ?: return 60 | 61 | /* 62 | * The order of registering plugins here matters. If the kapt plugin is registered first, then 63 | * it will be executed first and any changes made to the AST by later plugins won't apply to the 64 | * generated stub files and thus won't be visible to any annotation processors. So we decided 65 | * to register third-party plugins before kapt and hope that it works, although we don't 66 | * know for sure if that is the correct way. 67 | */ 68 | parameters.componentRegistrars.forEach { componentRegistrar -> 69 | componentRegistrar.registerProjectComponents(project, configuration) 70 | } 71 | 72 | if (!configuration.getBoolean(USE_FIR)) { 73 | KaptComponentRegistrar(parameters.processors, parameters.kaptOptions) 74 | .registerProjectComponents(project, configuration) 75 | } 76 | } 77 | 78 | companion object { 79 | /* 80 | * This compiler plugin is instantiated by K2JVMCompiler using 81 | * a service locator. So we can't just pass parameters to it easily. 82 | * Instead, we need to use a thread-local global variable to pass 83 | * any parameters that change between compilations 84 | */ 85 | val threadLocalParameters: ThreadLocal = ThreadLocal() 86 | } 87 | 88 | data class ThreadLocalParameters( 89 | val processors: List, 90 | val kaptOptions: KaptOptions.Builder, 91 | val componentRegistrars: List, 92 | val compilerPluginRegistrar: List, 93 | val supportsK2: Boolean, 94 | ) 95 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/com/tschuchort/compiletesting/MutliMessageCollector.kt: -------------------------------------------------------------------------------- 1 | package com.tschuchort.compiletesting 2 | 3 | import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity 4 | import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSourceLocation 5 | import org.jetbrains.kotlin.cli.common.messages.MessageCollector 6 | 7 | internal class MultiMessageCollector( 8 | private vararg val collectors: MessageCollector 9 | ) : MessageCollector { 10 | 11 | override fun clear() { 12 | collectors.forEach { it.clear() } 13 | } 14 | 15 | override fun hasErrors(): Boolean { 16 | return collectors.any { it.hasErrors() } 17 | } 18 | 19 | override fun report(severity: CompilerMessageSeverity, message: String, location: CompilerMessageSourceLocation?) { 20 | collectors.forEach { it.report(severity, message, location) } 21 | } 22 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/com/tschuchort/compiletesting/PrecursorTool.kt: -------------------------------------------------------------------------------- 1 | package com.tschuchort.compiletesting 2 | 3 | import java.io.File 4 | import java.io.PrintStream 5 | import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi 6 | 7 | /** 8 | * A standalone tool that can be run before the KotlinCompilation begins. 9 | */ 10 | @ExperimentalCompilerApi 11 | fun interface PrecursorTool { 12 | fun execute( 13 | compilation: KotlinCompilation, 14 | output: PrintStream, 15 | sources: List, 16 | ): KotlinCompilation.ExitCode 17 | } 18 | -------------------------------------------------------------------------------- /core/src/main/kotlin/com/tschuchort/compiletesting/SourceFile.kt: -------------------------------------------------------------------------------- 1 | package com.tschuchort.compiletesting 2 | 3 | import java.io.File 4 | import okio.buffer 5 | import okio.sink 6 | import org.intellij.lang.annotations.Language 7 | 8 | /** 9 | * A source file for the [KotlinCompilation] 10 | */ 11 | abstract class SourceFile { 12 | internal abstract fun writeTo(dir: File): File 13 | 14 | companion object { 15 | /** 16 | * Create a new Java source file for the compilation when the compilation is run 17 | */ 18 | fun java(name: String, @Language("java") contents: String, trimIndent: Boolean = true): SourceFile { 19 | require(File(name).hasJavaFileExtension()) 20 | val finalContents = if (trimIndent) contents.trimIndent() else contents 21 | return new(name, finalContents) 22 | } 23 | 24 | /** 25 | * Create a new Kotlin source file for the compilation when the compilation is run 26 | */ 27 | fun kotlin(name: String, @Language("kotlin") contents: String, trimIndent: Boolean = true): SourceFile { 28 | require(File(name).hasKotlinFileExtension()) 29 | val finalContents = if (trimIndent) contents.trimIndent() else contents 30 | return new(name, finalContents) 31 | } 32 | 33 | /** 34 | * Create a new source file for the compilation when the compilation is run 35 | */ 36 | fun new(name: String, contents: String) = object : SourceFile() { 37 | override fun writeTo(dir: File): File { 38 | val file = dir.resolve(name) 39 | file.parentFile.mkdirs() 40 | file.createNewFile() 41 | 42 | file.sink().buffer().use { 43 | it.writeUtf8(contents) 44 | } 45 | 46 | return file 47 | } 48 | } 49 | 50 | /** 51 | * Compile an existing source file 52 | */ 53 | @Deprecated("This will not work reliably with KSP, use `new` instead") 54 | fun fromPath(path: File) = object : SourceFile() { 55 | init { 56 | require(path.isFile) 57 | } 58 | 59 | override fun writeTo(dir: File): File = path 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /core/src/main/kotlin/com/tschuchort/compiletesting/StreamUtils.kt: -------------------------------------------------------------------------------- 1 | package com.tschuchort.compiletesting 2 | 3 | import java.io.* 4 | 5 | 6 | /** An output stream that does nothing, like /dev/null */ 7 | internal object NullStream : OutputStream() { 8 | override fun write(b: Int) { 9 | //NoOp 10 | } 11 | 12 | override fun close() { 13 | //NoOp 14 | } 15 | 16 | override fun flush() { 17 | //NoOp 18 | } 19 | 20 | override fun write(b: ByteArray, off: Int, len: Int) { 21 | //NoOp 22 | } 23 | 24 | override fun write(b: ByteArray) { 25 | //NoOp 26 | } 27 | } 28 | 29 | /** A combined stream that writes to all the output streams in [streams]. */ 30 | @Suppress("MemberVisibilityCanBePrivate") 31 | internal class TeeOutputStream(val streams: Collection) : OutputStream() { 32 | 33 | constructor(vararg streams: OutputStream) : this(streams.toList()) 34 | 35 | @Synchronized 36 | @Throws(IOException::class) 37 | override fun write(b: Int) { 38 | for(stream in streams) 39 | stream.write(b) 40 | } 41 | 42 | @Synchronized 43 | @Throws(IOException::class) 44 | override fun write(b: ByteArray) { 45 | for(stream in streams) 46 | stream.write(b) 47 | } 48 | 49 | @Synchronized 50 | @Throws(IOException::class) 51 | override fun write(b: ByteArray, off: Int, len: Int) { 52 | for(stream in streams) 53 | stream.write(b, off, len) 54 | } 55 | 56 | @Throws(IOException::class) 57 | override fun flush() { 58 | for(stream in streams) 59 | stream.flush() 60 | } 61 | 62 | @Throws(IOException::class) 63 | override fun close() { 64 | closeImpl(streams) 65 | } 66 | 67 | @Throws(IOException::class) 68 | private fun closeImpl(streamsToClose : Collection) { 69 | try { 70 | streamsToClose.firstOrNull()?.close() 71 | } 72 | finally { 73 | if(streamsToClose.size > 1) 74 | closeImpl(streamsToClose.drop(1)) 75 | } 76 | } 77 | } -------------------------------------------------------------------------------- /core/src/main/kotlin/com/tschuchort/compiletesting/Utils.kt: -------------------------------------------------------------------------------- 1 | package com.tschuchort.compiletesting 2 | 3 | import java.io.File 4 | import java.io.FileDescriptor 5 | import java.io.FileOutputStream 6 | import java.io.PrintStream 7 | import java.net.URL 8 | import java.net.URLClassLoader 9 | import java.nio.charset.Charset 10 | import javax.lang.model.SourceVersion 11 | import okio.Buffer 12 | 13 | internal fun MutableCollection.addAll(vararg elems: E) = addAll(elems) 14 | 15 | internal fun getJavaHome(): File { 16 | val path = System.getProperty("java.home") 17 | ?: System.getenv("JAVA_HOME") 18 | ?: throw IllegalStateException("no java home found") 19 | 20 | return File(path).also { check(it.isDirectory) } 21 | } 22 | 23 | internal val processJdkHome by lazy { 24 | if(isJdk9OrLater()) 25 | getJavaHome() 26 | else 27 | getJavaHome().parentFile 28 | } 29 | 30 | /** Checks if the JDK of the host process is version 9 or later */ 31 | internal fun isJdk9OrLater(): Boolean 32 | = SourceVersion.latestSupported().compareTo(SourceVersion.RELEASE_8) > 0 33 | 34 | internal fun File.listFilesRecursively(): List = walkTopDown() 35 | .filter { it.isFile } 36 | .toList() 37 | 38 | internal fun File.hasKotlinFileExtension() = hasFileExtension(listOf("kt", "kts")) 39 | 40 | internal fun File.hasJavaFileExtension() = hasFileExtension(listOf("java")) 41 | 42 | internal fun File.hasFileExtension(extensions: List) 43 | = extensions.any{ it.equals(extension, ignoreCase = true) } 44 | 45 | internal fun URLClassLoader.addUrl(url: URL) { 46 | val addUrlMethod = URLClassLoader::class.java.getDeclaredMethod("addURL", URL::class.java) 47 | addUrlMethod.isAccessible = true 48 | addUrlMethod.invoke(this, url) 49 | } 50 | 51 | internal inline fun withSystemProperty(key: String, value: String, f: () -> T): T 52 | = withSystemProperties(mapOf(key to value), f) 53 | 54 | 55 | internal inline fun withSystemProperties(properties: Map, f: () -> T): T { 56 | val previousProperties = mutableMapOf() 57 | 58 | for ((key, value) in properties) { 59 | previousProperties[key] = System.getProperty(key) 60 | System.setProperty(key, value) 61 | } 62 | 63 | try { 64 | return f() 65 | } finally { 66 | for ((key, value) in previousProperties) { 67 | if (value != null) 68 | System.setProperty(key, value) 69 | } 70 | } 71 | } 72 | 73 | internal inline fun withSystemOut(stream: PrintStream, crossinline f: () -> R): R { 74 | System.setOut(stream) 75 | val ret = f() 76 | System.setOut(PrintStream(FileOutputStream(FileDescriptor.out))) 77 | return ret 78 | } 79 | 80 | internal inline fun captureSystemOut(crossinline f: () -> R): Pair { 81 | val systemOutBuffer = Buffer() 82 | val ret = withSystemOut(PrintStream(systemOutBuffer.outputStream()), f) 83 | return Pair(ret, systemOutBuffer.readString(Charset.defaultCharset())) 84 | } 85 | 86 | internal fun File.existsOrNull(): File? = if (exists()) this else null -------------------------------------------------------------------------------- /core/src/main/kotlin/com/tschuchort/compiletesting/kapt/util.kt: -------------------------------------------------------------------------------- 1 | package com.tschuchort.compiletesting.kapt 2 | 3 | import java.io.File 4 | import org.jetbrains.kotlin.kapt.cli.KaptCliOption 5 | import org.jetbrains.kotlin.kapt3.base.KaptFlag 6 | import org.jetbrains.kotlin.kapt3.base.KaptOptions 7 | 8 | fun KaptOptions.Builder.toPluginOptions(): List { 9 | val options = mutableListOf() 10 | for (option in KaptCliOption.entries) { 11 | fun Any.pluginOption(value: String = this.toString()) { 12 | options += listOf("-P", "plugin:" + KaptCliOption.ANNOTATION_PROCESSING_COMPILER_PLUGIN_ID + ":" + option.optionName + "=" + value) 13 | } 14 | 15 | when (option) { 16 | KaptCliOption.SOURCE_OUTPUT_DIR_OPTION -> sourcesOutputDir?.pluginOption() 17 | KaptCliOption.CLASS_OUTPUT_DIR_OPTION -> classesOutputDir?.pluginOption() 18 | KaptCliOption.STUBS_OUTPUT_DIR_OPTION -> stubsOutputDir?.pluginOption() 19 | KaptCliOption.INCREMENTAL_DATA_OUTPUT_DIR_OPTION -> incrementalDataOutputDir?.pluginOption() 20 | 21 | KaptCliOption.CHANGED_FILES -> { 22 | for (file in changedFiles) { 23 | file.pluginOption() 24 | } 25 | } 26 | KaptCliOption.COMPILED_SOURCES_DIR -> compiledSources.joinToString(File.pathSeparator).pluginOption() 27 | KaptCliOption.INCREMENTAL_CACHE -> incrementalCache?.pluginOption() 28 | KaptCliOption.CLASSPATH_CHANGES -> { 29 | for (change in classpathChanges) { 30 | change.pluginOption() 31 | } 32 | } 33 | KaptCliOption.PROCESS_INCREMENTALLY -> (KaptFlag.INCREMENTAL_APT in flags).pluginOption() 34 | 35 | KaptCliOption.ANNOTATION_PROCESSOR_CLASSPATH_OPTION -> { 36 | for (path in processingClasspath) { 37 | path.pluginOption() 38 | } 39 | } 40 | KaptCliOption.ANNOTATION_PROCESSORS_OPTION -> processors 41 | .map(String::trim) 42 | .filterNot(String::isEmpty) 43 | .joinToString(",") 44 | .pluginOption() 45 | 46 | KaptCliOption.APT_OPTION_OPTION -> { 47 | for ((k, v) in processingOptions) { 48 | "$k=$v".pluginOption() 49 | } 50 | } 51 | KaptCliOption.JAVAC_OPTION_OPTION -> { 52 | for ((k, v) in javacOptions) { 53 | "$k=$v".pluginOption() 54 | } 55 | } 56 | 57 | KaptCliOption.VERBOSE_MODE_OPTION -> (KaptFlag.VERBOSE in flags).pluginOption() 58 | KaptCliOption.USE_LIGHT_ANALYSIS_OPTION -> (KaptFlag.USE_LIGHT_ANALYSIS in flags).pluginOption() 59 | KaptCliOption.CORRECT_ERROR_TYPES_OPTION -> (KaptFlag.CORRECT_ERROR_TYPES in flags).pluginOption() 60 | KaptCliOption.DUMP_DEFAULT_PARAMETER_VALUES -> (KaptFlag.DUMP_DEFAULT_PARAMETER_VALUES in flags).pluginOption() 61 | KaptCliOption.MAP_DIAGNOSTIC_LOCATIONS_OPTION -> (KaptFlag.MAP_DIAGNOSTIC_LOCATIONS in flags).pluginOption() 62 | KaptCliOption.INFO_AS_WARNINGS_OPTION -> (KaptFlag.INFO_AS_WARNINGS in flags).pluginOption() 63 | KaptCliOption.STRICT_MODE_OPTION -> (KaptFlag.STRICT in flags).pluginOption() 64 | KaptCliOption.STRIP_METADATA_OPTION -> (KaptFlag.STRIP_METADATA in flags).pluginOption() 65 | KaptCliOption.KEEP_KDOC_COMMENTS_IN_STUBS -> (KaptFlag.KEEP_KDOC_COMMENTS_IN_STUBS in flags).pluginOption() 66 | KaptCliOption.USE_K2 -> {} 67 | 68 | KaptCliOption.SHOW_PROCESSOR_STATS -> (KaptFlag.SHOW_PROCESSOR_STATS in flags).pluginOption() 69 | KaptCliOption.DUMP_PROCESSOR_STATS -> processorsStatsReportFile?.pluginOption() 70 | KaptCliOption.DUMP_FILE_READ_HISTORY -> fileReadHistoryReportFile?.pluginOption() 71 | KaptCliOption.INCLUDE_COMPILE_CLASSPATH -> (KaptFlag.INCLUDE_COMPILE_CLASSPATH in flags).pluginOption() 72 | 73 | KaptCliOption.DETECT_MEMORY_LEAKS_OPTION -> detectMemoryLeaks.stringValue.pluginOption() 74 | KaptCliOption.APT_MODE_OPTION -> mode.stringValue.pluginOption() 75 | 76 | else -> { 77 | // Deprecated or unsupported options 78 | } 79 | } 80 | } 81 | return options 82 | } -------------------------------------------------------------------------------- /core/src/test/java/com/tschuchort/compiletesting/JavaTestProcessor.java: -------------------------------------------------------------------------------- 1 | package com.tschuchort.compiletesting; 2 | 3 | import com.squareup.javapoet.JavaFile; 4 | import com.squareup.kotlinpoet.FileSpec; 5 | import com.squareup.kotlinpoet.TypeSpec; 6 | import kotlin.text.Charsets; 7 | 8 | import javax.annotation.processing.AbstractProcessor; 9 | import javax.annotation.processing.ProcessingEnvironment; 10 | import javax.annotation.processing.RoundEnvironment; 11 | import javax.lang.model.SourceVersion; 12 | import javax.lang.model.element.Element; 13 | import javax.lang.model.element.TypeElement; 14 | import javax.tools.Diagnostic; 15 | import java.io.File; 16 | import java.io.FileOutputStream; 17 | import java.util.LinkedHashSet; 18 | import java.util.Set; 19 | 20 | public class JavaTestProcessor extends AbstractProcessor { 21 | 22 | public static String ON_INIT_MSG = "java processor init"; 23 | public static String GENERATED_PACKAGE = "com.tschuchort.compiletesting"; 24 | public static String GENERATED_JAVA_CLASS_NAME = "JavaGeneratedJavaClass"; 25 | public static String GENERATED_KOTLIN_CLASS_NAME = "JavaGeneratedKotlinClass"; 26 | 27 | @Override 28 | public SourceVersion getSupportedSourceVersion() { 29 | return SourceVersion.latestSupported(); 30 | } 31 | 32 | @Override 33 | public Set getSupportedAnnotationTypes() { 34 | LinkedHashSet set = new LinkedHashSet<>(); 35 | set.add(ProcessElem.class.getCanonicalName()); 36 | return set; 37 | } 38 | 39 | @Override 40 | public Set getSupportedOptions() { 41 | LinkedHashSet set = new LinkedHashSet<>(); 42 | set.add("kapt.kotlin.generated"); 43 | return set; 44 | } 45 | 46 | @Override 47 | public synchronized void init(ProcessingEnvironment processingEnv) { 48 | super.init(processingEnv); 49 | processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, ON_INIT_MSG); 50 | } 51 | 52 | @Override 53 | public boolean process(Set annotations, RoundEnvironment roundEnv) { 54 | processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, "java processor was called"); 55 | 56 | for (Element annotatedElem : roundEnv.getElementsAnnotatedWith(ProcessElem.class)) { 57 | processingEnv.getMessager().printMessage(Diagnostic.Kind.WARNING, 58 | new ProcessedElemMessage(annotatedElem.getSimpleName().toString()).print()); 59 | } 60 | 61 | if(annotations.isEmpty()) { 62 | TypeSpec.Builder typeSpecBuilder = TypeSpec.classBuilder(GENERATED_KOTLIN_CLASS_NAME); 63 | 64 | FileSpec fileSpec = FileSpec.builder(GENERATED_PACKAGE, GENERATED_KOTLIN_CLASS_NAME + ".kt") 65 | .addType(typeSpecBuilder.build()).build(); 66 | 67 | writeKotlinFile(fileSpec, fileSpec.getName(), fileSpec.getPackageName()); 68 | 69 | try { 70 | JavaFile.builder(GENERATED_PACKAGE, 71 | com.squareup.javapoet.TypeSpec.classBuilder(GENERATED_JAVA_CLASS_NAME).build()) 72 | .build().writeTo(processingEnv.getFiler()); 73 | } catch (Exception e) { 74 | } 75 | } 76 | 77 | return false; 78 | } 79 | 80 | private void writeKotlinFile(FileSpec fileSpec, String fileName, String packageName) { 81 | String kaptKotlinGeneratedDir = processingEnv.getOptions().get("kapt.kotlin.generated"); 82 | 83 | String relativePath = packageName.replace('.', File.separatorChar); 84 | 85 | File outputFolder = new File(kaptKotlinGeneratedDir, relativePath); 86 | outputFolder.mkdirs(); 87 | 88 | // finally write to output file 89 | try { 90 | (new FileOutputStream(new File(outputFolder, fileName))).write(fileSpec.toString().getBytes(Charsets.UTF_8)); 91 | } 92 | catch(Exception e) { 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /core/src/test/kotlin/com/tschuchort/compiletesting/CompilerPluginsTest.kt: -------------------------------------------------------------------------------- 1 | package com.tschuchort.compiletesting 2 | 3 | import com.nhaarman.mockitokotlin2.any 4 | import com.nhaarman.mockitokotlin2.atLeastOnce 5 | import com.nhaarman.mockitokotlin2.verify 6 | import java.net.URL 7 | import javax.annotation.processing.AbstractProcessor 8 | import javax.annotation.processing.RoundEnvironment 9 | import javax.lang.model.element.TypeElement 10 | import org.assertj.core.api.Assertions.assertThat 11 | import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar 12 | import org.junit.Assert 13 | import org.junit.Test 14 | import org.junit.runner.RunWith 15 | import org.junit.runners.Parameterized 16 | import org.mockito.Mockito 17 | 18 | @RunWith(Parameterized::class) 19 | class CompilerPluginsTest( 20 | private val useK2: Boolean 21 | ) { 22 | companion object { 23 | @Parameterized.Parameters(name = "useK2={0}") 24 | @JvmStatic 25 | fun data() = arrayOf( 26 | arrayOf(true), 27 | arrayOf(false) 28 | ) 29 | } 30 | 31 | @Test 32 | fun `when compiler plugins are added they get executed`() { 33 | 34 | val mockPlugin = Mockito.mock(ComponentRegistrar::class.java) 35 | val fakeRegistrar = FakeCompilerPluginRegistrar() 36 | 37 | val result = defaultCompilerConfig(useK2).apply { 38 | sources = listOf(SourceFile.new("emptyKotlinFile.kt", "")) 39 | componentRegistrars = listOf(mockPlugin) 40 | compilerPluginRegistrars = listOf(fakeRegistrar) 41 | inheritClassPath = true 42 | }.compile() 43 | 44 | verify(mockPlugin, atLeastOnce()).registerProjectComponents(any(), any()) 45 | fakeRegistrar.assertRegistered() 46 | 47 | assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) 48 | } 49 | 50 | @Test 51 | fun `when compiler plugins and annotation processors are added they get executed`() { 52 | 53 | val annotationProcessor = object : AbstractProcessor() { 54 | override fun getSupportedAnnotationTypes(): Set = setOf(ProcessElem::class.java.canonicalName) 55 | 56 | override fun process(p0: MutableSet?, p1: RoundEnvironment?): Boolean { 57 | p1?.getElementsAnnotatedWith(ProcessElem::class.java)?.forEach { 58 | Assert.assertEquals("JSource", it?.simpleName.toString()) 59 | } 60 | return false 61 | } 62 | } 63 | 64 | val mockPlugin = Mockito.mock(ComponentRegistrar::class.java) 65 | 66 | val jSource = SourceFile.kotlin( 67 | "JSource.kt", """ 68 | package com.tschuchort.compiletesting; 69 | 70 | @ProcessElem 71 | class JSource { 72 | fun foo() { } 73 | } 74 | """ 75 | ) 76 | 77 | val result = defaultCompilerConfig(useK2).apply { 78 | sources = listOf(jSource) 79 | annotationProcessors = listOf(annotationProcessor) 80 | componentRegistrars = listOf(mockPlugin) 81 | inheritClassPath = true 82 | }.compile() 83 | 84 | verify(mockPlugin, atLeastOnce()).registerProjectComponents(any(), any()) 85 | 86 | assertThat(result.exitCode).isEqualTo(KotlinCompilation.ExitCode.OK) 87 | } 88 | 89 | @Test 90 | fun `convert jar url resource to path without decoding encoded path`() { 91 | // path on disk has "url%3Aport" path segment, but it's encoded from classLoader.getResources() 92 | val absolutePath = "jar:file:/path/to/jar/url%253Aport/core-0.4.0.jar!" + 93 | "/META-INF/services/org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar" 94 | val resultPath = KotlinCompilation().urlToResourcePath(URL(absolutePath)).toString() 95 | assertThat(resultPath.contains("url:")).isFalse() 96 | assertThat(resultPath.contains("url%25")).isFalse() 97 | assertThat(resultPath.contains("url%3A")).isTrue() 98 | } 99 | 100 | @Test 101 | fun `convert file url resource to path without decoding`() { 102 | // path on disk has "repos%3Aoss" path segment, but it's encoded from classLoader.getResources() 103 | val absolutePath = "file:/Users/user/repos%253Aoss/kotlin-compile-testing/core/build/resources/main" 104 | val resultPath = KotlinCompilation().urlToResourcePath(URL(absolutePath)).toString() 105 | assertThat(resultPath.contains("repos:")).isFalse() 106 | assertThat(resultPath.contains("repos%25")).isFalse() 107 | assertThat(resultPath.contains("repos%3A")).isTrue() 108 | } 109 | } 110 | 111 | -------------------------------------------------------------------------------- /core/src/test/kotlin/com/tschuchort/compiletesting/FakeCompilerPluginRegistrar.kt: -------------------------------------------------------------------------------- 1 | package com.tschuchort.compiletesting 2 | 3 | import org.jetbrains.kotlin.compiler.plugin.CompilerPluginRegistrar 4 | import org.jetbrains.kotlin.config.CompilerConfiguration 5 | 6 | class FakeCompilerPluginRegistrar( 7 | override val supportsK2: Boolean = false, 8 | ) : CompilerPluginRegistrar() { 9 | private var isRegistered = false 10 | 11 | override fun ExtensionStorage.registerExtensions(configuration: CompilerConfiguration) { 12 | isRegistered = true 13 | } 14 | 15 | fun assertRegistered() { 16 | if(!isRegistered) { 17 | throw AssertionError("FakeCompilerPluginRegistrar was not registered") 18 | } 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /core/src/test/kotlin/com/tschuchort/compiletesting/JavacUtilsTest.kt: -------------------------------------------------------------------------------- 1 | package com.tschuchort.compiletesting 2 | 3 | import org.junit.Assert.assertEquals 4 | import org.junit.Assert.assertFalse 5 | import org.junit.Assert.assertNull 6 | import org.junit.Assert.assertTrue 7 | import org.junit.Test 8 | 9 | class JavacUtilsTest { 10 | 11 | @Test 12 | fun `Old version scheme less than 9 is parsed correctly`() { 13 | assertFalse(isJavac9OrLater("1.8.0")) 14 | } 15 | 16 | @Test 17 | fun `Old version scheme greater equal 9 is parsed correctly`() { 18 | assertTrue(isJavac9OrLater("1.9.1")) 19 | assertTrue(isJavac9OrLater("1.11.0")) 20 | } 21 | 22 | @Test 23 | fun `New version scheme less than 9 is parsed correctly`() { 24 | assertFalse(isJavac9OrLater("8.1.0.1")) 25 | assertFalse(isJavac9OrLater("8.1.0")) 26 | assertFalse(isJavac9OrLater("8.1")) 27 | assertFalse(isJavac9OrLater("8")) 28 | } 29 | 30 | @Test 31 | fun `New version scheme greater equal 9 is parsed correctly`() { 32 | assertTrue(isJavac9OrLater("9.0.0.1")) 33 | assertTrue(isJavac9OrLater("9.0.0")) 34 | assertTrue(isJavac9OrLater("9.1.0")) 35 | assertTrue(isJavac9OrLater("9.1")) 36 | assertTrue(isJavac9OrLater("9")) 37 | assertTrue(isJavac9OrLater("12")) 38 | } 39 | 40 | @Test 41 | fun `Old version scheme with extra info is parsed correctly`() { 42 | assertTrue(isJavac9OrLater("1.11.0-bla")) 43 | } 44 | 45 | @Test 46 | fun `Standard javac -version output is parsed correctly`() { 47 | assertEquals("1.8.0_252", parseVersionString("javac 1.8.0_252")) 48 | } 49 | 50 | @Test 51 | fun `javac -version output with JAVA OPTIONS is parsed correctly`() { 52 | assertEquals( 53 | "1.8.0_222", 54 | parseVersionString( 55 | "Picked up _JAVA_OPTIONS: -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap javac 1.8.0_222" 56 | ) 57 | ) 58 | } 59 | 60 | @Test 61 | fun `Wrong javac -version output is returning null`() { 62 | assertNull( 63 | parseVersionString( 64 | "wrong javac" 65 | ) 66 | ) 67 | } 68 | } -------------------------------------------------------------------------------- /core/src/test/kotlin/com/tschuchort/compiletesting/KotlinJsCompilationTests.kt: -------------------------------------------------------------------------------- 1 | package com.tschuchort.compiletesting 2 | 3 | import com.nhaarman.mockitokotlin2.any 4 | import com.nhaarman.mockitokotlin2.argWhere 5 | import com.nhaarman.mockitokotlin2.atLeastOnce 6 | import com.nhaarman.mockitokotlin2.eq 7 | import com.nhaarman.mockitokotlin2.never 8 | import com.nhaarman.mockitokotlin2.spy 9 | import com.nhaarman.mockitokotlin2.verify 10 | import com.tschuchort.compiletesting.KotlinCompilation.ExitCode 11 | import com.tschuchort.compiletesting.MockitoAdditionalMatchersKotlin.Companion.not 12 | import java.io.File 13 | import org.assertj.core.api.Assertions.assertThat 14 | import org.jetbrains.kotlin.compiler.plugin.AbstractCliOption 15 | import org.jetbrains.kotlin.compiler.plugin.CliOption 16 | import org.jetbrains.kotlin.compiler.plugin.CommandLineProcessor 17 | import org.junit.Ignore 18 | import org.junit.Rule 19 | import org.junit.Test 20 | import org.junit.rules.TemporaryFolder 21 | 22 | @Ignore("These no longer work with Kotlin JS IR") 23 | @Suppress("MemberVisibilityCanBePrivate") 24 | class KotlinJsCompilationTests { 25 | @Rule @JvmField val temporaryFolder = TemporaryFolder() 26 | 27 | @Test 28 | fun `runs with only kotlin sources`() { 29 | val result = defaultJsCompilerConfig().apply { 30 | sources = listOf(SourceFile.kotlin("kSource.kt", "class KSource")) 31 | }.compile() 32 | 33 | assertThat(result.exitCode).isEqualTo(ExitCode.OK) 34 | assertThat(result.jsFiles).hasSize(1) 35 | val jsFile = result.jsFiles[0] 36 | assertThat(jsFile.readText()).contains("function KSource()") 37 | } 38 | 39 | @Test 40 | fun `runs with no sources`() { 41 | val result = defaultJsCompilerConfig().apply { 42 | sources = emptyList() 43 | }.compile() 44 | 45 | assertThat(result.exitCode).isEqualTo(ExitCode.OK) 46 | } 47 | 48 | @Test 49 | fun `runs with SourceFile from path`() { 50 | val sourceFile = temporaryFolder.newFile("KSource.kt").apply { 51 | writeText("class KSource") 52 | } 53 | 54 | val result = defaultJsCompilerConfig().apply { 55 | sources = listOf(SourceFile.fromPath(sourceFile)) 56 | }.compile() 57 | 58 | assertThat(result.exitCode).isEqualTo(ExitCode.OK) 59 | assertThat(result.jsFiles).hasSize(1) 60 | val jsFile = result.jsFiles[0] 61 | assertThat(jsFile.readText()).contains("function KSource()") 62 | } 63 | 64 | @Test 65 | fun `runs with SourceFile from paths with filename conflicts`() { 66 | temporaryFolder.newFolder("a") 67 | val sourceFileA = temporaryFolder.newFile("a/KSource.kt").apply { 68 | writeText("package a\n\nclass KSource") 69 | } 70 | 71 | temporaryFolder.newFolder("b") 72 | val sourceFileB = temporaryFolder.newFile("b/KSource.kt").apply { 73 | writeText("package b\n\nclass KSource") 74 | } 75 | 76 | val result = defaultJsCompilerConfig().apply { 77 | sources = listOf( 78 | SourceFile.fromPath(sourceFileA), 79 | SourceFile.fromPath(sourceFileB)) 80 | }.compile() 81 | 82 | assertThat(result.exitCode).isEqualTo(ExitCode.OK) 83 | assertThat(result.jsFiles).hasSize(1) 84 | val jsFile = result.jsFiles[0] 85 | assertThat(jsFile.readText()).contains("function KSource() {") 86 | assertThat(jsFile.readText()).contains("function KSource_0() {") 87 | } 88 | 89 | @Test 90 | fun `Kotlin can access browser window`() { 91 | val source = SourceFile.kotlin("kSource.kt", """ 92 | import kotlinx.browser.window 93 | 94 | fun main(addKotlincArgs: Array) { 95 | println(window.document) 96 | } 97 | """) 98 | 99 | val result = defaultJsCompilerConfig().apply { 100 | sources = listOf(source) 101 | }.compile() 102 | 103 | assertThat(result.exitCode).isEqualTo(ExitCode.OK) 104 | assertThat(result.jsFiles).hasSize(1) 105 | val jsFile = result.jsFiles[0] 106 | println(jsFile.readText()) 107 | assertThat(jsFile.readText()).contains("println(window.document);") 108 | } 109 | 110 | @Test 111 | fun `detects the plugin provided for compilation via pluginClasspaths property`() { 112 | val result = defaultJsCompilerConfig().apply { 113 | sources = listOf(SourceFile.kotlin("kSource.kt", "class KSource")) 114 | pluginClasspaths = listOf(classpathOf("kotlin-scripting-compiler-${BuildConfig.KOTLIN_VERSION}")) 115 | }.compile() 116 | 117 | assertThat(result.exitCode).isEqualTo(ExitCode.OK) 118 | assertThat(result.messages).contains( 119 | "provided plugin org.jetbrains.kotlin.scripting.compiler.plugin.ScriptingCompilerConfigurationComponentRegistrar" 120 | ) 121 | } 122 | 123 | @Test 124 | fun `returns an internal error when adding a non existing plugin for compilation`() { 125 | val result = defaultJsCompilerConfig().apply { 126 | sources = listOf(SourceFile.kotlin("kSource.kt", "class KSource")) 127 | pluginClasspaths = listOf(File("./non-existing-plugin.jar")) 128 | }.compile() 129 | 130 | assertThat(result.exitCode).isEqualTo(ExitCode.INTERNAL_ERROR) 131 | assertThat(result.messages).contains("non-existing-plugin.jar not found") 132 | } 133 | 134 | @Test 135 | fun `Custom plugin receives CLI argument`() { 136 | val kSource = SourceFile.kotlin( 137 | "KSource.kt", """ 138 | package com.tschuchort.compiletesting 139 | class KSource 140 | """.trimIndent() 141 | ) 142 | 143 | val cliProcessor = spy(object : CommandLineProcessor { 144 | override val pluginId = "myPluginId" 145 | override val pluginOptions = listOf(CliOption("test_option_name", "", "")) 146 | }) 147 | 148 | val result = defaultJsCompilerConfig().apply { 149 | sources = listOf(kSource) 150 | inheritClassPath = false 151 | pluginOptions = listOf(PluginOption("myPluginId", "test_option_name", "test_value")) 152 | commandLineProcessors = listOf(cliProcessor) 153 | }.compile() 154 | 155 | assertThat(result.exitCode).isEqualTo(ExitCode.OK) 156 | 157 | verify(cliProcessor, atLeastOnce()).processOption(argWhere { it.optionName == "test_option_name" }, eq("test_value"), any()) 158 | verify(cliProcessor, never()).processOption(argWhere { it.optionName == "test_option_name" }, not(eq("test_value")), any()) 159 | verify(cliProcessor, never()).processOption(argWhere { it.optionName != "test_option_name" }, any(), any()) 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /core/src/test/kotlin/com/tschuchort/compiletesting/KotlinTestProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.tschuchort.compiletesting 2 | 3 | import com.squareup.javapoet.JavaFile 4 | import com.squareup.kotlinpoet.FileSpec 5 | import com.squareup.kotlinpoet.TypeSpec 6 | import java.io.File 7 | import javax.annotation.processing.AbstractProcessor 8 | import javax.annotation.processing.ProcessingEnvironment 9 | import javax.annotation.processing.RoundEnvironment 10 | import javax.lang.model.SourceVersion 11 | import javax.lang.model.element.TypeElement 12 | import javax.tools.Diagnostic 13 | import com.squareup.javapoet.TypeSpec as JavaTypeSpec 14 | 15 | annotation class ProcessElem 16 | 17 | data class ProcessedElemMessage(val elementSimpleName: String) { 18 | fun print() = MSG_PREFIX + elementSimpleName + MSG_SUFFIX 19 | 20 | init { 21 | require(elementSimpleName.isNotEmpty()) 22 | } 23 | 24 | companion object { 25 | private const val MSG_PREFIX = "processed element{" 26 | private const val MSG_SUFFIX = "}" 27 | 28 | fun parseAllIn(s: String): List { 29 | val pattern = Regex(Regex.escape(MSG_PREFIX) + "(.+)?" + Regex.escape(MSG_SUFFIX)) 30 | return pattern.findAll(s) 31 | .map { match -> 32 | ProcessedElemMessage(match.destructured.component1()) 33 | }.toList() 34 | } 35 | } 36 | } 37 | 38 | class KotlinTestProcessor : AbstractProcessor() { 39 | companion object { 40 | private const val KAPT_KOTLIN_GENERATED_OPTION_NAME = "kapt.kotlin.generated" 41 | private const val GENERATE_KOTLIN_CODE_OPTION = "generate.kotlin.value" 42 | private const val GENERATE_ERRORS_OPTION = "generate.error" 43 | private const val FILE_SUFFIX_OPTION = "suffix" 44 | const val ON_INIT_MSG = "kotlin processor init" 45 | const val GENERATED_PACKAGE = "com.tschuchort.compiletesting" 46 | const val GENERATED_JAVA_CLASS_NAME = "KotlinGeneratedJavaClass" 47 | const val GENERATED_KOTLIN_CLASS_NAME = "KotlinGeneratedKotlinClass" 48 | } 49 | 50 | private val kaptKotlinGeneratedDir by lazy { processingEnv.options[KAPT_KOTLIN_GENERATED_OPTION_NAME] } 51 | 52 | override fun getSupportedSourceVersion(): SourceVersion = SourceVersion.latest() 53 | override fun getSupportedOptions() = setOf( 54 | KAPT_KOTLIN_GENERATED_OPTION_NAME, 55 | GENERATE_KOTLIN_CODE_OPTION, 56 | GENERATE_ERRORS_OPTION 57 | ) 58 | override fun getSupportedAnnotationTypes(): Set = setOf(ProcessElem::class.java.canonicalName) 59 | 60 | override fun init(processingEnv: ProcessingEnvironment) { 61 | processingEnv.messager.printMessage(Diagnostic.Kind.WARNING, ON_INIT_MSG) 62 | super.init(processingEnv) 63 | } 64 | 65 | override fun process(annotations: Set, roundEnv: RoundEnvironment): Boolean { 66 | processingEnv.messager.printMessage(Diagnostic.Kind.WARNING, "kotlin processor was called") 67 | 68 | for (annotatedElem in roundEnv.getElementsAnnotatedWith(ProcessElem::class.java)) { 69 | processingEnv.messager.printMessage(Diagnostic.Kind.WARNING, 70 | ProcessedElemMessage(annotatedElem.simpleName.toString()).print()) 71 | } 72 | 73 | if(annotations.isEmpty()) { 74 | FileSpec.builder(GENERATED_PACKAGE, GENERATED_KOTLIN_CLASS_NAME + ".kt") 75 | .addType( 76 | TypeSpec.classBuilder(GENERATED_KOTLIN_CLASS_NAME).build() 77 | ).build() 78 | .let { writeKotlinFile(it) } 79 | 80 | JavaFile.builder(GENERATED_PACKAGE, JavaTypeSpec.classBuilder(GENERATED_JAVA_CLASS_NAME).build()) 81 | .build().writeTo(processingEnv.filer) 82 | } 83 | 84 | return false 85 | } 86 | 87 | private fun writeKotlinFile(fileSpec: FileSpec, fileName: String = fileSpec.name, packageName: String = fileSpec.packageName) { 88 | val relativePath = packageName.replace('.', File.separatorChar) 89 | 90 | val outputFolder = File(kaptKotlinGeneratedDir!!, relativePath) 91 | outputFolder.mkdirs() 92 | 93 | // finally write to output file 94 | File(outputFolder, fileName).writeText(fileSpec.toString()) 95 | } 96 | } -------------------------------------------------------------------------------- /core/src/test/kotlin/com/tschuchort/compiletesting/Mockito.kt: -------------------------------------------------------------------------------- 1 | package com.tschuchort.compiletesting 2 | 3 | import com.nhaarman.mockitokotlin2.internal.createInstance 4 | import org.mockito.AdditionalMatchers 5 | 6 | class MockitoAdditionalMatchersKotlin { 7 | companion object { 8 | inline fun not(matcher: T): T { 9 | return AdditionalMatchers.not(matcher) ?: createInstance() 10 | } 11 | 12 | inline fun or(left: T, right: T): T { 13 | return AdditionalMatchers.or(left, right) ?: createInstance() 14 | } 15 | 16 | inline fun and(left: T, right: T): T { 17 | return AdditionalMatchers.and(left, right) ?: createInstance() 18 | } 19 | 20 | inline fun > geq(value: T): T { 21 | return AdditionalMatchers.geq(value) ?: createInstance() 22 | } 23 | 24 | inline fun > leq(value: T): T { 25 | return AdditionalMatchers.leq(value) ?: createInstance() 26 | } 27 | 28 | inline fun > gt(value: T): T { 29 | return AdditionalMatchers.gt(value) ?: createInstance() 30 | } 31 | 32 | inline fun > lt(value: T): T { 33 | return AdditionalMatchers.lt(value) ?: createInstance() 34 | } 35 | 36 | inline fun > cmpEq(value: T): T { 37 | return AdditionalMatchers.cmpEq(value) ?: createInstance() 38 | } 39 | 40 | fun find(regex: Regex): String { 41 | return AdditionalMatchers.find(regex.pattern) ?: createInstance() 42 | } 43 | 44 | fun eq(value: Float, delta: Float): Float { 45 | return AdditionalMatchers.eq(value, delta) 46 | } 47 | 48 | fun eq(value: Double, delta: Double): Double { 49 | return AdditionalMatchers.eq(value, delta) 50 | } 51 | } 52 | } -------------------------------------------------------------------------------- /core/src/test/kotlin/com/tschuchort/compiletesting/StreamUtilTests.kt: -------------------------------------------------------------------------------- 1 | package com.tschuchort.compiletesting 2 | 3 | import com.nhaarman.mockitokotlin2.mock 4 | import com.nhaarman.mockitokotlin2.verify 5 | import java.io.OutputStream 6 | import java.io.PrintStream 7 | import okio.Buffer 8 | import org.assertj.core.api.Assertions 9 | import org.junit.Test 10 | 11 | class StreamUtilTests { 12 | 13 | @Test 14 | fun `TeeOutputStream prints to all streams`() { 15 | val buf1 = Buffer() 16 | val buf2 = Buffer() 17 | 18 | val s = "test test \ntest\n" 19 | 20 | PrintStream( 21 | TeeOutputStream( 22 | PrintStream(buf1.outputStream()), 23 | buf2.outputStream() 24 | ) 25 | ).print(s) 26 | 27 | Assertions.assertThat(buf1.readUtf8()).isEqualTo(s) 28 | Assertions.assertThat(buf2.readUtf8()).isEqualTo(s) 29 | } 30 | 31 | @Test 32 | fun `TeeOutPutStream flushes all streams`() { 33 | val str1 = mock() 34 | val str2 = mock() 35 | 36 | TeeOutputStream(str1, str2).flush() 37 | 38 | verify(str1).flush() 39 | verify(str2).flush() 40 | } 41 | 42 | @Test 43 | fun `TeeOutPutStream closes all streams`() { 44 | val str1 = mock() 45 | val str2 = mock() 46 | 47 | TeeOutputStream(str1, str2).close() 48 | 49 | verify(str1).close() 50 | verify(str2).close() 51 | } 52 | } -------------------------------------------------------------------------------- /core/src/test/kotlin/com/tschuchort/compiletesting/TestUtils.kt: -------------------------------------------------------------------------------- 1 | package com.tschuchort.compiletesting 2 | 3 | import io.github.classgraph.ClassGraph 4 | import java.io.File 5 | import org.assertj.core.api.Assertions 6 | 7 | fun defaultCompilerConfig(useK2: Boolean): KotlinCompilation { 8 | return KotlinCompilation().apply { 9 | inheritClassPath = false 10 | correctErrorTypes = true 11 | verbose = true 12 | reportOutputFiles = false 13 | messageOutputStream = System.out 14 | if (useK2) { 15 | languageVersion = "2.0" 16 | useKapt4 = true 17 | } else { 18 | languageVersion = "1.9" 19 | } 20 | } 21 | } 22 | 23 | fun defaultJsCompilerConfig(): KotlinJsCompilation { 24 | return KotlinJsCompilation( ).apply { 25 | inheritClassPath = false 26 | verbose = true 27 | reportOutputFiles = false 28 | messageOutputStream = System.out 29 | moduleName = "test" 30 | } 31 | } 32 | 33 | 34 | fun assertClassLoadable(compileResult: JvmCompilationResult, className: String): Class<*> { 35 | return try { 36 | val clazz = compileResult.classLoader.loadClass(className) 37 | Assertions.assertThat(clazz).isNotNull 38 | clazz 39 | } 40 | catch(e: ClassNotFoundException) { 41 | Assertions.fail("Class $className could not be loaded") 42 | } 43 | } 44 | 45 | /** 46 | * Returns the classpath for a dependency (format $name-$version). 47 | * This is necessary to know the actual location of a dependency 48 | * which has been included in test runtime (build.gradle). 49 | */ 50 | fun classpathOf(dependency: String): File { 51 | val regex = Regex(".*$dependency\\.jar") 52 | return ClassGraph().classpathFiles.first { classpath -> classpath.name.matches(regex) } 53 | } 54 | -------------------------------------------------------------------------------- /core/src/test/resources/mockito-extensions/org.mockito.plugins.MockMaker: -------------------------------------------------------------------------------- 1 | mock-maker-inline -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | kotlin.code.style=official 2 | kotlin.incremental=false 3 | kapt.include.compile.classpath=false 4 | 5 | systemProp.kct.test.useKsp2=true 6 | 7 | # Dokka flags 8 | org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled 9 | org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true 10 | 11 | GROUP=dev.zacsweers.kctfork 12 | VERSION_NAME=0.7.0-SNAPSHOT 13 | POM_DESCRIPTION=A library that enables testing of Kotlin annotation processors, compiler plugins and code generation. 14 | POM_INCEPTION_YEAR=2019 15 | POM_URL=https\://github.com/zacsweers/kotlin-compile-testing 16 | POM_SCM_URL=https\://github.com/zacsweers/kotlin-compile-testing 17 | POM_SCM_CONNECTION=scm\:git\:https\://github.com/zacsweers/kotlin-compile-testing 18 | POM_SCM_DEV_CONNECTION=scm\:git\:https\://github.com/zacsweers/kotlin-compile-testing 19 | POM_LICENCE_NAME=Mozilla Public License 2.0 20 | POM_LICENCE_URL=https\://www.mozilla.org/en-US/MPL/2.0/ 21 | POM_LICENCE_DIST=repo 22 | POM_DEVELOPER_ID=zacsweers 23 | POM_DEVELOPER_NAME=Zac Sweers 24 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | idea = "251.25410.159" # (see https://plugins.jetbrains.com/docs/intellij/android-studio-releases-list.html) 3 | kotlin = "2.1.10" 4 | kotlinpoet = "2.2.0" 5 | ksp = "2.1.10-1.0.29" 6 | 7 | [plugins] 8 | buildconfig = { id = "com.github.gmazzo.buildconfig", version = "3.1.0" } 9 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } 10 | dokka = { id = "org.jetbrains.dokka", version = "2.0.0-Beta" } 11 | ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } 12 | mavenPublish = { id = "com.vanniktech.maven.publish", version = "0.32.0" } 13 | 14 | [libraries] 15 | autoService = "com.google.auto.service:auto-service-annotations:1.1.1" 16 | autoService-ksp = "dev.zacsweers.autoservice:auto-service-ksp:1.2.0" 17 | 18 | classgraph = "io.github.classgraph:classgraph:4.8.179" 19 | 20 | intellij-core = { module = "com.jetbrains.intellij.platform:core", version.ref = "idea" } 21 | intellij-util = { module = "com.jetbrains.intellij.platform:util", version.ref = "idea" } 22 | 23 | javapoet = "com.squareup:javapoet:1.13.0" 24 | 25 | kotlin-compilerEmbeddable = { module = "org.jetbrains.kotlin:kotlin-compiler-embeddable", version.ref = "kotlin" } 26 | kotlin-annotationProcessingEmbeddable = { module = "org.jetbrains.kotlin:kotlin-annotation-processing-embeddable", version.ref = "kotlin" } 27 | kotlin-kapt4 = { module = "org.jetbrains.kotlin:kotlin-annotation-processing-compiler", version.ref = "kotlin" } 28 | kotlin-scriptingCompiler = { module = "org.jetbrains.kotlin:kotlin-scripting-compiler", version.ref = "kotlin" } 29 | kotlin-reflect = { module = "org.jetbrains.kotlin:kotlin-reflect", version.ref = "kotlin" } 30 | kotlin-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } 31 | 32 | kotlinpoet = { module = "com.squareup:kotlinpoet", version.ref = "kotlinpoet"} 33 | kotlinpoet-ksp = { module = "com.squareup:kotlinpoet-ksp", version.ref = "kotlinpoet"} 34 | 35 | ksp = { module = "com.google.devtools.ksp:symbol-processing", version.ref = "ksp" } 36 | ksp-api = { module = "com.google.devtools.ksp:symbol-processing-api", version.ref = "ksp" } 37 | ksp-aaEmbeddable = { module = "com.google.devtools.ksp:symbol-processing-aa-embeddable", version.ref = "ksp" } 38 | ksp-commonDeps = { module = "com.google.devtools.ksp:symbol-processing-common-deps", version.ref = "ksp" } 39 | 40 | okio = "com.squareup.okio:okio:3.11.0" 41 | 42 | truth = { module = "com.google.truth:truth", version = "1.4.4" } 43 | junit = "junit:junit:4.13.2" 44 | mockito = "org.mockito:mockito-core:5.17.0" 45 | mockitoKotlin = "com.nhaarman.mockitokotlin2:mockito-kotlin:2.2.0" 46 | assertJ = "org.assertj:assertj-core:3.27.3" -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ZacSweers/kotlin-compile-testing/6c6d5d40806fbe3a1b6870d1df0fcf945ea6a469/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | # SPDX-License-Identifier: Apache-2.0 19 | # 20 | 21 | ############################################################################## 22 | # 23 | # Gradle start up script for POSIX generated by Gradle. 24 | # 25 | # Important for running: 26 | # 27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 28 | # noncompliant, but you have some other compliant shell such as ksh or 29 | # bash, then to run this script, type that shell name before the whole 30 | # command line, like: 31 | # 32 | # ksh Gradle 33 | # 34 | # Busybox and similar reduced shells will NOT work, because this script 35 | # requires all of these POSIX shell features: 36 | # * functions; 37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 39 | # * compound commands having a testable exit status, especially «case»; 40 | # * various built-in commands including «command», «set», and «ulimit». 41 | # 42 | # Important for patching: 43 | # 44 | # (2) This script targets any POSIX shell, so it avoids extensions provided 45 | # by Bash, Ksh, etc; in particular arrays are avoided. 46 | # 47 | # The "traditional" practice of packing multiple parameters into a 48 | # space-separated string is a well documented source of bugs and security 49 | # problems, so this is (mostly) avoided, by progressively accumulating 50 | # options in "$@", and eventually passing that to Java. 51 | # 52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 54 | # see the in-line comments for details. 55 | # 56 | # There are tweaks for specific operating systems such as AIX, CygWin, 57 | # Darwin, MinGW, and NonStop. 58 | # 59 | # (3) This script is generated from the Groovy template 60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 61 | # within the Gradle project. 62 | # 63 | # You can find Gradle at https://github.com/gradle/gradle/. 64 | # 65 | ############################################################################## 66 | 67 | # Attempt to set APP_HOME 68 | 69 | # Resolve links: $0 may be a link 70 | app_path=$0 71 | 72 | # Need this for daisy-chained symlinks. 73 | while 74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 75 | [ -h "$app_path" ] 76 | do 77 | ls=$( ls -ld "$app_path" ) 78 | link=${ls#*' -> '} 79 | case $link in #( 80 | /*) app_path=$link ;; #( 81 | *) app_path=$APP_HOME$link ;; 82 | esac 83 | done 84 | 85 | # This is normally unused 86 | # shellcheck disable=SC2034 87 | APP_BASE_NAME=${0##*/} 88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) 89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH="\\\"\\\"" 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | if ! command -v java >/dev/null 2>&1 137 | then 138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 139 | 140 | Please set the JAVA_HOME variable in your environment to match the 141 | location of your Java installation." 142 | fi 143 | fi 144 | 145 | # Increase the maximum file descriptors if we can. 146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 147 | case $MAX_FD in #( 148 | max*) 149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 150 | # shellcheck disable=SC2039,SC3045 151 | MAX_FD=$( ulimit -H -n ) || 152 | warn "Could not query maximum file descriptor limit" 153 | esac 154 | case $MAX_FD in #( 155 | '' | soft) :;; #( 156 | *) 157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 158 | # shellcheck disable=SC2039,SC3045 159 | ulimit -n "$MAX_FD" || 160 | warn "Could not set maximum file descriptor limit to $MAX_FD" 161 | esac 162 | fi 163 | 164 | # Collect all arguments for the java command, stacking in reverse order: 165 | # * args from the command line 166 | # * the main class name 167 | # * -classpath 168 | # * -D...appname settings 169 | # * --module-path (only if needed) 170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 171 | 172 | # For Cygwin or MSYS, switch paths to Windows format before running java 173 | if "$cygwin" || "$msys" ; then 174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 176 | 177 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 178 | 179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 180 | for arg do 181 | if 182 | case $arg in #( 183 | -*) false ;; # don't mess with options #( 184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 185 | [ -e "$t" ] ;; #( 186 | *) false ;; 187 | esac 188 | then 189 | arg=$( cygpath --path --ignore --mixed "$arg" ) 190 | fi 191 | # Roll the args list around exactly as many times as the number of 192 | # args, so each arg winds up back in the position where it started, but 193 | # possibly modified. 194 | # 195 | # NB: a `for` loop captures its iteration list before it begins, so 196 | # changing the positional parameters here affects neither the number of 197 | # iterations, nor the values presented in `arg`. 198 | shift # remove old arg 199 | set -- "$@" "$arg" # push replacement arg 200 | done 201 | fi 202 | 203 | 204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 206 | 207 | # Collect all arguments for the java command: 208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, 209 | # and any embedded shellness will be escaped. 210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be 211 | # treated as '${Hostname}' itself on the command line. 212 | 213 | set -- \ 214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 215 | -classpath "$CLASSPATH" \ 216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ 217 | "$@" 218 | 219 | # Stop when "xargs" is not available. 220 | if ! command -v xargs >/dev/null 2>&1 221 | then 222 | die "xargs is not available" 223 | fi 224 | 225 | # Use "xargs" to parse quoted args. 226 | # 227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 228 | # 229 | # In Bash we could simply go: 230 | # 231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 232 | # set -- "${ARGS[@]}" "$@" 233 | # 234 | # but POSIX shell has neither arrays nor command substitution, so instead we 235 | # post-process each arg (as a line of input to sed) to backslash-escape any 236 | # character that might be a shell metacharacter, then use eval to reverse 237 | # that process (while maintaining the separation between arguments), and wrap 238 | # the whole thing up as a single "set" statement. 239 | # 240 | # This will of course break if any of these variables contains a newline or 241 | # an unmatched quote. 242 | # 243 | 244 | eval "set -- $( 245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 246 | xargs -n1 | 247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 248 | tr '\n' ' ' 249 | )" '"$@"' 250 | 251 | exec "$JAVACMD" "$@" 252 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH= 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /ksp/build.gradle.kts: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile 2 | 3 | plugins { 4 | kotlin("jvm") 5 | alias(libs.plugins.mavenPublish) 6 | } 7 | 8 | tasks 9 | .withType() 10 | .matching { it.name.contains("test", ignoreCase = true) } 11 | .configureEach { 12 | compilerOptions { optIn.add("org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi") } 13 | } 14 | 15 | dependencies { 16 | api(projects.core) 17 | api(libs.ksp.api) 18 | 19 | implementation(libs.ksp) 20 | implementation(libs.ksp.commonDeps) 21 | implementation(libs.ksp.aaEmbeddable) 22 | 23 | testImplementation(libs.kotlinpoet.ksp) 24 | testImplementation(libs.autoService) { 25 | because("To test accessing inherited classpath symbols") 26 | } 27 | testImplementation(libs.kotlin.junit) 28 | testImplementation(libs.mockito) 29 | testImplementation(libs.mockitoKotlin) 30 | testImplementation(libs.assertJ) 31 | } 32 | -------------------------------------------------------------------------------- /ksp/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_ARTIFACT_ID=ksp 2 | POM_NAME=Kotlin Compile Testing (KSP) 3 | POM_DESCRIPTION=Kotlin Compile Testing (KSP) -------------------------------------------------------------------------------- /ksp/src/main/kotlin/com/tschuchort/compiletesting/Ksp.kt: -------------------------------------------------------------------------------- 1 | /** Adds support for KSP (https://goo.gle/ksp). */ 2 | package com.tschuchort.compiletesting 3 | 4 | import com.google.devtools.ksp.AbstractKotlinSymbolProcessingExtension 5 | import com.google.devtools.ksp.KspOptions 6 | import com.google.devtools.ksp.processing.KSPLogger 7 | import com.google.devtools.ksp.processing.SymbolProcessorProvider 8 | import com.google.devtools.ksp.processing.impl.MessageCollectorBasedKSPLogger 9 | import org.jetbrains.kotlin.cli.common.CLIConfigurationKeys 10 | import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity 11 | import org.jetbrains.kotlin.cli.jvm.config.JavaSourceRoot 12 | import org.jetbrains.kotlin.com.intellij.core.CoreApplicationEnvironment 13 | import org.jetbrains.kotlin.com.intellij.mock.MockProject 14 | import org.jetbrains.kotlin.com.intellij.openapi.Disposable 15 | import org.jetbrains.kotlin.com.intellij.psi.PsiTreeChangeAdapter 16 | import org.jetbrains.kotlin.com.intellij.psi.PsiTreeChangeListener 17 | import org.jetbrains.kotlin.compiler.plugin.ComponentRegistrar 18 | import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi 19 | import org.jetbrains.kotlin.config.CompilerConfiguration 20 | import org.jetbrains.kotlin.config.languageVersionSettings 21 | import org.jetbrains.kotlin.resolve.jvm.extensions.AnalysisHandlerExtension 22 | import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull 23 | import java.io.File 24 | import java.util.EnumSet 25 | 26 | /** Configure the given KSP tool for this compilation. */ 27 | @OptIn(ExperimentalCompilerApi::class) 28 | fun KotlinCompilation.configureKsp(useKsp2: Boolean = false, body: KspTool.() -> Unit) { 29 | if (useKsp2) { 30 | useKsp2() 31 | } 32 | getKspTool().body() 33 | } 34 | 35 | /** The list of symbol processors for the kotlin compilation. https://goo.gle/ksp */ 36 | @OptIn(ExperimentalCompilerApi::class) 37 | var KotlinCompilation.symbolProcessorProviders: MutableList 38 | get() = getKspTool().symbolProcessorProviders 39 | set(value) { 40 | val tool = getKspTool() 41 | tool.symbolProcessorProviders.clear() 42 | tool.symbolProcessorProviders.addAll(value) 43 | } 44 | 45 | /** The directory where generated KSP sources are written */ 46 | @OptIn(ExperimentalCompilerApi::class) 47 | val KotlinCompilation.kspSourcesDir: File 48 | get() = kspWorkingDir.resolve("sources") 49 | 50 | /** Arbitrary arguments to be passed to ksp */ 51 | @OptIn(ExperimentalCompilerApi::class) 52 | @Deprecated( 53 | "Use kspProcessorOptions", 54 | replaceWith = 55 | ReplaceWith("kspProcessorOptions", "com.tschuchort.compiletesting.kspProcessorOptions"), 56 | ) 57 | var KotlinCompilation.kspArgs: MutableMap 58 | get() = kspProcessorOptions 59 | set(options) { 60 | kspProcessorOptions = options 61 | } 62 | 63 | /** Arbitrary processor options to be passed to ksp */ 64 | @OptIn(ExperimentalCompilerApi::class) 65 | var KotlinCompilation.kspProcessorOptions: MutableMap 66 | get() = getKspTool().processorOptions 67 | set(options) { 68 | val tool = getKspTool() 69 | tool.processorOptions.clear() 70 | tool.processorOptions.putAll(options) 71 | } 72 | 73 | /** Controls for enabling incremental processing in KSP. */ 74 | @OptIn(ExperimentalCompilerApi::class) 75 | var KotlinCompilation.kspIncremental: Boolean 76 | get() = getKspTool().incremental 77 | set(value) { 78 | val tool = getKspTool() 79 | tool.incremental = value 80 | } 81 | 82 | /** Controls for enabling incremental processing logs in KSP. */ 83 | @OptIn(ExperimentalCompilerApi::class) 84 | var KotlinCompilation.kspIncrementalLog: Boolean 85 | get() = getKspTool().incrementalLog 86 | set(value) { 87 | val tool = getKspTool() 88 | tool.incrementalLog = value 89 | } 90 | 91 | /** Controls for enabling all warnings as errors in KSP. */ 92 | @OptIn(ExperimentalCompilerApi::class) 93 | var KotlinCompilation.kspAllWarningsAsErrors: Boolean 94 | get() = getKspTool().allWarningsAsErrors 95 | set(value) { 96 | val tool = getKspTool() 97 | tool.allWarningsAsErrors = value 98 | } 99 | 100 | /** 101 | * Run processors and compilation in a single compiler invocation if true. See 102 | * [com.google.devtools.ksp.KspCliOption.WITH_COMPILATION_OPTION]. 103 | */ 104 | @OptIn(ExperimentalCompilerApi::class) 105 | var KotlinCompilation.kspWithCompilation: Boolean 106 | get() = getKspTool().withCompilation 107 | set(value) { 108 | val tool = getKspTool() 109 | tool.withCompilation = value 110 | } 111 | 112 | /** Sets logging levels for KSP. Default is all. */ 113 | @OptIn(ExperimentalCompilerApi::class) 114 | var KotlinCompilation.kspLoggingLevels: Set 115 | get() = getKspTool().loggingLevels 116 | set(value) { 117 | val tool = getKspTool() 118 | tool.loggingLevels = value 119 | } 120 | 121 | @ExperimentalCompilerApi 122 | val JvmCompilationResult.sourcesGeneratedBySymbolProcessor: Sequence 123 | get() = outputDirectory.parentFile.resolve("ksp/sources") 124 | .walkTopDown() 125 | .filter { it.isFile } 126 | 127 | @OptIn(ExperimentalCompilerApi::class) 128 | internal val KotlinCompilation.kspJavaSourceDir: File 129 | get() = kspSourcesDir.resolve("java") 130 | 131 | @OptIn(ExperimentalCompilerApi::class) 132 | internal val KotlinCompilation.kspKotlinSourceDir: File 133 | get() = kspSourcesDir.resolve("kotlin") 134 | 135 | @OptIn(ExperimentalCompilerApi::class) 136 | internal val KotlinCompilation.kspResources: File 137 | get() = kspSourcesDir.resolve("resources") 138 | 139 | /** The working directory for KSP */ 140 | @OptIn(ExperimentalCompilerApi::class) 141 | internal val KotlinCompilation.kspWorkingDir: File 142 | get() = workingDir.resolve("ksp") 143 | 144 | /** The directory where compiled KSP classes are written */ 145 | // TODO this seems to be ignored by KSP and it is putting classes into regular classes directory 146 | // but we still need to provide it in the KSP options builder as it is required 147 | // once it works, we should make the property public. 148 | @OptIn(ExperimentalCompilerApi::class) 149 | internal val KotlinCompilation.kspClassesDir: File 150 | get() = kspWorkingDir.resolve("classes") 151 | 152 | /** The directory where compiled KSP caches are written */ 153 | @OptIn(ExperimentalCompilerApi::class) 154 | internal val KotlinCompilation.kspCachesDir: File 155 | get() = kspWorkingDir.resolve("caches") 156 | 157 | /** 158 | * Custom subclass of [AbstractKotlinSymbolProcessingExtension] where processors are pre-defined 159 | * instead of being loaded via ServiceLocator. 160 | */ 161 | private class KspTestExtension( 162 | options: KspOptions, 163 | processorProviders: List, 164 | logger: KSPLogger, 165 | ) : AbstractKotlinSymbolProcessingExtension(options = options, logger = logger, testMode = false) { 166 | private val loadedProviders = processorProviders 167 | 168 | override fun loadProviders(rootDisposable: Disposable): List = loadedProviders 169 | } 170 | 171 | /** Registers the [KspTestExtension] to load the given list of processors. */ 172 | @OptIn(ExperimentalCompilerApi::class) 173 | internal class KspCompileTestingComponentRegistrar(private val compilation: KotlinCompilation) : 174 | ComponentRegistrar, KspTool { 175 | override var symbolProcessorProviders = mutableListOf() 176 | override var processorOptions = mutableMapOf() 177 | override var incremental: Boolean = false 178 | override var incrementalLog: Boolean = false 179 | override var allWarningsAsErrors: Boolean = false 180 | override var withCompilation: Boolean = false 181 | override var loggingLevels: Set = 182 | EnumSet.allOf(CompilerMessageSeverity::class.java) 183 | 184 | override fun registerProjectComponents( 185 | project: MockProject, 186 | configuration: CompilerConfiguration, 187 | ) { 188 | if (symbolProcessorProviders.isEmpty()) { 189 | return 190 | } 191 | val options = 192 | KspOptions.Builder() 193 | .apply { 194 | this.projectBaseDir = compilation.kspWorkingDir 195 | 196 | this.processingOptions.putAll(compilation.kspArgs) 197 | 198 | this.incremental = this@KspCompileTestingComponentRegistrar.incremental 199 | this.incrementalLog = this@KspCompileTestingComponentRegistrar.incrementalLog 200 | this.allWarningsAsErrors = this@KspCompileTestingComponentRegistrar.allWarningsAsErrors 201 | this.withCompilation = this@KspCompileTestingComponentRegistrar.withCompilation 202 | 203 | this.cachesDir = 204 | compilation.kspCachesDir.also { 205 | it.deleteRecursively() 206 | it.mkdirs() 207 | } 208 | this.kspOutputDir = 209 | compilation.kspSourcesDir.also { 210 | it.deleteRecursively() 211 | it.mkdirs() 212 | } 213 | this.classOutputDir = 214 | compilation.kspClassesDir.also { 215 | it.deleteRecursively() 216 | it.mkdirs() 217 | } 218 | this.javaOutputDir = 219 | compilation.kspJavaSourceDir.also { 220 | it.deleteRecursively() 221 | it.mkdirs() 222 | compilation.registerGeneratedSourcesDir(it) 223 | } 224 | this.kotlinOutputDir = 225 | compilation.kspKotlinSourceDir.also { 226 | it.deleteRecursively() 227 | it.mkdirs() 228 | } 229 | this.resourceOutputDir = 230 | compilation.kspResources.also { 231 | it.deleteRecursively() 232 | it.mkdirs() 233 | } 234 | this.languageVersionSettings = configuration.languageVersionSettings 235 | configuration[CLIConfigurationKeys.CONTENT_ROOTS] 236 | ?.filterIsInstance() 237 | ?.forEach { this.javaSourceRoots.add(it.file) } 238 | } 239 | .build() 240 | 241 | // Temporary until friend-paths is fully supported https://youtrack.jetbrains.com/issue/KT-34102 242 | @Suppress("invisible_member", "invisible_reference") 243 | val messageCollector = compilation.createMessageCollectorAccess("ksp") 244 | val messageCollectorBasedKSPLogger = 245 | MessageCollectorBasedKSPLogger( 246 | messageCollector = messageCollector, 247 | wrappedMessageCollector = messageCollector, 248 | allWarningsAsErrors = allWarningsAsErrors, 249 | ) 250 | val registrar = 251 | KspTestExtension(options, symbolProcessorProviders, messageCollectorBasedKSPLogger) 252 | AnalysisHandlerExtension.registerExtension(project, registrar) 253 | // Dummy extension point; Required by dropPsiCaches(). 254 | CoreApplicationEnvironment.registerExtensionPoint( 255 | project.extensionArea, 256 | PsiTreeChangeListener.EP.name, 257 | PsiTreeChangeAdapter::class.java, 258 | ) 259 | } 260 | } 261 | 262 | /** Gets the test registrar from the plugin list or adds if it does not exist. */ 263 | @OptIn(ExperimentalCompilerApi::class) 264 | internal fun KotlinCompilation.getKspRegistrar(): KspCompileTestingComponentRegistrar { 265 | componentRegistrars.firstIsInstanceOrNull()?.let { 266 | return it 267 | } 268 | val kspRegistrar = KspCompileTestingComponentRegistrar(this) 269 | componentRegistrars += kspRegistrar 270 | return kspRegistrar 271 | } 272 | -------------------------------------------------------------------------------- /ksp/src/main/kotlin/com/tschuchort/compiletesting/Ksp2.kt: -------------------------------------------------------------------------------- 1 | /* Adds support for KSP (https://goo.gle/ksp). */ 2 | package com.tschuchort.compiletesting 3 | 4 | import com.google.devtools.ksp.impl.KotlinSymbolProcessing 5 | import com.google.devtools.ksp.processing.KSPJvmConfig 6 | import com.google.devtools.ksp.processing.SymbolProcessorProvider 7 | import java.io.File 8 | import java.io.PrintStream 9 | import java.util.EnumSet 10 | import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity 11 | import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi 12 | 13 | @ExperimentalCompilerApi 14 | class Ksp2PrecursorTool : PrecursorTool, KspTool { 15 | override var withCompilation: Boolean 16 | get() = false 17 | set(value) { 18 | // Irrelevant/unavailable on KSP 2 19 | } 20 | 21 | override val symbolProcessorProviders: MutableList = mutableListOf() 22 | override val processorOptions: MutableMap = mutableMapOf() 23 | override var incremental: Boolean = false 24 | override var incrementalLog: Boolean = false 25 | override var allWarningsAsErrors: Boolean = false 26 | override var loggingLevels: Set = 27 | EnumSet.allOf(CompilerMessageSeverity::class.java) 28 | 29 | // Extra hook for direct configuration of KspJvmConfig.Builder, for advanced use cases 30 | var onBuilder: (KSPJvmConfig.Builder.() -> Unit)? = null 31 | 32 | override fun execute( 33 | compilation: KotlinCompilation, 34 | output: PrintStream, 35 | sources: List, 36 | ): KotlinCompilation.ExitCode { 37 | if (symbolProcessorProviders.isEmpty()) { 38 | return KotlinCompilation.ExitCode.OK 39 | } 40 | 41 | val config = 42 | KSPJvmConfig.Builder() 43 | .apply { 44 | projectBaseDir = compilation.kspWorkingDir.absoluteFile 45 | 46 | incremental = this@Ksp2PrecursorTool.incremental 47 | incrementalLog = this@Ksp2PrecursorTool.incrementalLog 48 | allWarningsAsErrors = this@Ksp2PrecursorTool.allWarningsAsErrors 49 | processorOptions = this@Ksp2PrecursorTool.processorOptions.toMap() 50 | 51 | jvmTarget = compilation.jvmTarget 52 | jdkHome = compilation.jdkHome 53 | languageVersion = compilation.languageVersion ?: KotlinVersion.CURRENT.languageVersion() 54 | apiVersion = compilation.apiVersion ?: KotlinVersion.CURRENT.languageVersion() 55 | 56 | // TODO adopt new roots model 57 | moduleName = compilation.moduleName ?: "main" 58 | sourceRoots = sources.filter { it.extension == "kt" }.mapNotNull { it.parentFile.absoluteFile }.distinct() 59 | javaSourceRoots = sources.filter { it.extension == "java" }.mapNotNull { it.parentFile.absoluteFile }.distinct() 60 | @Suppress("invisible_member", "invisible_reference") 61 | libraries = compilation.classpaths + compilation.commonClasspaths() 62 | 63 | cachesDir = 64 | compilation.kspCachesDir.also { 65 | it.deleteRecursively() 66 | it.mkdirs() 67 | }.absoluteFile 68 | outputBaseDir = 69 | compilation.kspSourcesDir.also { 70 | it.deleteRecursively() 71 | it.mkdirs() 72 | }.absoluteFile 73 | classOutputDir = 74 | compilation.kspClassesDir.also { 75 | it.deleteRecursively() 76 | it.mkdirs() 77 | }.absoluteFile 78 | javaOutputDir = 79 | compilation.kspJavaSourceDir.also { 80 | it.deleteRecursively() 81 | it.mkdirs() 82 | compilation.registerGeneratedSourcesDir(it) 83 | }.absoluteFile 84 | kotlinOutputDir = 85 | compilation.kspKotlinSourceDir.also { 86 | it.deleteRecursively() 87 | it.mkdirs() 88 | compilation.registerGeneratedSourcesDir(it) 89 | }.absoluteFile 90 | resourceOutputDir = 91 | compilation.kspResources.also { 92 | it.deleteRecursively() 93 | it.mkdirs() 94 | }.absoluteFile 95 | 96 | onBuilder?.invoke(this) 97 | } 98 | .build() 99 | 100 | // Temporary until friend-paths is fully supported https://youtrack.jetbrains.com/issue/KT-34102 101 | @Suppress("invisible_member", "invisible_reference") 102 | val messageCollector = compilation.createMessageCollectorAccess("ksp") 103 | val logger = 104 | TestKSPLogger( 105 | messageCollector = messageCollector, 106 | allWarningsAsErrors = config.allWarningsAsErrors, 107 | ) 108 | 109 | return try { 110 | when (KotlinSymbolProcessing(config, symbolProcessorProviders.toList(), logger).execute()) { 111 | KotlinSymbolProcessing.ExitCode.OK -> KotlinCompilation.ExitCode.OK 112 | KotlinSymbolProcessing.ExitCode.PROCESSING_ERROR -> 113 | KotlinCompilation.ExitCode.COMPILATION_ERROR 114 | } 115 | } finally { 116 | logger.reportAll() 117 | } 118 | } 119 | } 120 | 121 | private fun KotlinVersion.languageVersion(): String { 122 | return "$major.$minor" 123 | } 124 | 125 | /** Enables KSP2. */ 126 | @OptIn(ExperimentalCompilerApi::class) 127 | fun KotlinCompilation.useKsp2() { 128 | precursorTools.getOrPut("ksp2", ::Ksp2PrecursorTool) 129 | } 130 | -------------------------------------------------------------------------------- /ksp/src/main/kotlin/com/tschuchort/compiletesting/KspTool.kt: -------------------------------------------------------------------------------- 1 | package com.tschuchort.compiletesting 2 | 3 | import com.google.devtools.ksp.processing.SymbolProcessorProvider 4 | import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity 5 | import org.jetbrains.kotlin.compiler.plugin.ExperimentalCompilerApi 6 | 7 | sealed interface KspTool { 8 | val symbolProcessorProviders: MutableList 9 | val processorOptions: MutableMap 10 | var incremental: Boolean 11 | var incrementalLog: Boolean 12 | var allWarningsAsErrors: Boolean 13 | var withCompilation: Boolean 14 | var loggingLevels: Set 15 | } 16 | 17 | /** Gets or creates the [KspTool] if it doesn't exist. */ 18 | @OptIn(ExperimentalCompilerApi::class) 19 | internal fun KotlinCompilation.getKspTool(): KspTool { 20 | val ksp2Tool = precursorTools["ksp2"] as? Ksp2PrecursorTool? 21 | return ksp2Tool ?: getKspRegistrar() 22 | } 23 | -------------------------------------------------------------------------------- /ksp/src/main/kotlin/com/tschuchort/compiletesting/TestKSPLogger.kt: -------------------------------------------------------------------------------- 1 | package com.tschuchort.compiletesting 2 | 3 | import com.google.devtools.ksp.processing.KSPLogger 4 | import com.google.devtools.ksp.symbol.FileLocation 5 | import com.google.devtools.ksp.symbol.KSNode 6 | import com.google.devtools.ksp.symbol.NonExistLocation 7 | import java.io.PrintWriter 8 | import java.io.StringWriter 9 | import org.jetbrains.kotlin.cli.common.messages.CompilerMessageSeverity 10 | import org.jetbrains.kotlin.cli.common.messages.MessageCollector 11 | 12 | internal class TestKSPLogger( 13 | private val messageCollector: MessageCollector, 14 | private val allWarningsAsErrors: Boolean, 15 | ) : KSPLogger { 16 | 17 | companion object { 18 | const val PREFIX = "[ksp] " 19 | } 20 | 21 | data class Event(val severity: CompilerMessageSeverity, val message: String) 22 | 23 | val recordedEvents = mutableListOf() 24 | 25 | private val reportToCompilerSeverity = 26 | setOf(CompilerMessageSeverity.ERROR, CompilerMessageSeverity.EXCEPTION) 27 | 28 | private var reportedToCompiler = false 29 | 30 | private fun convertMessage(message: String, symbol: KSNode?): String = 31 | when (val location = symbol?.location) { 32 | is FileLocation -> "$PREFIX${location.filePath}:${location.lineNumber}: $message" 33 | is NonExistLocation, 34 | null -> "$PREFIX$message" 35 | } 36 | 37 | override fun logging(message: String, symbol: KSNode?) { 38 | recordedEvents.add(Event(CompilerMessageSeverity.LOGGING, convertMessage(message, symbol))) 39 | } 40 | 41 | override fun info(message: String, symbol: KSNode?) { 42 | recordedEvents.add(Event(CompilerMessageSeverity.INFO, convertMessage(message, symbol))) 43 | } 44 | 45 | override fun warn(message: String, symbol: KSNode?) { 46 | val severity = 47 | if (allWarningsAsErrors) CompilerMessageSeverity.ERROR else CompilerMessageSeverity.WARNING 48 | recordedEvents.add(Event(severity, convertMessage(message, symbol))) 49 | } 50 | 51 | override fun error(message: String, symbol: KSNode?) { 52 | recordedEvents.add(Event(CompilerMessageSeverity.ERROR, convertMessage(message, symbol))) 53 | } 54 | 55 | override fun exception(e: Throwable) { 56 | val writer = StringWriter() 57 | e.printStackTrace(PrintWriter(writer)) 58 | recordedEvents.add(Event(CompilerMessageSeverity.EXCEPTION, writer.toString())) 59 | } 60 | 61 | fun reportAll() { 62 | for (event in recordedEvents) { 63 | if (!reportedToCompiler && event.severity in reportToCompilerSeverity) { 64 | reportedToCompiler = true 65 | messageCollector.report(event.severity, "Error occurred in KSP, check log for detail") 66 | } 67 | messageCollector.report(event.severity, event.message) 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /ksp/src/test/kotlin/com/tschuchort/compiletesting/AbstractTestSymbolProcessor.kt: -------------------------------------------------------------------------------- 1 | package com.tschuchort.compiletesting 2 | 3 | import com.google.devtools.ksp.processing.CodeGenerator 4 | import com.google.devtools.ksp.processing.Resolver 5 | import com.google.devtools.ksp.processing.SymbolProcessor 6 | import com.google.devtools.ksp.processing.SymbolProcessorProvider 7 | import com.google.devtools.ksp.symbol.KSAnnotated 8 | 9 | fun simpleProcessor(process: (resolver: Resolver, codeGenerator: CodeGenerator) -> Unit) = 10 | SymbolProcessorProvider { env -> 11 | object : SymbolProcessor { 12 | override fun process(resolver: Resolver): List { 13 | process(resolver, env.codeGenerator) 14 | return emptyList() 15 | } 16 | } 17 | } 18 | 19 | /** Helper class to write tests, only used in Ksp Compile Testing tests, not a public API. */ 20 | internal open class AbstractTestSymbolProcessor(protected val codeGenerator: CodeGenerator) : 21 | SymbolProcessor { 22 | override fun process(resolver: Resolver): List { 23 | return emptyList() 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /ksp/src/test/kotlin/com/tschuchort/compiletesting/TestClasses.kt: -------------------------------------------------------------------------------- 1 | package com.tschuchort.compiletesting 2 | 3 | enum class AnnotationEnumValue { 4 | ONE, TWO, THREE 5 | } 6 | 7 | annotation class AnotherAnnotation(val input: String) 8 | 9 | annotation class ClasspathTestAnnotation( 10 | val enumValue: AnnotationEnumValue, 11 | val enumValueArray: Array, 12 | val anotherAnnotation: AnotherAnnotation, 13 | val anotherAnnotationArray: Array, 14 | ) -------------------------------------------------------------------------------- /release.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -exo pipefail 4 | 5 | # Gets a property out of a .properties file 6 | # usage: getProperty $key $filename 7 | function getProperty() { 8 | grep "${1}" "$2" | cut -d'=' -f2 9 | } 10 | 11 | NEW_VERSION=$1 12 | SNAPSHOT_VERSION=$(getProperty 'VERSION_NAME' gradle.properties) 13 | 14 | echo "Publishing $NEW_VERSION" 15 | 16 | # Prepare release 17 | sed -i '' "s/${SNAPSHOT_VERSION}/${NEW_VERSION}/g" gradle.properties 18 | git commit -am "Prepare for release $NEW_VERSION." 19 | git tag -a "$NEW_VERSION" -m "Version $NEW_VERSION" 20 | 21 | # Publish 22 | ./gradlew publish --no-configuration-cache 23 | 24 | # Prepare next snapshot 25 | echo "Restoring snapshot version $SNAPSHOT_VERSION" 26 | sed -i '' "s/${NEW_VERSION}/${SNAPSHOT_VERSION}/g" gradle.properties 27 | git commit -am "Prepare next development version." 28 | 29 | # Push it all up 30 | git push && git push --tags -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | dependencyResolutionManagement { 2 | versionCatalogs { 3 | if (System.getenv("DEP_OVERRIDES") == "true") { 4 | val overrides = System.getenv().filterKeys { it.startsWith("DEP_OVERRIDE_") } 5 | maybeCreate("libs").apply { 6 | for ((key, value) in overrides) { 7 | val catalogKey = key.removePrefix("DEP_OVERRIDE_").lowercase() 8 | println("Overriding $catalogKey with $value") 9 | version(catalogKey, value) 10 | } 11 | } 12 | } 13 | } 14 | 15 | // Non-delegate APIs are annoyingly not public so we have to use withGroovyBuilder 16 | fun hasProperty(key: String): Boolean { 17 | return settings.withGroovyBuilder { 18 | "hasProperty"(key) as Boolean 19 | } 20 | } 21 | 22 | repositories { 23 | // Repos are declared roughly in order of likely to hit. 24 | 25 | // Snapshots/local go first in order to pre-empty other repos that may contain unscrupulous 26 | // snapshots. 27 | if (hasProperty("kct.config.enableSnapshots")) { 28 | maven("https://oss.sonatype.org/content/repositories/snapshots") 29 | maven("https://androidx.dev/snapshots/latest/artifacts/repository") 30 | } 31 | 32 | if (hasProperty("kct.config.enableMavenLocal")) { 33 | mavenLocal() 34 | } 35 | 36 | mavenCentral() 37 | 38 | google() 39 | 40 | maven("https://www.jetbrains.com/intellij-repository/releases") { 41 | name = "Intellij" 42 | } 43 | 44 | maven("https://cache-redirector.jetbrains.com/intellij-dependencies") { 45 | name = "Intellij" 46 | } 47 | } 48 | } 49 | 50 | pluginManagement { 51 | // Non-delegate APIs are annoyingly not public so we have to use withGroovyBuilder 52 | fun hasProperty(key: String): Boolean { 53 | return settings.withGroovyBuilder { 54 | "hasProperty"(key) as Boolean 55 | } 56 | } 57 | 58 | repositories { 59 | // Repos are declared roughly in order of likely to hit. 60 | 61 | // Snapshots/local go first in order to pre-empty other repos that may contain unscrupulous 62 | // snapshots. 63 | if (hasProperty("kct.config.enableSnapshots")) { 64 | maven("https://oss.sonatype.org/content/repositories/snapshots") 65 | maven("https://androidx.dev/snapshots/latest/artifacts/repository") 66 | } 67 | 68 | if (hasProperty("kct.config.enableMavenLocal")) { 69 | mavenLocal() 70 | } 71 | 72 | mavenCentral() 73 | 74 | google() 75 | 76 | // Gradle's plugin portal proxies jcenter, which we don't want. To avoid this, we specify 77 | // exactly which dependencies to pull from here. 78 | exclusiveContent { 79 | forRepository(::gradlePluginPortal) 80 | filter { 81 | includeModule("com.gradle", "gradle-enterprise-gradle-plugin") 82 | includeModule("com.gradle.enterprise", "com.gradle.enterprise.gradle.plugin") 83 | includeModule("com.diffplug.spotless", "com.diffplug.spotless.gradle.plugin") 84 | includeModule("org.gradle.kotlin.kotlin-dsl", "org.gradle.kotlin.kotlin-dsl.gradle.plugin") 85 | includeModule("org.gradle.kotlin", "gradle-kotlin-dsl-plugins") 86 | includeModule("com.github.gmazzo.buildconfig", "com.github.gmazzo.buildconfig.gradle.plugin") 87 | includeModule("com.github.gmazzo", "gradle-buildconfig-plugin") 88 | } 89 | } 90 | } 91 | plugins { id("com.gradle.enterprise") version "3.17.4" } 92 | } 93 | 94 | rootProject.name = "kotlin-compile-testing" 95 | include("ksp") 96 | include("core") 97 | 98 | // https://docs.gradle.org/current/userguide/declaring_dependencies.html#sec:type-safe-project-accessors 99 | enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") --------------------------------------------------------------------------------