├── .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 extends TypeElement> 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")
--------------------------------------------------------------------------------