├── BUILD ├── LICENSE ├── README.md ├── WORKSPACE ├── convert ├── BUILD └── src │ └── com │ └── google │ └── devtools │ └── jvmtools │ ├── analysis │ ├── Cfg.kt │ ├── InterproceduralAnalysis.kt │ ├── Reachability.kt │ ├── Tuple.kt │ ├── UAnalysis.kt │ ├── UTransferFunction.kt │ ├── mutation │ │ └── CollectionMutationAnalysis.kt │ └── nullness │ │ ├── Nullness.kt │ │ ├── NullnessAnalysis.kt │ │ ├── NullnessAnnotations.kt │ │ └── TypeArgumentVisitor.kt │ └── convert │ ├── psi2k │ ├── JavaPsiUtils.kt │ ├── MappedMethod.kt │ └── Psi2kTranslator.kt │ └── util │ └── ControlFlow.kt ├── docs └── contributing.md └── repositories.bzl /BUILD: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | load("@rules_kotlin//kotlin:core.bzl", "define_kt_toolchain") 16 | 17 | alias( 18 | name = "com_github_google_kotlin_convert", 19 | actual = "//convert", 20 | ) 21 | 22 | define_kt_toolchain( 23 | name = "kotlin_toolchain", 24 | jvm_target = "11", 25 | ) 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Java-to-Kotlin Conversion 2 | 3 | This repository contains code for a basic Java-to-Kotlin source translator that 4 | aims to preserve the original API as much as possible. It is intended to be used 5 | in conjunction with other steps/tools as follows: 6 | 7 | 1. prepare code to be translated (e.g., by adding nullness annotations) 8 | 2. apply the translator 9 | 3. run an auto-formatter 10 | 4. apply improvements to make the code more typical (e.g., use `data` classes) 11 | 12 | We don't currently provide any support for this repository, but you are welcome 13 | to [contribute](docs/contributing.md). 14 | 15 | This is not an officially supported Google product, and it is not affiliated 16 | with either Oracle or the Kotlin Foundation. 17 | 18 | The Java programming language was developed by Oracle. 19 | -------------------------------------------------------------------------------- /WORKSPACE: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | workspace(name = "com_github_google_kotlin_convert") 16 | 17 | # Repositories 18 | load("//:repositories.bzl", "rules_jvm_external", "rules_kotlin") 19 | 20 | rules_jvm_external() 21 | 22 | load("@rules_jvm_external//:repositories.bzl", "rules_jvm_external_deps") 23 | 24 | rules_jvm_external_deps() 25 | 26 | load("@rules_jvm_external//:setup.bzl", "rules_jvm_external_setup") 27 | 28 | rules_jvm_external_setup() 29 | 30 | load("@rules_jvm_external//:defs.bzl", "maven_install") 31 | 32 | # Maven 33 | maven_install( 34 | artifacts = [ 35 | "com.github.ben-manes.caffeine:caffeine:3.1.8", 36 | "com.jetbrains.intellij.platform:uast:241.15989.155", 37 | "org.jetbrains.kotlin:kotlin-compiler:1.9.23", 38 | ], 39 | repositories = [ 40 | "https://repo.maven.apache.org/maven2/", 41 | "https://www.jetbrains.com/intellij-repository/releases/", 42 | "https://cache-redirector.jetbrains.com/intellij-dependencies/", 43 | ], 44 | ) 45 | 46 | # Kotlin 47 | rules_kotlin() 48 | 49 | load("@rules_kotlin//kotlin:repositories.bzl", "kotlin_repositories") 50 | 51 | kotlin_repositories() 52 | 53 | register_toolchains("//:kotlin_toolchain") 54 | -------------------------------------------------------------------------------- /convert/BUILD: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | load("@rules_kotlin//kotlin:jvm.bzl", "kt_jvm_library") 16 | 17 | kt_jvm_library( 18 | name = "convert", 19 | srcs = glob(["src/**/*.kt"]), 20 | visibility = ["//:__pkg__"], 21 | deps = [ 22 | "@maven//:com_github_ben_manes_caffeine_caffeine", 23 | "@maven//:com_jetbrains_intellij_platform_uast", 24 | "@maven//:org_jetbrains_kotlin_kotlin_compiler", 25 | ], 26 | ) 27 | -------------------------------------------------------------------------------- /convert/src/com/google/devtools/jvmtools/analysis/Cfg.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devtools.jvmtools.analysis 18 | 19 | import com.google.common.collect.HashBasedTable 20 | import com.google.common.collect.HashMultimap 21 | import com.google.common.collect.ImmutableMultimap 22 | import com.google.errorprone.annotations.CanIgnoreReturnValue 23 | import com.google.errorprone.annotations.Immutable 24 | import com.intellij.psi.PsiElement 25 | import com.intellij.psi.PsiType 26 | import com.intellij.psi.PsiTypes 27 | import org.jetbrains.uast.UAnnotation 28 | import org.jetbrains.uast.UBinaryExpression 29 | import org.jetbrains.uast.UBlockExpression 30 | import org.jetbrains.uast.UBreakExpression 31 | import org.jetbrains.uast.UContinueExpression 32 | import org.jetbrains.uast.UDeclaration 33 | import org.jetbrains.uast.UDeclarationsExpression 34 | import org.jetbrains.uast.UDoWhileExpression 35 | import org.jetbrains.uast.UElement 36 | import org.jetbrains.uast.UExpression 37 | import org.jetbrains.uast.UForEachExpression 38 | import org.jetbrains.uast.UForExpression 39 | import org.jetbrains.uast.UIfExpression 40 | import org.jetbrains.uast.UImportStatement 41 | import org.jetbrains.uast.UJumpExpression 42 | import org.jetbrains.uast.ULambdaExpression 43 | import org.jetbrains.uast.ULocalVariable 44 | import org.jetbrains.uast.UMethod 45 | import org.jetbrains.uast.UParameter 46 | import org.jetbrains.uast.UPolyadicExpression 47 | import org.jetbrains.uast.UReturnExpression 48 | import org.jetbrains.uast.USwitchClauseExpression 49 | import org.jetbrains.uast.USwitchClauseExpressionWithBody 50 | import org.jetbrains.uast.USwitchExpression 51 | import org.jetbrains.uast.UThrowExpression 52 | import org.jetbrains.uast.UTryExpression 53 | import org.jetbrains.uast.UWhileExpression 54 | import org.jetbrains.uast.UastBinaryOperator 55 | import org.jetbrains.uast.visitor.UastTypedVisitor 56 | import org.jetbrains.uast.visitor.UastVisitor 57 | 58 | /** 59 | * Control flow graph built from UAST [UElement]s. 60 | * 61 | * Edges in the graph connect nodes in (possible) execution order, with nested UAST nodes generally 62 | * appearing before their parents. Nodes can have multiple [successors] at control flow branches 63 | * (e.g., after an `if` condition), and multiple predessors at control flow merges (e.g., a 64 | * [UIfExpression] can have incoming edges from the ends of each branch). 65 | * 66 | * Edges can optionally (best effort) be labeled with conditions, which in particular represent 67 | * control flow branches after Boolean conditions. While edges of this graph can't be directly 68 | * queried, [successorsWithConditions] returns successors with any labels attached to the connecting 69 | * edges. 70 | * 71 | * UAST doesn't make all operations explicit. In those cases, we insert special node objects that 72 | * don't exist in the original UAST. The additional node classes can be seen and visited in 73 | * [CfgTypedVisitor]. 74 | */ 75 | data class Cfg 76 | private constructor( 77 | /** The unique first ("entry") node in the [Cfg]. */ 78 | val entryNode: UElement, 79 | /** The unique last ("exit", or sink) node in the [Cfg]. */ 80 | val exitNode: UElement, 81 | private val forwardEdges: ImmutableMultimap, 82 | private val edgeConditions: (UElement, UElement) -> CfgEdgeLabel?, 83 | /** Allows finding [PsiElement]s in a [Cfg]. */ 84 | val sourceMapping: ImmutableMultimap, 85 | private val reversed: Boolean = false, 86 | ) { 87 | /** Returns the set of nodes that follow the given [node] in this [Cfg]. */ 88 | fun successors(node: UElement): Collection = forwardEdges[node] 89 | 90 | /** 91 | * Returns a map from [successors] of the given [node] to the labels attached to the connecting 92 | * edges, if any. 93 | * 94 | * If two nodes conceptually are connected with both a `true` and a `false` edge, the two are 95 | * collapsed into a single, unlabeled edge. 96 | */ 97 | // Internal for now b/c the CfgEdgeLabel API feels a bit wonky 98 | // TODO(kmb): Consider using `Set>` as edge labels 99 | internal fun successorsWithConditions(node: UElement): Map = 100 | forwardEdges[node].associateWith { edgeConditions(node, it) } 101 | 102 | fun reverse(): Cfg { 103 | require(!reversed) { "Don't reverse a reversed CFG, just use the original." } 104 | return Cfg( 105 | entryNode = exitNode, 106 | exitNode = entryNode, 107 | forwardEdges.inverse(), 108 | edgeConditions = { source: UElement, dest: UElement -> edgeConditions(dest, source) }, 109 | sourceMapping, 110 | reversed = true, 111 | ) 112 | } 113 | 114 | companion object { 115 | /** Derives a [Cfg] for the given method. */ 116 | fun create(root: UMethod): Cfg = createCfg(root) 117 | 118 | /** Derives a [Cfg] for the given [CfgRoot]. */ 119 | fun create(root: CfgRoot): Cfg = createCfg(root.rootNode) 120 | 121 | private fun createCfg(root: UElement): Cfg = 122 | CfgBuilder(root).run { 123 | root.accept(this) 124 | // TODO(kmb): consider eagerly dropping unreachable edges 125 | Cfg( 126 | checkNotNull(entryNode) { "No entry node found in $root" }, 127 | exitNode = root, 128 | ImmutableMultimap.copyOf(forwardEdges), 129 | edgeConditions::get, 130 | sourceMapping.build(), 131 | ) 132 | } 133 | } 134 | } 135 | 136 | /** Type safe wrapper for root node to build CFG from. */ 137 | @Immutable 138 | sealed class CfgRoot : CfgRootKey { 139 | /** The root node, whose type is refined in the sealed subtypes. */ 140 | abstract val rootNode: UElement 141 | 142 | // A CfgRoot is a trivial CfgRootKey for itself 143 | override val callee: CfgRoot 144 | get() = this 145 | 146 | data class Method(@Suppress("Immutable") override val rootNode: UMethod) : CfgRoot() 147 | 148 | data class Lambda(@Suppress("Immutable") override val rootNode: ULambdaExpression) : CfgRoot() 149 | 150 | companion object { 151 | fun of(rootNode: UMethod) = Method(rootNode) 152 | 153 | fun of(rootNode: ULambdaExpression) = Lambda(rootNode) 154 | } 155 | } 156 | 157 | /** [Cfg] node visitor that allows visiting synthetic nodes. */ 158 | interface CfgTypedVisitor : UastTypedVisitor { 159 | fun visitCfgSyntheticExpression(node: CfgSyntheticExpression, data: D): R = 160 | visitExpression(node, data) 161 | 162 | fun visitCfgSwitchBranchExpression(node: CfgSwitchBranchExpression, data: D): R = 163 | visitCfgSyntheticExpression(node, data) 164 | 165 | fun visitCfgForeachIteratedExpression(node: CfgForeachIteratedExpression, data: D): R = 166 | visitCfgSyntheticExpression(node, data) 167 | } 168 | 169 | /** 170 | * Base class for synthetic [UExpression] nodes that make implicit computation explicit in the CFG. 171 | */ 172 | abstract class CfgSyntheticExpression(val wrappedExpression: UExpression) : UExpression { 173 | @Deprecated("ambiguous psi element, use `sourcePsi` or `javaPsi`") 174 | override val psi: PsiElement? 175 | get() = null 176 | 177 | override val uAnnotations: List 178 | get() = emptyList() 179 | 180 | override val uastParent: UElement? 181 | get() = wrappedExpression.uastParent 182 | 183 | override fun asLogString(): String = javaClass.simpleName 184 | 185 | override fun toString() = asRenderString() 186 | } 187 | 188 | /** Makes multiplexing table switch on [USwitchExpression] value explicit. */ 189 | class CfgSwitchBranchExpression(expression: UExpression) : CfgSyntheticExpression(expression) { 190 | override fun accept(visitor: UastTypedVisitor, data: D): R { 191 | return if (visitor is CfgTypedVisitor) { 192 | visitor.visitCfgSwitchBranchExpression(this, data) 193 | } else { 194 | visitor.visitExpression(this, data) 195 | } 196 | } 197 | 198 | override fun asRenderString(): String = "branch(${wrappedExpression.asRenderString()})" 199 | } 200 | 201 | /** Makes dereference of iterated value in [UForEachExpression]s explicit. */ 202 | class CfgForeachIteratedExpression(expression: UExpression) : CfgSyntheticExpression(expression) { 203 | override fun accept(visitor: UastTypedVisitor, data: D): R { 204 | return if (visitor is CfgTypedVisitor) { 205 | visitor.visitCfgForeachIteratedExpression(this, data) 206 | } else { 207 | visitor.visitExpression(this, data) 208 | } 209 | } 210 | 211 | override fun asRenderString(): String = "iterate(${wrappedExpression.asRenderString()})" 212 | } 213 | 214 | /** 215 | * "Tags" for edges that don't simply represent regular, unconditional control flow. 216 | * 217 | * Note that for simplicity, a given directed edge (i.e., source-destination node pair) can only 218 | * carry at most one [CfgEdgeLabel] that represents all the ways in which that edge can be taken, 219 | * rather than associating a set of labels with each edge. We also use "no" (i.e., `null`) label for 220 | * an edge that is only taken unconditionally during regular (i.e., non-exceptional) execution, 221 | * rather than defining an object for this case (see [Cfg.successorsWithConditions]). 222 | */ 223 | internal sealed interface CfgEdgeLabel { 224 | /** 225 | * Edge only taken if the source node was a binary expression that evaluated to the edge's 226 | * [condition] (and never as a result of an exception). 227 | */ 228 | enum class CfgConditionalEdge(val condition: Boolean) : CfgEdgeLabel { 229 | FALSE(condition = false), 230 | TRUE(condition = true), 231 | } 232 | 233 | /** 234 | * Edge taken during exceptional control flow (that may or may not also be taken during regular 235 | * control flow). 236 | * 237 | * Possible exception types are collected in [thrown]. 238 | */ 239 | data class CfgExceptionalEdge internal constructor(internal val thrown_: MutableSet) : 240 | CfgEdgeLabel { 241 | /** 242 | * Exceptions flowing along this edge. A `null` element indicates that this is also a regular 243 | * control flow edge with the same source and destination, which typically only happens for 244 | * edges connecting an inner `finally` block to a surrounding one. 245 | * 246 | * An edge labeled with a `thrown` set only containing `null` would be equivalent to an edge 247 | * with no label at all, but [Cfg]s should just use edges with no labels in that case. 248 | */ 249 | // We include marker for regular control flow here because (1) this helps not forget about the 250 | // extra case in TransferResult.toInput and (2) it's rare and thus saves space. 251 | val thrown: Set 252 | get() = thrown_ 253 | } 254 | 255 | companion object { 256 | /** Returns the [CfgConditionalEdge] corresponding to the given [condition]. */ 257 | internal fun conditionalEdge(condition: Boolean) = 258 | if (condition) CfgConditionalEdge.TRUE else CfgConditionalEdge.FALSE 259 | 260 | /** Convenience factory for [CfgExceptionalEdge]s. */ 261 | internal fun exceptionalEdge(firstThrown: PsiType) = 262 | CfgExceptionalEdge(mutableSetOf(firstThrown)) 263 | } 264 | } 265 | 266 | /** 267 | * Interface for defining hashable keys that reference a [CfgRoot] in [callee]. 268 | * 269 | * Note [CfgRoot] itself can trivially implement this interface, but this allows including 270 | * additional properties in the key. Non-[equal][equals] keys can reference the same [callee]. 271 | */ 272 | // This interface is defined here to avoid circular references between source files. 273 | @Immutable 274 | interface CfgRootKey { 275 | /** [CfgRoot] referenced by this key. */ 276 | val callee: CfgRoot 277 | 278 | override fun equals(other: Any?): Boolean 279 | 280 | override fun hashCode(): Int 281 | } 282 | 283 | private class CfgBuilder(private val root: UElement) : UastVisitor { 284 | /** 285 | * Helper class for [CfgBuilder] that in particular ensures that [last] is only set using 286 | * [setLast] (i.e., together with [condition]). 287 | */ 288 | private class CfgBuilderState { 289 | fun setLast(last: UElement?, condition: Boolean? = null) { 290 | this.condition = condition 291 | this.last = requireNotNull(last) { "never set last back to null, was ${this.last}" } 292 | for (callback in markCallbacks) callback(last) 293 | markCallbacks.clear() 294 | } 295 | 296 | fun setLastWithoutCallbacks(last: UElement?, condition: Boolean? = null) { 297 | this.condition = condition 298 | this.last = last 299 | } 300 | 301 | fun popCallbacks(): List<(UElement) -> Unit> { 302 | val result = markCallbacks.toList() 303 | markCallbacks.clear() 304 | return result 305 | } 306 | 307 | var first: UElement? = null 308 | private set 309 | 310 | var last: UElement? = null 311 | private set 312 | 313 | var condition: Boolean? = null 314 | 315 | /** Lambdas invoked the next time [last] is set. List is cleared after each set. */ 316 | val markCallbacks = mutableListOf<(UElement) -> Unit>({ first = it }) 317 | } 318 | 319 | private val state = CfgBuilderState() 320 | 321 | // This property allows CfgBuilderState to be private for now 322 | val entryNode: UElement? 323 | get() = state.first 324 | 325 | val forwardEdges = HashMultimap.create() 326 | val edgeConditions = HashBasedTable.create() 327 | val sourceMapping = ImmutableMultimap.builder() 328 | 329 | /** Maps potential jump targets such as loops to their entry nodes. */ 330 | private val continuationEntries = mutableMapOf() 331 | 332 | private val throwableType: PsiType by lazy { 333 | PsiType.getJavaLangThrowable(root.sourcePsi?.manager!!, root.sourcePsi?.resolveScope!!) 334 | } 335 | 336 | private val npeType: PsiType by lazy { 337 | PsiType.getTypeByName( 338 | "java.lang.NullPointerException", 339 | root.sourcePsi?.project!!, 340 | root.sourcePsi?.resolveScope!!, 341 | ) 342 | } 343 | 344 | /** Visits the receiver node and returns its entry node. */ 345 | private fun UElement.doAccept(): UElement { 346 | lateinit var entry: UElement 347 | state.markCallbacks += { entry = it } 348 | accept(this@CfgBuilder) 349 | return entry 350 | } 351 | 352 | /** 353 | * Adds a source mapping for [node] if one exists and [skip] is `false`. 354 | * 355 | * @return [skip]'s value 356 | */ 357 | @CanIgnoreReturnValue 358 | private fun addSourceMapping(node: UElement, skip: Boolean = false): Boolean { 359 | if (!skip) node.sourcePsi?.let { sourceMapping.put(it, node) } 360 | return skip 361 | } 362 | 363 | private fun addEdge(source: UElement, dest: UElement, condition: Boolean?) { 364 | forwardEdges.put(source, dest) 365 | when (val existingLabel = edgeConditions[source, dest]) { 366 | is CfgEdgeLabel.CfgExceptionalEdge -> existingLabel.thrown_ += null // add regular edge 367 | is CfgEdgeLabel.CfgConditionalEdge -> 368 | if (condition == null || existingLabel.condition != condition) { 369 | edgeConditions.remove(source, dest) // revert back to unconditional edge 370 | } // else we already have this conditional edge 371 | null -> 372 | if (condition != null) { 373 | edgeConditions.put(source, dest, CfgEdgeLabel.conditionalEdge(condition)) 374 | } 375 | } 376 | } 377 | 378 | private fun addExceptionEdge(source: UElement, dest: UElement, thrownType: PsiType) { 379 | check(throwableType.isAssignableFrom(thrownType)) { "Not a Throwable: $thrownType" } 380 | val isExistingEdge = !forwardEdges.put(source, dest) 381 | when (val existingLabel = edgeConditions[source, dest]) { 382 | is CfgEdgeLabel.CfgExceptionalEdge -> existingLabel.thrown_ += thrownType 383 | else -> 384 | edgeConditions.put( 385 | source, 386 | dest, 387 | CfgEdgeLabel.exceptionalEdge(thrownType).also { 388 | if (isExistingEdge) it.thrown_ += null // record pre-existing regular edge 389 | }, 390 | ) 391 | } 392 | } 393 | 394 | private fun addEdgeFromLastIfPossible(node: UElement, condition: Boolean? = state.condition) { 395 | val prevNode = state.last 396 | if (prevNode?.canCompleteNormally() == true) { 397 | addEdge(prevNode, node, condition) 398 | } 399 | } 400 | 401 | override fun visitElement(node: UElement): Boolean { 402 | return addSourceMapping(node) 403 | } 404 | 405 | override fun visitAnnotation(node: UAnnotation): Boolean = 406 | addSourceMapping(node, skip = node != root) 407 | 408 | override fun visitImportStatement(node: UImportStatement) = true 409 | 410 | // Don't recurse into declarations other than those we override separately 411 | override fun visitDeclaration(node: UDeclaration): Boolean = 412 | addSourceMapping(node, skip = node != root) 413 | 414 | override fun visitLocalVariable(node: ULocalVariable): Boolean = visitElement(node) 415 | 416 | override fun visitParameter(node: UParameter): Boolean = visitElement(node) 417 | 418 | override fun visitBinaryExpression(node: UBinaryExpression): Boolean { 419 | return visitPolyadicExpression(node) 420 | } 421 | 422 | override fun visitIfExpression(node: UIfExpression): Boolean { 423 | addSourceMapping(node) 424 | node.condition.accept(this) 425 | val afterCondition = state.last 426 | 427 | state.condition = true 428 | node.thenExpression?.accept(this) 429 | addEdgeFromLastIfPossible(node) 430 | 431 | state.setLast(afterCondition, condition = false) 432 | node.elseExpression?.accept(this) 433 | addEdgeFromLastIfPossible(node) // adds condition -> exit edge if no else branch 434 | 435 | state.setLast(node) 436 | return true // already visited above 437 | } 438 | 439 | override fun visitLambdaExpression(node: ULambdaExpression): Boolean { 440 | addSourceMapping(node) 441 | return if (node == root) { 442 | false 443 | } else { 444 | // Treat lambda as value: make a node for it, but don't recurse, since it doesn't execute 445 | afterVisitElement(node) 446 | true 447 | } 448 | } 449 | 450 | override fun visitPolyadicExpression(node: UPolyadicExpression): Boolean { 451 | addSourceMapping(node) 452 | if (node.operator is UastBinaryOperator.LogicalOperator) { 453 | // Handle short-circuiting operators 454 | // || short-circuits on "true"; && short-circuits on "false" 455 | val shortCircuitCondition = node.operator == UastBinaryOperator.LOGICAL_OR 456 | for (operand in node.operands.dropLast(1)) { 457 | operand.accept(this) 458 | addEdgeFromLastIfPossible(node, shortCircuitCondition) 459 | state.condition = !shortCircuitCondition 460 | } 461 | node.operands.lastOrNull()?.accept(this) 462 | addEdgeFromLastIfPossible(node) 463 | state.setLast(node) 464 | return true // already visited above 465 | } 466 | return false // handle as normal nested expression that executes in sequence 467 | } 468 | 469 | override fun visitSwitchExpression(node: USwitchExpression): Boolean { 470 | addSourceMapping(node) 471 | addSourceMapping(node.body) 472 | 473 | // For Java-style switches, insert a synthetic node that marks the table-driven branching 474 | // characteristic of a switch. Note that among other things, the switched-on expression is 475 | // implicitly checked to be non-null (and unboxed if necessary), so this node helps identify 476 | // that. Additionally, it should allow generating conditional edges. 477 | // We could use node itself as this marker node, but by employing a synthetic node, node can 478 | // follow its children in the CFG for consistency with how we handle other AST nodes. 479 | // Another alternative might be to generate nodes that approximate what happens under the cover, 480 | // e.g., a node for .ordinal() in the case of an enum switch. But that's tedious and/or 481 | // imprecise, esp. for string switches, and arguably obfuscates what's going on. 482 | val branch = 483 | node.expression?.let { 484 | it.accept(this) 485 | CfgSwitchBranchExpression(it) 486 | } 487 | // TODO(kmb): generate if-else edges if there's no condition or conditions are matched in order 488 | if (branch != null) { 489 | addEdgeFromLastIfPossible(branch, condition = null) 490 | // No condition matched TODO(kmb) only generate this edge if switch isn't exhaustive 491 | addEdge(branch, node, condition = null) 492 | state.setLast(branch) 493 | } 494 | 495 | val valuesSeen = mutableSetOf() 496 | var lastExit: UElement? = null 497 | for (case in node.body.expressions) { 498 | // if-else if somehow allows smartcasts on lastExit where `when` doesn't 499 | if (case is USwitchClauseExpressionWithBody) { 500 | addSourceMapping(case) 501 | 502 | val bodyEntry = case.body.doAccept() 503 | 504 | // Entry edge TODO(kmb): generate conditional edges or find other way to propagate case by 505 | // case results 506 | if (branch != null) addEdge(branch, bodyEntry, condition = null) 507 | if (lastExit?.canCompleteNormally() == true) { 508 | // Fallthrough edge 509 | addEdge(lastExit, bodyEntry, condition = null) 510 | } 511 | lastExit = state.last 512 | 513 | // TODO(kmb): for switch expressions, add edges representing the "result" of this case 514 | // and omit fallthrough/fallout edges instead 515 | 516 | // Don't visit values: they're constants and aren't evaluated at runtime 517 | // TODO(kmb): generate (conditional) edges if conditions are matched in order as in Kotlin 518 | valuesSeen.addAll(case.caseValues.mapNotNull { it.evaluate() }) 519 | } else if (case is USwitchClauseExpression) { 520 | valuesSeen.addAll(case.caseValues.mapNotNull { it.evaluate() }) 521 | } else { 522 | // Shouldn't get here, but we'll simply visit for now. 523 | case.accept(this) 524 | } 525 | } 526 | if (lastExit?.canCompleteNormally() == true) { 527 | // Fallout edge 528 | addEdge(lastExit, node, condition = null) 529 | } 530 | 531 | addEdge(node.body, node, condition = null) 532 | state.setLast(node) 533 | return true // already visited above 534 | } 535 | 536 | override fun visitSwitchClauseExpression(node: USwitchClauseExpression): Boolean { 537 | throw IllegalStateException("shouldn't get here: $node") 538 | } 539 | 540 | override fun visitDoWhileExpression(node: UDoWhileExpression): Boolean { 541 | addSourceMapping(node) 542 | val loopEntry = node.body.doAccept() 543 | continuationEntries[node] = loopEntry 544 | node.condition.accept(this) 545 | addEdgeFromLastIfPossible(node, condition = false) // edge out of the loop 546 | addEdgeFromLastIfPossible(loopEntry, condition = true) // back edge to loop entry 547 | 548 | state.setLast(node) 549 | return true // already visited above 550 | } 551 | 552 | override fun visitForExpression(node: UForExpression): Boolean { 553 | addSourceMapping(node) 554 | node.declaration?.accept(this) 555 | 556 | // For the purposes of continue statements for this loop, we want to consider "update" the 557 | // loop entry to jump to (or "condition" if there is no update), but "update" isn't visited 558 | // upon first loop entry. So we pretend here that we've already visited "body" to visit 559 | // "update" and "condition" so loopEntry is set correctly for any continue statements when 560 | // we visit "body" below". 561 | val predecessor = state.last 562 | val incomingCondition = state.condition 563 | // Since declaration may be null, there may be pending callbacks: stash while we visit update 564 | val pendingCallbacks = state.popCallbacks() 565 | lateinit var continueEntry: UElement 566 | state.markCallbacks += { 567 | continueEntry = it 568 | continuationEntries[node] = it 569 | } 570 | state.setLastWithoutCallbacks(node.body) 571 | node.update?.accept(this) 572 | 573 | state.markCallbacks += pendingCallbacks 574 | lateinit var firstEntry: UElement 575 | state.markCallbacks += { firstEntry = it } 576 | node.condition?.let { 577 | it.accept(this) 578 | addEdgeFromLastIfPossible(node, condition = false) // edge out of the loop 579 | state.condition = true 580 | } // else this is an infinite loop 581 | 582 | node.body.accept(this) 583 | addEdgeFromLastIfPossible(continueEntry, condition = null) // back edge to continue the loop 584 | if (predecessor != null) addEdge(predecessor, firstEntry, incomingCondition) 585 | // else firstEntry is the cfg entry 586 | 587 | state.setLast(node) 588 | return true // already visited above 589 | } 590 | 591 | override fun visitForEachExpression(node: UForEachExpression): Boolean { 592 | addSourceMapping(node) 593 | node.iteratedValue.accept(this) 594 | val iteration = CfgForeachIteratedExpression(node.iteratedValue) 595 | addEdgeFromLastIfPossible(iteration, condition = null) 596 | state.setLast(iteration) 597 | addEdgeFromLastIfPossible(node, condition = null) // edge to skip the loop (nothing to iterate) 598 | 599 | val loopEntry = node.variable.doAccept() 600 | continuationEntries[node] = loopEntry 601 | node.body.accept(this) 602 | addEdgeFromLastIfPossible(loopEntry, condition = null) // back edge to loop entry 603 | addEdgeFromLastIfPossible(node, condition = null) // edge out of the loop 604 | 605 | state.setLast(node) 606 | return true // already visited above 607 | } 608 | 609 | override fun visitWhileExpression(node: UWhileExpression): Boolean { 610 | addSourceMapping(node) 611 | val loopEntry = node.condition.doAccept() 612 | continuationEntries[node] = loopEntry 613 | addEdgeFromLastIfPossible(node, condition = false) // edge out of the loop 614 | 615 | state.condition = true 616 | node.body.accept(this) 617 | addEdgeFromLastIfPossible(loopEntry, condition = null) // back edge to loop entry 618 | 619 | state.setLast(node) 620 | return true // already visited above 621 | } 622 | 623 | override fun visitTryExpression(node: UTryExpression): Boolean { 624 | addSourceMapping(node) 625 | 626 | val predecessor = state.last 627 | val incomingCondition = state.condition 628 | val pendingCallbacks = state.popCallbacks() 629 | 630 | val finallyEntry: UElement? = 631 | node.finallyClause?.let { finally -> 632 | state.setLastWithoutCallbacks(node.tryClause) 633 | val finallyEntry = finally.doAccept() 634 | continuationEntries[finally] = finallyEntry 635 | // Only create regular exit if try block or any catch block can exit regularly. 636 | // TODO(kmb): more reliably detect finally blocks whose normal entry isn't reachable 637 | if ( 638 | node.tryClause.canCompleteNormally() || 639 | node.catchClauses.any { it.body.canCompleteNormally() } 640 | ) { 641 | addEdgeFromLastIfPossible(node, condition = null) 642 | } 643 | finallyEntry 644 | } 645 | 646 | for (catch in node.catchClauses) { 647 | state.setLastWithoutCallbacks(null) // only exceptional edges can lead into catch clauses 648 | val catchEntry = catch.doAccept() 649 | continuationEntries[catch] = catchEntry 650 | addEdgeFromLastIfPossible(finallyEntry ?: node, condition = null) 651 | } 652 | 653 | // Restore original predecessor, condition, and any pending callbacks 654 | state.setLastWithoutCallbacks(predecessor, incomingCondition) 655 | state.markCallbacks += pendingCallbacks 656 | 657 | node.resourceVariables.forEach { it.accept(this) } 658 | node.tryClause.accept(this) 659 | addEdgeFromLastIfPossible(finallyEntry ?: node, condition = null) 660 | 661 | state.setLast(node) 662 | return true // already visited above 663 | } 664 | 665 | override fun afterVisitElement(node: UElement) { 666 | addEdgeFromLastIfPossible(node) 667 | state.setLast(node) 668 | } 669 | 670 | override fun afterVisitExpression(node: UExpression) { 671 | afterVisitElement(node) 672 | // We don't get here for some expressions that represent control flow, such as jumps, `throw`, 673 | // and IfExpression, but that's ok since they can't fail exceptionally. 674 | // To account for unexpected errors, we assume any Throwable can be thrown 675 | if (node.mayFailWithThrowable()) addExceptionEdges(node, throwableType) 676 | } 677 | 678 | override fun afterVisitBreakExpression(node: UBreakExpression) { 679 | afterVisitJumpExpression(node) 680 | } 681 | 682 | override fun afterVisitContinueExpression(node: UContinueExpression) { 683 | addEdgeFromLastIfPossible(node) 684 | state.setLast(node) // setLast before creating outgoing edge in case node is loop's entry node 685 | addJumpEdges( 686 | node, 687 | jumpTarget = continuationEntries[node.jumpTarget] ?: node.jumpTarget ?: root, 688 | jumpRoot = node.jumpTarget ?: root, 689 | ) 690 | } 691 | 692 | override fun afterVisitReturnExpression(node: UReturnExpression) { 693 | afterVisitJumpExpression(node) 694 | } 695 | 696 | private fun afterVisitJumpExpression(node: UJumpExpression) { 697 | addEdgeFromLastIfPossible(node) 698 | state.setLast(node) 699 | addJumpEdges(node, node.jumpTarget ?: root) 700 | } 701 | 702 | private fun addJumpEdges(node: UElement, jumpTarget: UElement, jumpRoot: UElement = jumpTarget) { 703 | var origin = node 704 | var child = node 705 | var parent: UElement? = node.uastParent 706 | while (parent != null && parent != root && parent != jumpRoot) { 707 | // Jumps go through any finally blocks along the way 708 | if (parent is UTryExpression && child != parent.finallyClause) { 709 | parent.finallyClause?.let { finally -> 710 | addEdge(origin, continuationEntries.getOrDefault(finally, finally), condition = null) 711 | if (!finally.canCompleteNormally()) return 712 | origin = finally 713 | } 714 | } 715 | child = parent 716 | parent = child.uastParent 717 | } 718 | addEdge(origin, jumpTarget, condition = null) 719 | } 720 | 721 | override fun afterVisitThrowExpression(node: UThrowExpression) { 722 | addEdgeFromLastIfPossible(node) 723 | var thrown = node.thrownExpression.getExpressionType() ?: throwableType 724 | when { 725 | // `throw null` throws NullPointerException 726 | thrown == PsiTypes.nullType() -> thrown = npeType 727 | // This shouldn't happen, but simply correct the type to be any Throwable 728 | !throwableType.isAssignableFrom(thrown) -> thrown = throwableType 729 | // If NPE isn't already included in thrown, add edges for it in case that thrownException is 730 | // null, see https://docs.oracle.com/javase/specs/jls/se21/html/jls-14.html#jls-14.18. 731 | // Note we could conceivably omit these edges when thrownException expressions is known to be 732 | // non-null, at least for simple cases that don't require deep analysis, such as `new` 733 | // expressions or when a caught exception is rethrown, but that may be surprising in some use 734 | // cases so may be better done as an optional feature. 735 | !thrown.isAssignableFrom(npeType) -> addExceptionEdges(node, npeType) 736 | } 737 | addExceptionEdges(node, thrown) 738 | state.setLast(node) 739 | } 740 | 741 | private fun addExceptionEdges(node: UElement, thrown: PsiType) { 742 | val alreadyCaught = mutableSetOf() 743 | var origin = node 744 | var child = node 745 | var parent: UElement? = node.uastParent 746 | while (parent != null && parent != root) { 747 | if (parent is UTryExpression && child != parent.finallyClause) { 748 | for (catch in parent.catchClauses) { 749 | for (caught in catch.types) { 750 | if (alreadyCaught.any { it.isAssignableFrom(caught) }) continue 751 | val isDefiniteCatch = caught.isAssignableFrom(thrown) 752 | if (isDefiniteCatch || thrown.isAssignableFrom(caught)) { 753 | // Use the narrowest of thrown and caught as the edge's label 754 | addExceptionEdge( 755 | origin, 756 | continuationEntries.getOrDefault(catch, catch), 757 | if (isDefiniteCatch) thrown else caught, 758 | ) 759 | alreadyCaught += caught 760 | } 761 | // If the given exception was definitely caught, stop walking try statements 762 | if (isDefiniteCatch) return 763 | } 764 | } 765 | 766 | // Uncaught exceptions flow throw finally block and on from there 767 | parent.finallyClause?.let { finally -> 768 | addExceptionEdge(origin, continuationEntries.getOrDefault(finally, finally), thrown) 769 | if (!finally.canCompleteNormally()) return 770 | origin = finally 771 | } 772 | } 773 | child = parent 774 | parent = child.uastParent 775 | } 776 | addExceptionEdge(origin, root, thrown) 777 | } 778 | 779 | private fun UExpression.mayFailWithThrowable(): Boolean = 780 | this !is UBlockExpression && this !is UDeclarationsExpression && canCompleteNormally() 781 | } 782 | -------------------------------------------------------------------------------- /convert/src/com/google/devtools/jvmtools/analysis/InterproceduralAnalysis.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devtools.jvmtools.analysis 18 | 19 | import com.github.benmanes.caffeine.cache.Caffeine 20 | import com.google.common.collect.HashMultimap 21 | import com.google.common.collect.ImmutableMultimap 22 | import com.google.common.collect.Maps 23 | import com.google.common.collect.Multimap 24 | import com.google.errorprone.annotations.Immutable 25 | import com.intellij.psi.PsiElement 26 | import org.jetbrains.uast.UElement 27 | 28 | /** 29 | * Context object to request analysis results from while analyzing [analysisKey], see 30 | * [InterproceduralAnalysisBuilder]. 31 | */ 32 | abstract class UInterproceduralAnalysisContext>( 33 | /** Key for the current intraprocedural analysis. */ 34 | val analysisKey: K 35 | ) { 36 | /** 37 | * Returns intraprocedural analysis results for the given [calleeKey] using [initialState]. 38 | * 39 | * Calling this method with the same [calleeKey] but different [initialState] will return results 40 | * computed using the join of all [initialState] values presented with the same [calleeKey]. 41 | */ 42 | abstract fun dataflowResult(calleeKey: K, initialState: V): UDataflowResult 43 | 44 | override fun toString(): String = "UInterproceduralAnalysisContext[$analysisKey]" 45 | } 46 | 47 | /** 48 | * Query dataflow analysis results created using [InterproceduralAnalysisBuilder]. 49 | * 50 | * @param K keys analysis results, specifying a root and optional context 51 | * @param V analysis result type, see [UAnalysis] 52 | */ 53 | class InterproceduralResult 54 | internal constructor( 55 | private val results: Map>, 56 | private val defaultResult: UDataflowResult, 57 | ) { 58 | /** Groups known analysis keys by their roots to allow querying all results for a given root. */ 59 | val analysisKeys: ImmutableMultimap by lazy { 60 | ImmutableMultimap.copyOf(results.keys.map { Maps.immutableEntry(it.callee, it) }) 61 | } 62 | 63 | /** Returns analysis results for the given key or a dummy result if the key is unknown. */ 64 | operator fun get(analysisKey: K): UDataflowResult = 65 | results.getOrDefault(analysisKey, defaultResult) 66 | } 67 | 68 | /** 69 | * Build up interprocedural analysis results by using [addRoot] to register all entry points to be 70 | * analyzed together, followed by [build] to access results as [InterproceduralResult]. 71 | * 72 | * @param K keys analysis results, specifying a root and optional context 73 | * @param V abstract values being tracked by the analysis, see [UTransferFunction] 74 | */ 75 | // Based on https://cmu-program-analysis.github.io/2022/resources/program-analysis.pdf ch. 8 76 | class InterproceduralAnalysisBuilder>( 77 | /** [Bottom][Value.isBottom] value to use absent other information. */ 78 | val bottom: V, 79 | /** Creates transfer functions that can use given context to request interprocedural results. */ 80 | private val transferFactory: (UInterproceduralAnalysisContext) -> UTransferFunction, 81 | ) { 82 | private val dummyResult = BottomAnalysis(bottom) 83 | private val worklist: MutableSet = mutableSetOf() 84 | private val analyzing: MutableSet = mutableSetOf() 85 | private val results: MutableMap> = mutableMapOf() 86 | private val callers: Multimap = HashMultimap.create() 87 | 88 | /** Simple cache to avoid re-analyzing the same method over and over. */ 89 | private val cfgs = Caffeine.newBuilder().maximumSize(100L).build() 90 | 91 | /** 92 | * Adds an [analysisKey] be analyzed with [initialState]. 93 | * 94 | * @param initialState closure to compute initial state for analyzing [analysisKey] that can 95 | * request and use analysis results for other keys. 96 | */ 97 | fun addRoot(analysisKey: K, initialState: (UInterproceduralAnalysisContext) -> V) { 98 | val unused = getOrCompute(analysisKey, initialState(ContextImpl(analysisKey))) 99 | processWorklist() 100 | } 101 | 102 | /** 103 | * Returns analysis results derived from the given [roots][addRoot], which can include results for 104 | * more keys than were given if analyzing the given roots involved results for other keys. 105 | */ 106 | fun build(): InterproceduralResult = 107 | InterproceduralResult( 108 | results.mapValues { (_, summary) -> summary.intraproceduralResults }, 109 | dummyResult, 110 | ) 111 | 112 | private fun processWorklist() { 113 | while (worklist.isNotEmpty()) { 114 | val next = worklist.removeFirst() 115 | analyze(next, checkNotNull(results[next]) { "don't have summary for $next" }) 116 | } 117 | } 118 | 119 | private fun getOrCompute(analysisKey: K, initialState: V): UDataflowResult { 120 | var summary = results[analysisKey] 121 | if (summary != null) { 122 | if (initialState.implies(summary.before)) { 123 | return summary.intraproceduralResults 124 | } else { 125 | summary.before = summary.before join initialState 126 | } 127 | } else { 128 | summary = DataflowSummary(before = initialState, after = bottom, dummyResult) 129 | results[analysisKey] = summary 130 | } 131 | 132 | return analyze(analysisKey, summary) 133 | } 134 | 135 | private fun analyze(analysisKey: K, summary: DataflowSummary): UDataflowResult { 136 | if (!analyzing.add(analysisKey)) return dummyResult 137 | 138 | val analysis = 139 | DataflowAnalysis(cfgs.get(analysisKey.callee) { Cfg.create(it) }) { 140 | transferFactory(ContextImpl(analysisKey)) 141 | } 142 | analysis.runAnalysis(summary.before) 143 | analyzing -= analysisKey 144 | 145 | summary.intraproceduralResults = analysis 146 | val newResult = analysis.finalResult 147 | if (!newResult.implies(summary.after)) { 148 | summary.after = summary.after join newResult 149 | worklist += callers[analysisKey] 150 | } 151 | return analysis 152 | } 153 | 154 | private inner class ContextImpl(analysisKey: K) : 155 | UInterproceduralAnalysisContext(analysisKey) { 156 | override fun dataflowResult(calleeKey: K, initialState: V): UDataflowResult { 157 | val result = getOrCompute(calleeKey, initialState) 158 | // Register context as a "caller" of calleeContext 159 | callers.put(calleeKey, analysisKey) 160 | return result 161 | } 162 | } 163 | } 164 | 165 | private data class DataflowSummary( 166 | var before: V, 167 | var after: V, 168 | var intraproceduralResults: UDataflowResult, 169 | ) 170 | 171 | /** Dummy [UAnalysis] that always returns [bottom]. */ 172 | @Immutable 173 | private class BottomAnalysis>(val bottom: V) : UDataflowResult { 174 | override val finalResult: V 175 | get() = bottom 176 | 177 | override fun get(node: UElement): V = bottom 178 | 179 | override fun get(element: PsiElement): V = bottom 180 | 181 | override fun uastNodes(element: PsiElement): Collection = emptyList() 182 | } 183 | -------------------------------------------------------------------------------- /convert/src/com/google/devtools/jvmtools/analysis/Reachability.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devtools.jvmtools.analysis 18 | 19 | import com.intellij.psi.PsiSwitchLabelStatement 20 | import org.jetbrains.uast.UBlockExpression 21 | import org.jetbrains.uast.UBreakExpression 22 | import org.jetbrains.uast.UCatchClause 23 | import org.jetbrains.uast.UContinueExpression 24 | import org.jetbrains.uast.UDeclaration 25 | import org.jetbrains.uast.UDoWhileExpression 26 | import org.jetbrains.uast.UElement 27 | import org.jetbrains.uast.UExpression 28 | import org.jetbrains.uast.UExpressionList 29 | import org.jetbrains.uast.UForExpression 30 | import org.jetbrains.uast.UIdentifier 31 | import org.jetbrains.uast.UIfExpression 32 | import org.jetbrains.uast.UJumpExpression 33 | import org.jetbrains.uast.ULabeledExpression 34 | import org.jetbrains.uast.UReturnExpression 35 | import org.jetbrains.uast.USwitchClauseExpression 36 | import org.jetbrains.uast.USwitchClauseExpressionWithBody 37 | import org.jetbrains.uast.USwitchExpression 38 | import org.jetbrains.uast.UThrowExpression 39 | import org.jetbrains.uast.UTryExpression 40 | import org.jetbrains.uast.UVariable 41 | import org.jetbrains.uast.UWhileExpression 42 | import org.jetbrains.uast.UYieldExpression 43 | import org.jetbrains.uast.visitor.UastTypedVisitor 44 | import org.jetbrains.uast.visitor.UastVisitor 45 | 46 | /** 47 | * Returns whether the given expression 48 | * [can complete normally](https://docs.oracle.com/javase/specs/jls/se21/html/jls-14.html#jls-14.22) 49 | * _assuming without checking_ that it is 50 | * [reachable](https://docs.oracle.com/javase/specs/jls/se21/html/jls-14.html#jls-14.22). 51 | * 52 | * Not checking reachability here is done for a few reasons: 53 | * * it considers the given element on its own, avoiding `false` results because the given 54 | * expression happens to be unreachable 55 | * * Unlike Java, Kotlin doesn't seem to require all expressions be reachable, so it's somewhat 56 | * problematic to return `false` here "just" because the given element is unreachable. 57 | * * this implementation is _much_ faster (absent memoization): reachability is expensive to check 58 | * because it requires traversing parents and checking completion of preceding elements; 59 | * conversely, this function only considers the given element and its children. 60 | * * this approach is also likely easier to maintain, as there's no mutually recursive second 61 | * algorithm for reachability to be kept "in sync". 62 | */ 63 | // TODO(kmb): handle Kotlin-style nested `throw`s etc and methods that return Nothing 64 | fun UElement.canCompleteNormally(): Boolean = accept(CanCompleteNormally, Unit) 65 | 66 | private object CanCompleteNormally : UastTypedVisitor { 67 | override fun visitElement(node: UElement, data: Unit): Boolean { 68 | if (node is UIdentifier) return true 69 | TODO("Not yet implemented: $node") 70 | } 71 | 72 | override fun visitDeclaration(node: UDeclaration, data: Unit): Boolean = true 73 | 74 | override fun visitVariable(node: UVariable, data: Unit): Boolean = true 75 | 76 | override fun visitExpression(node: UExpression, data: Unit): Boolean = true 77 | 78 | override fun visitExpressionList(node: UExpressionList, data: Unit): Boolean = 79 | if (node.uastParent is USwitchExpression) { 80 | // No default label 81 | // TODO(kmb): maybe exhaustive enum switch all cases aborting should also return false? 82 | node.expressions.none { case -> 83 | case is USwitchClauseExpression && 84 | case.caseValues.any { 85 | // UAST doesn't encode default/else label very well, so look at PSI 86 | val psi = it.javaPsi 87 | psi is PsiSwitchLabelStatement && psi.isDefaultCase 88 | } 89 | } || 90 | // Last block falls out 91 | node.expressions.lastOrNull()?.canCompleteNormally() != false 92 | } else { 93 | // Includes switch entry bodies 94 | node.expressions.lastOrNull()?.canCompleteNormally() != false 95 | } 96 | 97 | override fun visitBlockExpression(node: UBlockExpression, data: Unit): Boolean = 98 | node.expressions.lastOrNull()?.canCompleteNormally() != false 99 | 100 | override fun visitLabeledExpression(node: ULabeledExpression, data: Unit): Boolean = 101 | node.expression.canCompleteNormally() || node.expression.canBreakTo(node.expression) 102 | 103 | override fun visitIfExpression(node: UIfExpression, data: Unit): Boolean = 104 | (node.thenExpression?.canCompleteNormally() != false) || 105 | (node.elseExpression?.canCompleteNormally() != false) 106 | 107 | override fun visitSwitchExpression(node: USwitchExpression, data: Unit): Boolean = 108 | // TODO(kmb): handle switch expressions, in both Java and Kotlin 109 | node.body.canCompleteNormally() || node.body.canBreakTo(node) 110 | 111 | override fun visitSwitchClauseExpression(node: USwitchClauseExpression, data: Unit): Boolean = 112 | node is USwitchClauseExpressionWithBody && node.body.canCompleteNormally() 113 | 114 | override fun visitWhileExpression(node: UWhileExpression, data: Unit): Boolean = 115 | node.condition.evaluate() != true || node.body.canBreakTo(node) 116 | 117 | override fun visitDoWhileExpression(node: UDoWhileExpression, data: Unit): Boolean = 118 | (node.condition.evaluate() != true && 119 | (node.body.canCompleteNormally() || node.body.canContinueTo(node))) || 120 | node.body.canBreakTo(node) 121 | 122 | override fun visitForExpression(node: UForExpression, data: Unit): Boolean = 123 | (node.condition != null && node.condition?.evaluate() != true) || node.body.canBreakTo(node) 124 | 125 | override fun visitBreakExpression(node: UBreakExpression, data: Unit): Boolean = false 126 | 127 | override fun visitContinueExpression(node: UContinueExpression, data: Unit): Boolean = false 128 | 129 | override fun visitReturnExpression(node: UReturnExpression, data: Unit): Boolean = false 130 | 131 | override fun visitThrowExpression(node: UThrowExpression, data: Unit): Boolean = false 132 | 133 | override fun visitYieldExpression(node: UYieldExpression, data: Unit): Boolean = false 134 | 135 | override fun visitTryExpression(node: UTryExpression, data: Unit): Boolean = 136 | (node.tryClause.canCompleteNormally() || node.catchClauses.any { it.canCompleteNormally() }) && 137 | node.finallyClause?.canCompleteNormally() != false 138 | 139 | override fun visitCatchClause(node: UCatchClause, data: Unit): Boolean = 140 | node.body.canCompleteNormally() 141 | 142 | private fun UElement.canBreakTo(neededTarget: UExpression): Boolean { 143 | var found = false 144 | accept( 145 | object : UastVisitor { 146 | override fun visitElement(node: UElement): Boolean = found 147 | 148 | override fun visitBreakExpression(node: UBreakExpression): Boolean { 149 | found = 150 | found || 151 | ((node.jumpTarget == neededTarget || 152 | // jumpTarget is null if it's the node itself, so handle this case specially 153 | (node.jumpTarget == null && 154 | node == neededTarget && 155 | node.uastParent.let { it is ULabeledExpression && it.label == node.label })) && 156 | node.canJumpToTarget()) 157 | return found 158 | } 159 | } 160 | ) 161 | return found 162 | } 163 | 164 | private fun UElement.canContinueTo(neededTarget: UDoWhileExpression): Boolean { 165 | var found = false 166 | accept( 167 | object : UastVisitor { 168 | override fun visitElement(node: UElement): Boolean = found 169 | 170 | override fun visitContinueExpression(node: UContinueExpression): Boolean { 171 | found = found || (node.jumpTarget == neededTarget && node.canJumpToTarget()) 172 | return found 173 | } 174 | } 175 | ) 176 | return found 177 | } 178 | 179 | /** Returns `false` if there's a `finally` block in the way that can't complete normally. */ 180 | private fun UJumpExpression.canJumpToTarget(): Boolean { 181 | var child: UElement = this 182 | var parent: UElement? = uastParent 183 | while (parent != null && child != jumpTarget) { 184 | if ( 185 | parent is UTryExpression && 186 | child != parent.finallyClause && 187 | parent.finallyClause?.canCompleteNormally() == false 188 | ) { 189 | return false 190 | } 191 | child = parent 192 | parent = child.uastParent 193 | } 194 | return true 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /convert/src/com/google/devtools/jvmtools/analysis/Tuple.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devtools.jvmtools.analysis 18 | 19 | import com.google.common.collect.ImmutableMap 20 | import com.google.errorprone.annotations.Immutable 21 | import com.google.errorprone.annotations.ImmutableTypeParameter 22 | import com.intellij.psi.PsiVariable 23 | import org.jetbrains.uast.UExpression 24 | 25 | /** 26 | * Tracks [V] values separately for each non-null key [K], treating missing mappings equivalent to 27 | * [bottom][Value.isBottom]. 28 | */ 29 | @Immutable 30 | data class Tuple<@ImmutableTypeParameter K : Any, V : Value>( 31 | private val contents: ImmutableMap 32 | ) : Value>, Map by contents { 33 | constructor(values: Map) : this(ImmutableMap.copyOf(values)) 34 | 35 | override val isBottom: Boolean 36 | get() = contents.values.all { it.isBottom } // trivially true if contents empty as desired 37 | 38 | override fun join(other: Tuple): Tuple { 39 | val result = contents.toMutableMap() 40 | for ((k, v) in other.contents) { 41 | result.compute(k) { _, existing -> existing?.join(v) ?: v } 42 | } 43 | return Tuple(result) 44 | } 45 | 46 | override fun implies(other: Tuple): Boolean = 47 | contents.all { (k, v) -> 48 | val otherV = other[k] 49 | if (otherV != null) v.implies(otherV) else v.isBottom 50 | } 51 | 52 | /** 53 | * Returns a [Tuple] identical to this one except mapping the given [key] to the given [value]. 54 | */ 55 | fun withMapping(key: K, value: V): Tuple { 56 | if (contents[key] == value) return this 57 | val newContents = contents.toMutableMap() 58 | newContents[key] = value 59 | return Tuple(newContents) 60 | } 61 | 62 | /** Returns a [Tuple] identical to this one except for the given new mappings. */ 63 | operator fun plus(newMappings: Collection>): Tuple { 64 | if (newMappings.isEmpty()) return this 65 | val newContents = contents.toMutableMap() 66 | for ((key, value) in newMappings) newContents[key] = value 67 | return Tuple(newContents) 68 | } 69 | 70 | companion object { 71 | private val EMPTY = Tuple(ImmutableMap.of()) 72 | 73 | /** Returns an empty [Tuple], i.e., a [bottom][Tuple.isBottom] object. */ 74 | @Suppress("UNCHECKED_CAST") 75 | fun > empty(): Tuple = EMPTY as Tuple 76 | } 77 | } 78 | 79 | /** 80 | * Pairs a [store] with a [value] tuple: 81 | * * [value] represents what we know about each expression at a given program point 82 | * * [store] tracks variables in scope 83 | */ 84 | @Immutable 85 | data class State>(val value: Tuple, val store: Tuple) : 86 | Value> { 87 | override val isBottom: Boolean 88 | get() = value.isBottom && store.isBottom 89 | 90 | override fun join(other: State) = State(value join other.value, store join other.store) 91 | 92 | override fun implies(other: State): Boolean = 93 | value.implies(other.value) && store.implies(other.store) 94 | 95 | companion object { 96 | private val EMPTY = State(Tuple.empty(), Tuple.empty()) 97 | 98 | /** Returns an empty [State], i.e., containing empty tuples. */ 99 | @Suppress("UNCHECKED_CAST") fun > empty(): State = EMPTY as State 100 | 101 | /** Returns a [State] with the given [storeContents] and an empty [value] tuple. */ 102 | fun > withStore(storeContents: Map): State = 103 | State(Tuple.empty(), @Suppress("Immutable") Tuple(storeContents)) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /convert/src/com/google/devtools/jvmtools/analysis/UAnalysis.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devtools.jvmtools.analysis 18 | 19 | import com.google.errorprone.annotations.Immutable 20 | import com.intellij.psi.PsiElement 21 | import org.jetbrains.uast.UElement 22 | 23 | /** Analysis results accessed by [PsiElement]s. */ 24 | interface PsiAnalysis { 25 | /** Returns result for the given [element] assuming its successful execution. */ 26 | operator fun get(element: PsiElement): T 27 | } 28 | 29 | /** Analysis results accessed by UAST [UElement]s. */ 30 | interface UAnalysis : PsiAnalysis { 31 | /** Returns result for the given [node] assuming its successful execution. */ 32 | operator fun get(node: UElement): T 33 | 34 | /** 35 | * Returns [UElement]s corresponding to the given source PSI [element]. 36 | * 37 | * This helps querying `Tuple` results. 38 | */ 39 | fun uastNodes(element: PsiElement): Collection 40 | } 41 | 42 | /** 43 | * Abstract values tracked by a [dataflow][UTransferFunction] analysis. 44 | * 45 | * Concrete implementations should implement a lattice of finite height, to guarantee termination of 46 | * iterative dataflow analyses. 47 | * 48 | * @param T the concrete class of values so [join] can return the receiver type 49 | */ 50 | @Immutable 51 | interface Value> { 52 | /** 53 | * `true` if this is the most precise value that implies all others, `false` otherwise. 54 | * 55 | * In logical terms, "bottom" corresponds to "false": it [implies] all other values, and is the 56 | * unit element for [join] (aka logical OR). 57 | */ 58 | val isBottom: Boolean 59 | 60 | /** 61 | * Returns `true` if this value is at least as precise as [other], and `false` otherwise. 62 | * 63 | * This function imposes a partial order on a given class of [Value]s, with the receiver being 64 | * less than or equal to [other] if `true`. 65 | */ 66 | fun implies(other: T): Boolean 67 | 68 | /** 69 | * Returns a value that abstracts this and the given value similar to logical OR, i.e., it must be 70 | * true that `this` and [other] both [imply][implies] the returned value. 71 | * 72 | * To guarantee that an iterative analysis terminates, this function should eventually return a 73 | * "least precise" ("top") value that, when joined with any other value, returns itself. 74 | */ 75 | infix fun join(other: T): T 76 | } 77 | 78 | /** Lifts [Value.join] to nullable arguments, treating `null` as [Value.isBottom]. */ 79 | infix fun > T?.join(other: T?): T? = 80 | when { 81 | other == null -> this 82 | this == null -> other 83 | else -> join(other) 84 | } 85 | -------------------------------------------------------------------------------- /convert/src/com/google/devtools/jvmtools/analysis/UTransferFunction.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devtools.jvmtools.analysis 18 | 19 | import com.google.common.annotations.VisibleForTesting 20 | import com.google.errorprone.annotations.Immutable 21 | import com.intellij.psi.PsiElement 22 | import com.intellij.psi.PsiType 23 | import com.intellij.psi.PsiTypes 24 | import org.jetbrains.uast.UElement 25 | import org.jetbrains.uast.UExpression 26 | import org.jetbrains.uast.UMethod 27 | 28 | /** Result of a dataflow [analysis][analyze]. */ 29 | interface UDataflowResult : UAnalysis { 30 | /** Final analysis result at the exit node. */ 31 | val finalResult: T 32 | } 33 | 34 | /** Context for [UTransferFunction] implementations to query analysis results. */ 35 | // Identical to UAnalysis, but can add additional query functions here 36 | interface UDataflowContext> : UAnalysis 37 | 38 | /** 39 | * Shorthand for functions that construct [UTransferFunction]s for given root node (e.g., a method) 40 | * that can query the given context. 41 | */ 42 | typealias TransferFactory = (UDataflowContext) -> UTransferFunction 43 | 44 | /** 45 | * Analyzes this method with the [UTransferFunction] returned by [transferFactory]. 46 | * 47 | * @param initialState initial analysis state to use as the entry node's initial "before" state. 48 | * @param transferFactory returns [UTransferFunction] for this method, which (awkwardly) allows the 49 | * transfer function to receive a reference back to the running analysis to query results. 50 | */ 51 | fun > UMethod.analyze( 52 | initialState: T, 53 | transferFactory: TransferFactory, 54 | ): UDataflowResult = 55 | DataflowAnalysis(Cfg.create(this), transferFactory).also { it.runAnalysis(initialState) } 56 | 57 | /** 58 | * Analyzes this [CfgRoot] with the [UTransferFunction] returned by [transferFactory]. 59 | * 60 | * @param initialState initial analysis state to use as the entry node's initial "before" state. 61 | * @param transferFactory returns [UTransferFunction] for this root, which (awkwardly) allows the 62 | * transfer function to receive a reference back to the running analysis to query results. 63 | */ 64 | fun > CfgRoot.analyze( 65 | initialState: T, 66 | transferFactory: TransferFactory, 67 | ): UDataflowResult = 68 | DataflowAnalysis(Cfg.create(this), transferFactory).also { it.runAnalysis(initialState) } 69 | 70 | /** 71 | * Base class for dataflow transfer functions. Subclasses should override [CfgTypedVisitor] methods 72 | * for nodes they care about and derive [TransferResult]s from the given input state as needed. 73 | * 74 | * Use [toNormalResult] or [passthroughResult] to turn an input into [TransferResult] "unseen", 75 | * which the default implementations in this interface do for all nodes. 76 | * 77 | * @param T abstract values being tracked 78 | */ 79 | interface UTransferFunction> : CfgTypedVisitor, TransferResult> { 80 | /** 81 | * Whether this is a backwards transfer function, meaning the [TransferResult]s it returns are 82 | * immediately _prior to_ execution of the given node, given [TransferInput]s representing state 83 | * immediately following the node's execution (including from exceptional successors). 84 | * 85 | * Backwards transfer functions should nearly never return anything but [passthroughResult]s or 86 | * [TransferResult.normal] results with no exceptional values. [TransferInput.value] will however 87 | * allow querying input values separately by condition when visiting control flow branches. 88 | * 89 | * Returns `false` by default and should be overridden to return `true` to define backwards 90 | * transfer functions. 91 | */ 92 | val isBackwards: Boolean 93 | get() = false 94 | 95 | /** 96 | * The concrete [T]'s most precise ("bottom") value, which will be used when no other information 97 | * is available (e.g., in [UAnalysis.get]). 98 | */ 99 | val bottom: T 100 | 101 | /** Overridden to pass through the given [data] by default, joining any conditional inputs. */ 102 | override fun visitElement(node: UElement, data: TransferInput): TransferResult = 103 | TransferResult.normal(data.value()) 104 | 105 | /** Overridden to pass through the given [data] by default, including conditionals. */ 106 | override fun visitExpression(node: UExpression, data: TransferInput): TransferResult = 107 | data.passthroughResult(node) 108 | } 109 | 110 | /** 111 | * Result of [transferring][UTransferFunction] over a node, which can be constructed as a [normal] 112 | * result, or [conditional] upon the node's Boolean value. 113 | * 114 | * This class is "write-only" for transfer functions: it defines `internal` functions to be used in 115 | * this package only. 116 | * 117 | * @param T abstract values being tracked 118 | * @see TransferInput 119 | */ 120 | @Immutable 121 | sealed class TransferResult>( 122 | @Suppress("Immutable") // PsiType can't be annotated 123 | private val exceptionalValues: Map 124 | ) { 125 | /** 126 | * Returns the default result to be propagated except along labeled edges with matching 127 | * [Conditional] or [exceptionalValues]. 128 | */ 129 | internal abstract fun defaultResultValue(): T 130 | 131 | /** 132 | * Transforms this result to an input, optionally filtered by the given edge label. 133 | * 134 | * @param label [Cfg] edge label to filter by, `null` meaning *regular return*, i.e., as defined 135 | * by [Cfg.successorsWithConditions] 136 | * @param default value to use for other conditions, typically, [UTransferFunction.bottom] 137 | */ 138 | internal abstract fun toInput(label: CfgEdgeLabel?, default: T): TransferInput 139 | 140 | /** 141 | * Returns a value for the given exception type. 142 | * 143 | * @return value for the mapping with most precise type or [defaultResultValue] if no such type 144 | */ 145 | private fun exceptionalValue(thrownType: PsiType): T { 146 | var result: T? = null 147 | var found: PsiType? = null 148 | // Return most precise entry in exceptionalValues (note there can't be multiple such entries 149 | // if thrownType and all keys in exceptionalValues are subtypes of Throwable) 150 | for ((exception, value) in exceptionalValues) { 151 | if (found != null && exception.isAssignableFrom(found)) continue 152 | if (exception.isAssignableFrom(thrownType)) { 153 | result = value 154 | found = exception 155 | } 156 | } 157 | return result ?: defaultResultValue() // use regular return if no matching exception 158 | } 159 | 160 | internal fun toExceptionalInput( 161 | exceptionalLabel: CfgEdgeLabel.CfgExceptionalEdge, 162 | default: T, 163 | ): TransferInput = 164 | TransferInput.Normal( 165 | exceptionalLabel.thrown 166 | .map { if (it != null) exceptionalValue(it) else defaultResultValue() } 167 | .reduceOrNull(Value::join) ?: default 168 | ) 169 | 170 | @VisibleForTesting 171 | internal class Normal>(val value: T, exceptionalValues: Map) : 172 | TransferResult(exceptionalValues) { 173 | override fun defaultResultValue(): T = value 174 | 175 | override fun toInput(label: CfgEdgeLabel?, default: T): TransferInput = 176 | when (label) { 177 | null -> TransferInput.Normal(value) 178 | CfgEdgeLabel.CfgConditionalEdge.TRUE -> TransferInput.Conditional(value, default) 179 | CfgEdgeLabel.CfgConditionalEdge.FALSE -> TransferInput.Conditional(default, value) 180 | is CfgEdgeLabel.CfgExceptionalEdge -> toExceptionalInput(label, default) 181 | } 182 | } 183 | 184 | @VisibleForTesting 185 | internal class Conditional>( 186 | val trueValue: T, 187 | val falseValue: T, 188 | exceptionalValues: Map, 189 | ) : TransferResult(exceptionalValues) { 190 | override fun defaultResultValue(): T = trueValue join falseValue 191 | 192 | override fun toInput(label: CfgEdgeLabel?, default: T): TransferInput = 193 | when (label) { 194 | null -> TransferInput.Conditional(trueValue, falseValue) 195 | CfgEdgeLabel.CfgConditionalEdge.TRUE -> TransferInput.Conditional(trueValue, default) 196 | CfgEdgeLabel.CfgConditionalEdge.FALSE -> TransferInput.Conditional(default, falseValue) 197 | is CfgEdgeLabel.CfgExceptionalEdge -> toExceptionalInput(label, default) 198 | } 199 | } 200 | 201 | companion object { 202 | /** Makes standard result that propagates the given [value] to all successor nodes. */ 203 | fun > normal( 204 | value: T, 205 | exceptionalValues: Map = emptyMap(), 206 | ): TransferResult = Normal(value, exceptionalValues) 207 | 208 | /** 209 | * Makes conditional result that should only be used for Boolean expression results. Propagates 210 | * [trueValue] and [falseValue] separately to successor nodes and only propagates [trueValue] 211 | * ([falseValue], respectively) to successors only reachable if the expression evaluates to 212 | * `true` (`false`, respectively). 213 | */ 214 | fun > conditional( 215 | trueValue: T, 216 | falseValue: T, 217 | exceptionalValues: Map = emptyMap(), 218 | ): TransferResult = Conditional(trueValue, falseValue, exceptionalValues) 219 | } 220 | } 221 | 222 | /** 223 | * Input to [transfer functions][UTransferFunction] that exposes incoming analysis [value]s. 224 | * 225 | * @param T abstract values being tracked 226 | * @see TransferInput 227 | */ 228 | @Immutable 229 | sealed class TransferInput> { 230 | /** 231 | * Whether this is a conditional input, i.e., [value] may return different results depending on 232 | * the given condition. 233 | */ 234 | abstract val isConditional: Boolean 235 | 236 | /** 237 | * Returns the incoming analysis value, optionally filtered by [condition]. 238 | * 239 | * @param condition optionally limits result to the given condition, `null` meaning *no filter*, 240 | * i.e., [Value.join]ed over all incoming values including through exceptional control flow. 241 | * This parameter has no effect unless [isConditional] is `true`. 242 | */ 243 | // TODO(kmb): provide a way to query incoming exceptional values separately 244 | abstract fun value(condition: Boolean? = null): T 245 | 246 | internal abstract fun merge(other: TransferInput): TransferInput 247 | 248 | internal abstract fun implies(other: TransferInput): Boolean 249 | 250 | internal data class Normal>(val value: T) : TransferInput() { 251 | override val isConditional: Boolean 252 | get() = false 253 | 254 | override fun value(condition: Boolean?): T = value 255 | 256 | override fun merge(other: TransferInput): TransferInput = 257 | if (other is Conditional) { 258 | Conditional(value join other.trueValue, value join other.falseValue) 259 | } else { 260 | Normal(value join other.value()) 261 | } 262 | 263 | override fun implies(other: TransferInput): Boolean { 264 | return if (other is Normal) value.implies(other.value) else false 265 | } 266 | } 267 | 268 | internal data class Conditional>(val trueValue: T, val falseValue: T) : 269 | TransferInput() { 270 | override val isConditional: Boolean 271 | get() = true 272 | 273 | override fun value(condition: Boolean?): T = 274 | when (condition) { 275 | null -> trueValue join falseValue 276 | true -> trueValue 277 | false -> falseValue 278 | } 279 | 280 | override fun merge(other: TransferInput): TransferInput = 281 | if (other is Conditional) { 282 | Conditional(trueValue join other.trueValue, falseValue join other.falseValue) 283 | } else { 284 | val joined = other.value() 285 | Conditional(trueValue join joined, falseValue join joined) 286 | } 287 | 288 | override fun implies(other: TransferInput): Boolean { 289 | return trueValue.implies(other.value(condition = true)) && 290 | falseValue.implies(other.value(condition = false)) 291 | } 292 | } 293 | } 294 | 295 | /** Returns [TransferResult.normal] result for this input, merging any conditional inputs. */ 296 | fun > TransferInput.toNormalResult(): TransferResult = 297 | TransferResult.normal(value()) 298 | 299 | /** 300 | * Derives [TransferResult] from this input for the given node, preserving conditional inputs if the 301 | * node is [PsiTypes.booleanType], i.e., can have conditional exits. 302 | */ 303 | fun > TransferInput.passthroughResult(node: UExpression): TransferResult = 304 | if (this is TransferInput.Conditional && node.getExpressionType() == PsiTypes.booleanType()) { 305 | TransferResult.conditional(trueValue, falseValue) 306 | } else { 307 | toNormalResult() 308 | } 309 | 310 | /** 311 | * Implements dataflow analysis over a given CFG and transfer function. 312 | * 313 | * @param T abstract values being tracked 314 | */ 315 | internal class DataflowAnalysis>(cfg: Cfg, transferFactory: TransferFactory) : 316 | UDataflowContext, UDataflowResult { 317 | private val before = mutableMapOf>() 318 | private val after = mutableMapOf>() 319 | 320 | // transferFactory should store this reference for later use or ignore it; calling methods on it 321 | // will likely fail as it usually requires `cfg`, which in turn requires `transfer.isBackwards`. 322 | // A better solution would be nice, but in practice this usually works. 323 | private val transfer: UTransferFunction = transferFactory(this) 324 | val cfg: Cfg = if (transfer.isBackwards) cfg.reverse() else cfg 325 | 326 | override val finalResult: T 327 | get() = get(cfg.exitNode) 328 | 329 | fun runAnalysis(initialState: T) { 330 | val pending = mutableSetOf() 331 | before[cfg.entryNode] = TransferInput.Normal(initialState) 332 | pending += cfg.entryNode 333 | while (pending.isNotEmpty()) { 334 | // Remove and transfer a node; add nodes to be visited as a result 335 | // TODO(kmb): implement efficient visitation order to minimize re-computations 336 | val node: UElement = pending.removeFirst() 337 | pending += transferOver(node) 338 | } 339 | } 340 | 341 | private fun transferOver(node: UElement): List { 342 | val before = checkNotNull(before[node]) { "don't have any state for $node" } 343 | val after = node.accept(transfer, before) 344 | return updateAfter(node, after) 345 | } 346 | 347 | /** 348 | * Sets and propagates [node]'s given "after" [result]. 349 | * 350 | * @return nodes to be (re-) visited because their "before" input changed. 351 | */ 352 | private fun updateAfter(node: UElement, result: TransferResult): List { 353 | after[node] = result 354 | val toUpdate = mutableListOf() 355 | for ((successor, condition) in cfg.successorsWithConditions(node)) { 356 | var newInput = result.toInput(condition, transfer.bottom) 357 | val previous = before.putIfAbsent(successor, newInput) 358 | if (previous != null) { 359 | newInput = previous.merge(newInput) 360 | before[successor] = newInput 361 | } 362 | if (previous == null || !newInput.implies(previous)) toUpdate += successor 363 | } 364 | return toUpdate 365 | } 366 | 367 | override fun get(node: UElement): T { 368 | return after[node]?.defaultResultValue() ?: transfer.bottom 369 | } 370 | 371 | override fun get(element: PsiElement): T { 372 | val nodes = cfg.sourceMapping[element] 373 | return nodes.mapNotNull { after[it]?.defaultResultValue() }.reduceOrNull(Value::join) 374 | ?: transfer.bottom 375 | } 376 | 377 | override fun uastNodes(element: PsiElement): Collection = cfg.sourceMapping[element] 378 | } 379 | 380 | /** Removes and returns the collection's first element (as returned by its [MutableIterator]). */ 381 | internal fun MutableCollection.removeFirst(): T { 382 | val iter = iterator() 383 | val result = iter.next() 384 | iter.remove() 385 | return result 386 | } 387 | -------------------------------------------------------------------------------- /convert/src/com/google/devtools/jvmtools/analysis/mutation/CollectionMutationAnalysis.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2025 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devtools.jvmtools.analysis.mutation 18 | 19 | import com.google.devtools.jvmtools.analysis.CfgRoot 20 | import com.google.devtools.jvmtools.analysis.CfgRootKey 21 | import com.google.devtools.jvmtools.analysis.InterproceduralAnalysisBuilder 22 | import com.google.devtools.jvmtools.analysis.InterproceduralResult 23 | import com.google.devtools.jvmtools.analysis.State 24 | import com.google.devtools.jvmtools.analysis.TransferInput 25 | import com.google.devtools.jvmtools.analysis.TransferResult 26 | import com.google.devtools.jvmtools.analysis.Tuple 27 | import com.google.devtools.jvmtools.analysis.UTransferFunction 28 | import com.google.devtools.jvmtools.analysis.Value 29 | import com.google.devtools.jvmtools.analysis.passthroughResult 30 | import com.google.devtools.jvmtools.analysis.toNormalResult 31 | import com.google.errorprone.annotations.Immutable 32 | import com.intellij.psi.LambdaUtil.getFunctionalInterfaceMethod 33 | import com.intellij.psi.PsiMethod 34 | import com.intellij.psi.PsiModifierListOwner 35 | import com.intellij.psi.PsiTypes 36 | import com.intellij.psi.PsiVariable 37 | import com.intellij.psi.util.InheritanceUtil 38 | import org.jetbrains.uast.UBinaryExpression 39 | import org.jetbrains.uast.UBinaryExpressionWithType 40 | import org.jetbrains.uast.UCallExpression 41 | import org.jetbrains.uast.UExpression 42 | import org.jetbrains.uast.UFile 43 | import org.jetbrains.uast.UIfExpression 44 | import org.jetbrains.uast.ULambdaExpression 45 | import org.jetbrains.uast.UMethod 46 | import org.jetbrains.uast.UParenthesizedExpression 47 | import org.jetbrains.uast.UQualifiedReferenceExpression 48 | import org.jetbrains.uast.UReturnExpression 49 | import org.jetbrains.uast.USimpleNameReferenceExpression 50 | import org.jetbrains.uast.UVariable 51 | import org.jetbrains.uast.UastBinaryExpressionWithTypeKind 52 | import org.jetbrains.uast.UastBinaryOperator 53 | import org.jetbrains.uast.getParameterForArgument 54 | import org.jetbrains.uast.toUElementOfType 55 | import org.jetbrains.uast.tryResolve 56 | import org.jetbrains.uast.visitor.AbstractUastVisitor 57 | 58 | /** 59 | * Totally ordered lattice representing [UNUSED] (bottom), [READ] (only), or [MODIFIED] (and 60 | * possibly read) collections. 61 | * 62 | * Declared in ascending order so [implies] and [join] can be implemented using `<=`. 63 | */ 64 | @Immutable 65 | enum class Mutation : Value { 66 | UNUSED, 67 | READ, 68 | MODIFIED; 69 | 70 | override val isBottom: Boolean 71 | get() = this == UNUSED 72 | 73 | override fun implies(other: Mutation): Boolean = this <= other 74 | 75 | override fun join(other: Mutation): Mutation = maxOf(this, other) 76 | } 77 | 78 | @Immutable 79 | data class MutationKey(override val callee: CfgRoot, val resultMutation: Mutation) : CfgRootKey 80 | 81 | /** [collectionMutation] aims to infer collections that Kotlin would consider `MutableList` etc. */ 82 | object CollectionMutationAnalysis { 83 | internal val BOTTOM = State.empty() 84 | 85 | /** Infers mutated collections in the given [UFile]. */ 86 | fun UFile.collectionMutation(): InterproceduralResult> { 87 | val result = 88 | InterproceduralAnalysisBuilder>(BOTTOM) { ctx -> 89 | CollectionMutationTransfer(ctx.analysisKey.resultMutation) mutations@{ call, resultMutation 90 | -> 91 | val callee = call.resolve() ?: return@mutations Tuple.empty() 92 | val declaredMutations: Tuple = 93 | @Suppress("Immutable") // PSI can't be annotated 94 | Tuple(callee.parameterList.parameters.associateWith { mutationFromAnnotation(it) }) 95 | 96 | // Limit scope of analysis to given file for now 97 | if (callee.containingFile != sourcePsi) return@mutations declaredMutations 98 | val method = callee.toUElementOfType() 99 | // If we have a method body for the called method, analyze it even if the method is open 100 | // or public. Since we're optimistically assuming readonly collections above that's not 101 | // really worse than just going by annotations for open methods, but ideally we'd 102 | // analyze any overrides of open methods (at least visible ones). 103 | // TODO(b/309967546): handle other files and analyze possible callees for open methods 104 | if (method?.uastBody != null) { 105 | val root = CfgRoot.of(method) 106 | // Always honor @Mutable annotations by joining analysis results with declared 107 | declaredMutations join 108 | ctx 109 | .dataflowResult( 110 | MutationKey(root, root.declaredResultMutation() join resultMutation), 111 | BOTTOM, 112 | ) 113 | .finalResult 114 | .store 115 | } else { 116 | declaredMutations 117 | } 118 | } 119 | } 120 | 121 | accept( 122 | object : AbstractUastVisitor() { 123 | override fun visitMethod(node: UMethod): Boolean { 124 | addRoot(CfgRoot.of(node)) 125 | return super.visitMethod(node) 126 | } 127 | 128 | override fun visitLambdaExpression(node: ULambdaExpression): Boolean { 129 | addRoot(CfgRoot.of(node)) 130 | return super.visitLambdaExpression(node) 131 | } 132 | 133 | private fun addRoot(root: CfgRoot) { 134 | result.addRoot(MutationKey(root, root.declaredResultMutation())) { BOTTOM } 135 | } 136 | } 137 | ) 138 | 139 | return result.build() 140 | } 141 | 142 | private fun CfgRoot.declaredResultMutation(): Mutation { 143 | val method: PsiMethod? = 144 | when (this) { 145 | is CfgRoot.Method -> rootNode 146 | is CfgRoot.Lambda -> getFunctionalInterfaceMethod(rootNode.functionalInterfaceType) 147 | } 148 | 149 | return if (method?.returnType == PsiTypes.voidType()) { 150 | Mutation.UNUSED 151 | } else { 152 | mutationFromAnnotation(method) 153 | } 154 | } 155 | 156 | // TODO(b/309967546): handle MutableXxx parameters / return types for methods defined in Kotlin 157 | private fun mutationFromAnnotation(declaration: PsiModifierListOwner?): Mutation = 158 | if (declaration?.hasMutableAnnotation() == true) Mutation.MODIFIED else Mutation.READ 159 | 160 | /** Returns true if there's a `@Mutable` annotation. */ 161 | private fun PsiModifierListOwner.hasMutableAnnotation() = 162 | hasAnnotation("kotlin.annotations.jvm.Mutable") 163 | } 164 | 165 | /** 166 | * Defines a backwards dataflow analysis that records mutations to collections, including through 167 | * local variables and iterators etc. 168 | * 169 | * @param assumedReturn the mutation to assume for the return value of the method 170 | * @param calleeMutations a function that returns the mutations of parameters of the called method 171 | */ 172 | private class CollectionMutationTransfer( 173 | val assumedReturn: Mutation, 174 | val calleeMutations: (UCallExpression, Mutation) -> Tuple, 175 | ) : UTransferFunction> { 176 | override val isBackwards: Boolean 177 | get() = true 178 | 179 | override val bottom: State 180 | get() = CollectionMutationAnalysis.BOTTOM 181 | 182 | override fun visitVariable( 183 | node: UVariable, 184 | data: TransferInput>, 185 | ): TransferResult> { 186 | val rhs = node.uastInitializer ?: return data.toNormalResult() 187 | val input = data.value() 188 | @Suppress("DEPRECATION") // .psi gives us PsiVariable 189 | return TransferResult.normal(input.withValue(rhs, input.store.getOrUnused(node.psi))) 190 | } 191 | 192 | override fun visitReturnExpression( 193 | node: UReturnExpression, 194 | data: TransferInput>, 195 | ): TransferResult> { 196 | return TransferResult.normal(data.value().withValue(node.returnExpression, assumedReturn)) 197 | } 198 | 199 | override fun visitBinaryExpression( 200 | node: UBinaryExpression, 201 | data: TransferInput>, 202 | ): TransferResult> = 203 | if (node.operator == UastBinaryOperator.ASSIGN) { 204 | val lhs = node.leftOperand.tryResolve() as? PsiVariable 205 | if (lhs != null) { 206 | val input = data.value() 207 | // propagate from left to right (i.e., backwards), then reset left 208 | val result = input.value.withMapping(node.rightOperand, input.store.getOrUnused(lhs)) 209 | TransferResult.normal(State(result, input.store.withMapping(lhs, Mutation.UNUSED))) 210 | } else { 211 | data.passthroughResult(node) 212 | } 213 | } else { 214 | visitPolyadicExpression(node, data) 215 | } 216 | 217 | override fun visitSimpleNameReferenceExpression( 218 | node: USimpleNameReferenceExpression, 219 | data: TransferInput>, 220 | ): TransferResult> { 221 | val variable = node.resolve() as? PsiVariable ?: return data.passthroughResult(node) 222 | val input = data.value() 223 | // update store with mutations recorded for this variable use 224 | return TransferResult.normal( 225 | State( 226 | input.value, 227 | input.store.withMapping( 228 | variable, 229 | input.value.getOrUnused(node) join input.store.getOrUnused(variable), 230 | ), 231 | ) 232 | ) 233 | } 234 | 235 | override fun visitIfExpression( 236 | node: UIfExpression, 237 | data: TransferInput>, 238 | ): TransferResult> = 239 | if (node.isTernary) { 240 | val input = data.value() 241 | val value = input.value.getOrUnused(node) 242 | TransferResult.normal( 243 | State( 244 | input.value + listOfNotNull(node.thenExpression, node.elseExpression).map { it to value }, 245 | input.store, 246 | ) 247 | ) 248 | } else { 249 | super.visitIfExpression(node, data) 250 | } 251 | 252 | override fun visitQualifiedReferenceExpression( 253 | node: UQualifiedReferenceExpression, 254 | data: TransferInput>, 255 | ): TransferResult> = 256 | // Propagate to selector--this helps dealing with method calls being modeled as qualified 257 | // ref expressions. It's at first glance a bit odd for field references, but in the current 258 | // analysis that just ends up merging all accesses to the same field regardless of access path, 259 | // which is fine considering we just want to see if there's any mutating accesses that would 260 | // make the field mutable. 261 | propagateToSubexpr(node, node.selector, data) 262 | 263 | override fun visitParenthesizedExpression( 264 | node: UParenthesizedExpression, 265 | data: TransferInput>, 266 | ): TransferResult> = propagateToSubexpr(node, node.expression, data) 267 | 268 | override fun visitBinaryExpressionWithType( 269 | node: UBinaryExpressionWithType, 270 | data: TransferInput>, 271 | ): TransferResult> = 272 | if (node.operationKind is UastBinaryExpressionWithTypeKind.TypeCast) { 273 | propagateToSubexpr(node, node.operand, data) 274 | } else { 275 | super.visitBinaryExpressionWithType(node, data) 276 | } 277 | 278 | override fun visitCallExpression( 279 | node: UCallExpression, 280 | data: TransferInput>, 281 | ): TransferResult> { 282 | val input = data.value() 283 | val callee = node.resolve() 284 | 285 | // Handle collection operations 286 | if ( 287 | InheritanceUtil.isInheritor(callee?.containingClass, "java.lang.Iterable") || 288 | InheritanceUtil.isInheritor(callee?.containingClass, "java.util.Map") || 289 | InheritanceUtil.isInheritor(callee?.containingClass, "java.util.Map.Entry") || 290 | InheritanceUtil.isInheritor(callee?.containingClass, "java.util.Iterator") 291 | ) { 292 | val mutation = 293 | when (callee?.name) { 294 | "add", // collection 295 | "addAll", // collection 296 | "put", // map 297 | "putAll", // map 298 | "putIfAbsent", // map 299 | "set", // collection, iterator 300 | "setValue", // Map.Entry 301 | "replace", 302 | "compute", 303 | "computeIfAbsent", 304 | "computeIfPresent", 305 | "merge", 306 | "remove", 307 | "removeAll" -> Mutation.MODIFIED 308 | // merge iterator etc.'s mutations into its originating collection 309 | "iterator", 310 | "listIterator", 311 | "entries", 312 | "keySet", 313 | "values" -> input.value.getOrUnused(node) 314 | else -> Mutation.READ 315 | } 316 | return TransferResult.normal(input.withValue(node.receiver, mutation)) 317 | } 318 | 319 | val value = input.value.getOrUnused(node) 320 | val calleeMutations: Tuple = calleeMutations(node, value) 321 | // Map callee mutations to the corresponding call arguments 322 | return TransferResult.normal( 323 | State( 324 | input.value + 325 | node.valueArguments.map { arg -> 326 | val paramNullness = node.getParameterForArgument(arg)?.let { calleeMutations[it] } 327 | arg to (paramNullness ?: Mutation.UNUSED) 328 | }, 329 | input.store, 330 | ) 331 | ) 332 | } 333 | 334 | private fun propagateToSubexpr( 335 | node: UExpression, 336 | subexpr: UExpression, 337 | data: TransferInput>, 338 | ): TransferResult> { 339 | val input = data.value() 340 | return TransferResult.normal(input.withValue(subexpr, input.value.getOrUnused(node))) 341 | } 342 | 343 | private fun State.withValue(key: UExpression?, newValue: Mutation): State = 344 | if (key != null) State(value.withMapping(key, newValue), store) else this 345 | 346 | private fun Tuple.getOrUnused(key: T): Mutation = 347 | getOrDefault(key, Mutation.UNUSED) 348 | } 349 | -------------------------------------------------------------------------------- /convert/src/com/google/devtools/jvmtools/analysis/nullness/Nullness.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devtools.jvmtools.analysis.nullness 18 | 19 | import com.google.devtools.jvmtools.analysis.Value 20 | import com.google.errorprone.annotations.Immutable 21 | 22 | /** 23 | * Analysis value to represent a reference's nullness, or [BOTTOM] if it hasn't been initialized. 24 | * 25 | * [implies] and [join] are consistent with the "obvious" partial order: [BOTTOM] < (([NONULL] < 26 | * [PARAMETRIC]) xor [NULL]) < [NULLABLE]. 27 | */ 28 | @Immutable 29 | enum class Nullness : Value { 30 | BOTTOM { 31 | override fun join(other: Nullness): Nullness = other 32 | 33 | override fun implies(other: Nullness): Boolean = true 34 | 35 | override fun equate(other: Nullness): Nullness = BOTTOM 36 | }, 37 | NONULL { 38 | override fun join(other: Nullness): Nullness = 39 | when (other) { 40 | BOTTOM, 41 | NONULL -> NONULL 42 | PARAMETRIC -> PARAMETRIC 43 | NULL, 44 | NULLABLE -> NULLABLE 45 | } 46 | 47 | override fun implies(other: Nullness): Boolean = 48 | other == NONULL || other == PARAMETRIC || other == NULLABLE 49 | 50 | override fun equate(other: Nullness): Nullness = 51 | when (other) { 52 | BOTTOM, 53 | NULL -> BOTTOM 54 | NONULL, 55 | PARAMETRIC, 56 | NULLABLE -> NONULL 57 | } 58 | }, 59 | 60 | /** 61 | * Meant for type variables with nullable upper bound, where this helps distinguish `T` from `T?` 62 | * ([NULLABLE]) and `T!!` ([NONULL], written `T & Any` in Kotlin). 63 | * 64 | * Type variables with nullable upper bound _may_ represent a nullable type (but may not) and are 65 | * therefore less precise than [NONULL] but don't need to be qualified as [NULLABLE], because they 66 | * are neither definitely inclusive, nor definitely exclusive, of `null`. For this reason we also 67 | * don't consider [NULL] to [imply][implies] [PARAMETRIC]--since [NULL] isn't _strictly_ more 68 | * precise--and thus joining [PARAMETRIC] and [NULL] yields [NULLABLE]. 69 | * 70 | * Note: for type variables with non-nullable upper bound, use [NONULL] for `T` (since it's 71 | * exclusive of `null` and thus the same as `T!!`) and [NULLABLE] for `T?`. 72 | */ 73 | PARAMETRIC { 74 | override fun join(other: Nullness): Nullness = 75 | when (other) { 76 | BOTTOM, 77 | NONULL, 78 | PARAMETRIC -> PARAMETRIC 79 | NULL, 80 | NULLABLE -> NULLABLE 81 | } 82 | 83 | override fun implies(other: Nullness): Boolean = other == PARAMETRIC || other == NULLABLE 84 | 85 | override fun equate(other: Nullness): Nullness = 86 | when (other) { 87 | BOTTOM -> BOTTOM 88 | NULL -> NULL 89 | NONULL -> NONULL 90 | PARAMETRIC, 91 | NULLABLE -> PARAMETRIC 92 | } 93 | }, 94 | NULL { 95 | override fun join(other: Nullness): Nullness = 96 | when (other) { 97 | BOTTOM, 98 | NULL -> NULL 99 | NONULL, 100 | PARAMETRIC, 101 | NULLABLE -> NULLABLE 102 | } 103 | 104 | override fun implies(other: Nullness): Boolean = other == NULL || other == NULLABLE 105 | 106 | override fun equate(other: Nullness): Nullness = 107 | when (other) { 108 | BOTTOM, 109 | NONULL -> BOTTOM 110 | PARAMETRIC, 111 | NULL, 112 | NULLABLE -> NULL 113 | } 114 | }, 115 | NULLABLE { 116 | override fun join(other: Nullness): Nullness = NULLABLE 117 | 118 | override fun implies(other: Nullness): Boolean = other == NULLABLE 119 | 120 | override fun equate(other: Nullness): Nullness = other 121 | }; 122 | 123 | override val isBottom: Boolean 124 | get() = this == BOTTOM 125 | 126 | /** 127 | * Returns the result of equating this and [other], which in particular will be [BOTTOM] when 128 | * giving contradicting inputs (i.e., [NULL] and [NONULL]) and otherwise the more precise of the 129 | * given values. 130 | * 131 | * Note this function is usually the dual of [join] (i.e., the "meet", or logical AND), except 132 | * that it returns [NULL] when given [NULL] and [PARAMETRIC]. 133 | */ 134 | abstract infix fun equate(other: Nullness): Nullness 135 | } 136 | -------------------------------------------------------------------------------- /convert/src/com/google/devtools/jvmtools/analysis/nullness/NullnessAnalysis.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devtools.jvmtools.analysis.nullness 18 | 19 | import com.google.devtools.jvmtools.convert.util.runIf 20 | import com.google.devtools.jvmtools.analysis.CfgForeachIteratedExpression 21 | import com.google.devtools.jvmtools.analysis.CfgRoot 22 | import com.google.devtools.jvmtools.analysis.CfgSwitchBranchExpression 23 | import com.google.devtools.jvmtools.analysis.InterproceduralAnalysisBuilder 24 | import com.google.devtools.jvmtools.analysis.InterproceduralResult 25 | import com.google.devtools.jvmtools.analysis.State 26 | import com.google.devtools.jvmtools.analysis.TransferInput 27 | import com.google.devtools.jvmtools.analysis.TransferResult 28 | import com.google.devtools.jvmtools.analysis.Tuple 29 | import com.google.devtools.jvmtools.analysis.UDataflowResult 30 | import com.google.devtools.jvmtools.analysis.UInterproceduralAnalysisContext 31 | import com.google.devtools.jvmtools.analysis.UTransferFunction 32 | import com.intellij.lang.java.JavaLanguage 33 | import com.intellij.psi.PsiArrayType 34 | import com.intellij.psi.PsiClass 35 | import com.intellij.psi.PsiClassType 36 | import com.intellij.psi.PsiField 37 | import com.intellij.psi.PsiLocalVariable 38 | import com.intellij.psi.PsiModifier 39 | import com.intellij.psi.PsiModifierListOwner 40 | import com.intellij.psi.PsiParameter 41 | import com.intellij.psi.PsiType 42 | import com.intellij.psi.PsiTypeParameter 43 | import com.intellij.psi.PsiTypes 44 | import com.intellij.psi.PsiVariable 45 | import com.intellij.psi.util.PsiUtil.extractIterableTypeParameter 46 | import org.jetbrains.uast.UArrayAccessExpression 47 | import org.jetbrains.uast.UBinaryExpression 48 | import org.jetbrains.uast.UBinaryExpressionWithType 49 | import org.jetbrains.uast.UCallExpression 50 | import org.jetbrains.uast.UCallableReferenceExpression 51 | import org.jetbrains.uast.UClass 52 | import org.jetbrains.uast.UClassLiteralExpression 53 | import org.jetbrains.uast.UElement 54 | import org.jetbrains.uast.UExpression 55 | import org.jetbrains.uast.UFile 56 | import org.jetbrains.uast.UForEachExpression 57 | import org.jetbrains.uast.UIfExpression 58 | import org.jetbrains.uast.ULambdaExpression 59 | import org.jetbrains.uast.ULiteralExpression 60 | import org.jetbrains.uast.ULocalVariable 61 | import org.jetbrains.uast.UMethod 62 | import org.jetbrains.uast.UObjectLiteralExpression 63 | import org.jetbrains.uast.UParameter 64 | import org.jetbrains.uast.UParenthesizedExpression 65 | import org.jetbrains.uast.UPolyadicExpression 66 | import org.jetbrains.uast.UQualifiedReferenceExpression 67 | import org.jetbrains.uast.UReferenceExpression 68 | import org.jetbrains.uast.USimpleNameReferenceExpression 69 | import org.jetbrains.uast.USuperExpression 70 | import org.jetbrains.uast.UThisExpression 71 | import org.jetbrains.uast.UUnaryExpression 72 | import org.jetbrains.uast.UastBinaryExpressionWithTypeKind 73 | import org.jetbrains.uast.UastBinaryOperator 74 | import org.jetbrains.uast.UastCallKind 75 | import org.jetbrains.uast.UastPrefixOperator 76 | import org.jetbrains.uast.getParentOfType 77 | import org.jetbrains.uast.internal.acceptList 78 | import org.jetbrains.uast.resolveToUElementOfType 79 | import org.jetbrains.uast.visitor.AbstractUastVisitor 80 | import org.jetbrains.uast.visitor.UastTypedVisitor 81 | 82 | /** [Nullness] dataflow analysis that aims to imitate Kotlin's nullness type system. */ 83 | object NullnessAnalysis { 84 | internal val BOTTOM = State.empty() 85 | 86 | fun UFile.nullness(): InterproceduralResult> { 87 | val result = 88 | InterproceduralAnalysisBuilder>(BOTTOM) { ctx -> 89 | fun analyzeLambda(lambda: ULambdaExpression): State = 90 | ctx.dataflowResult(CfgRoot.of(lambda)).finalResult 91 | 92 | NullnessTransfer( 93 | root = ctx.analysisKey.callee.rootNode, 94 | analyzeLambda = ::analyzeLambda, 95 | analyzeCallee = callee@{ call, state -> 96 | val callee = call.resolveToUElementOfType() ?: return@callee null 97 | // Trust annotations for non-private methods for now, since we can't fully analyze 98 | // their call graphs. 99 | if (!callee.hasModifierProperty(PsiModifier.PRIVATE)) return@callee null 100 | 101 | // Methods with type parameters are complicated because they might be parametric, 102 | // so skip those for now as well. 103 | // TODO(b/384955376): include methods with type parameters in interprocedural analysis 104 | if (callee.hasTypeParameters()) return@callee null 105 | 106 | val storeContents = mutableMapOf() 107 | callee.parameterList.parameters.forEachIndexed { index, param -> 108 | if ((param.type as? PsiClassType)?.resolve() is PsiTypeParameter) return@callee null 109 | // Always honor callee annotations but join with what we know about arguments 110 | storeContents[param] = 111 | nullnessFromAnnotation(param.type, param) join 112 | (state.value[call.getArgumentForParameter(index)] ?: Nullness.BOTTOM) 113 | } 114 | 115 | val root = CfgRoot.of(callee) 116 | val dataflowResult = 117 | ctx.dataflowResult(root, ctx.capturedState(root, storeContents)).finalResult 118 | 119 | callee 120 | .returnValueNullness(dataflowResult, ::analyzeLambda) 121 | .takeIf { it != Nullness.PARAMETRIC } 122 | ?.let { 123 | // If we get a result, join it with callee annotation 124 | it join nullnessFromAnnotation(callee.returnType, callee) 125 | } 126 | }, 127 | ) 128 | } 129 | accept( 130 | object : AbstractUastVisitor() { 131 | override fun visitMethod(node: UMethod): Boolean { 132 | if (node.sourcePsi != null) { 133 | val root = CfgRoot.of(node) 134 | result.addRoot(root) { ctx -> ctx.initialState(root) } 135 | } 136 | return super.visitMethod(node) 137 | } 138 | 139 | override fun visitLambdaExpression(node: ULambdaExpression): Boolean { 140 | // We'll usually visit lambdas in the course of analyzing the surrounding method, but to 141 | // guarantee data for all lambdas we also treat them as roots. 142 | val root = CfgRoot.of(node) 143 | result.addRoot(root) { ctx -> ctx.initialState(root) } 144 | return super.visitLambdaExpression(node) 145 | } 146 | } 147 | ) 148 | return result.build() 149 | } 150 | 151 | private fun UInterproceduralAnalysisContext>.dataflowResult( 152 | root: CfgRoot 153 | ): UDataflowResult> = dataflowResult(root, initialState(root)) 154 | 155 | /** 156 | * Returns the initial [State.store] to use for the given analysis [root], which includes: 157 | * 1. the given root's parameters based on their annotations, and 158 | * 2. any captured final variables with the inferred analysis result at the point in the outer 159 | * scope where the variable is captured. 160 | */ 161 | private fun UInterproceduralAnalysisContext>.initialState( 162 | root: CfgRoot 163 | ): State { 164 | val storeContents = mutableMapOf() 165 | 166 | @Suppress("DEPRECATION") // .psi gives us a PsiParameter 167 | when (root) { 168 | is CfgRoot.Method -> root.rootNode.uastParameters 169 | is CfgRoot.Lambda -> root.rootNode.valueParameters 170 | }.associateTo(storeContents) { it.psi to nullnessFromAnnotation(it.type, it) } 171 | 172 | if (root is CfgRoot.Lambda) { 173 | // Try to refine lambda parameters based on surrounding call arguments 174 | val enclosing = 175 | when ( 176 | val rootNode = 177 | root.rootNode.getParentOfType( 178 | strict = true, 179 | UMethod::class.java, 180 | ULambdaExpression::class.java, 181 | ) 182 | ) { 183 | is UMethod -> CfgRoot.of(rootNode) 184 | is ULambdaExpression -> CfgRoot.of(rootNode) 185 | else -> null 186 | } 187 | if (enclosing != null) { 188 | root.rootNode.refineFromArguments( 189 | storeContents, 190 | analysis = { dataflowResult(enclosing)[it] }, 191 | analyzeLambda = { dataflowResult(CfgRoot.of(it)).finalResult }, 192 | ) 193 | } 194 | } 195 | 196 | return capturedState(root, storeContents) 197 | } 198 | 199 | private fun UInterproceduralAnalysisContext>.capturedState( 200 | root: CfgRoot, 201 | storeContents: MutableMap, 202 | ): State { 203 | val seen = referencedVariables(root) 204 | seen.removeAll(storeContents.keys) 205 | if (root.rootNode.lang != JavaLanguage.INSTANCE) { 206 | // In Java, captured variables are effectively final; otherwise, exclude non-final variables 207 | seen.removeIf { !it.hasModifierProperty(PsiModifier.FINAL) } 208 | } 209 | if (seen.isEmpty()) return State.withStore(storeContents) // no captured variables 210 | 211 | // Node to query analysis results at, which is the enclosing object literal or lambda 212 | var query: UExpression? = 213 | when (root) { 214 | is CfgRoot.Method -> root.rootNode.getParentOfType() 215 | is CfgRoot.Lambda -> root.rootNode 216 | } 217 | // Analysis root for `query`, which we'll search for by walking query's parents 218 | var queryRoot: UElement? = query 219 | while (seen.isNotEmpty() && query != null && queryRoot != null) { 220 | queryRoot = queryRoot.uastParent 221 | if (queryRoot is UMethod) { 222 | val cfgRoot = CfgRoot.of(queryRoot) 223 | dataflowResult(cfgRoot)[query].store.addAvailableKeys(storeContents, seen) 224 | // Continue with enclosing object literal if any 225 | query = queryRoot.getParentOfType() 226 | queryRoot = query 227 | } else if (queryRoot is ULambdaExpression) { 228 | val cfgRoot = CfgRoot.of(queryRoot) 229 | dataflowResult(cfgRoot)[query].store.addAvailableKeys(storeContents, seen) 230 | query = queryRoot 231 | } 232 | } 233 | return State.withStore(storeContents) 234 | } 235 | 236 | private fun referencedVariables(root: CfgRoot): MutableSet { 237 | val seen = mutableSetOf() 238 | root.rootNode.accept( 239 | object : AbstractUastVisitor() { 240 | override fun visitMethod(node: UMethod): Boolean = 241 | node != root.rootNode // don't visit nested methods 242 | 243 | override fun visitClass(node: UClass): Boolean = true // don't visit nested classes 244 | 245 | override fun visitLambdaExpression(node: ULambdaExpression): Boolean = 246 | node != root.rootNode // don't visit nested lambdas 247 | 248 | override fun visitObjectLiteralExpression(node: UObjectLiteralExpression): Boolean { 249 | node.receiver?.accept(this) 250 | node.valueArguments.acceptList(this) 251 | return true // don't visit object literal body 252 | } 253 | 254 | override fun visitSimpleNameReferenceExpression( 255 | node: USimpleNameReferenceExpression 256 | ): Boolean { 257 | val referee = node.resolve() 258 | if (referee is PsiLocalVariable || referee is PsiParameter) { 259 | seen += referee as PsiVariable 260 | } 261 | return super.visitSimpleNameReferenceExpression(node) 262 | } 263 | } 264 | ) 265 | return seen 266 | } 267 | 268 | private fun Tuple.addAvailableKeys( 269 | dest: MutableMap, 270 | wanted: MutableSet, 271 | ) { 272 | val iter = wanted.iterator() 273 | while (iter.hasNext()) { 274 | val storeKey = iter.next() 275 | this[storeKey]?.let { 276 | dest.putIfAbsent(storeKey, it) 277 | iter.remove() 278 | } 279 | } 280 | } 281 | } 282 | 283 | /** Transfer function for tracking reference [Nullness] similar to how Kotlin would. */ 284 | private class NullnessTransfer( 285 | /** The root node of what's being analyzed, i.e., a [CfgRoot.rootNode]. */ 286 | private val root: UElement, 287 | /** Produces interprocedural analysis result of the given lambda as needed by [resultNullness]. */ 288 | private val analyzeLambda: (ULambdaExpression) -> State, 289 | /** 290 | * Analysis function that given a call site and current analysis state returns the callee's return 291 | * value [Nullness] or `null` if interprocedural analysis is not supported for this call. 292 | */ 293 | private val analyzeCallee: (UCallExpression, State) -> Nullness?, 294 | ) : UTransferFunction> { 295 | override val bottom: State 296 | get() = NullnessAnalysis.BOTTOM 297 | 298 | private val javaLangIterable: PsiClass by lazy { 299 | PsiType.getTypeByName( 300 | "java.lang.Iterable", 301 | root.sourcePsi?.project!!, 302 | root.sourcePsi?.resolveScope!!, 303 | ) 304 | .resolve()!! 305 | } 306 | private val throwableType: PsiClassType by lazy { 307 | PsiType.getJavaLangThrowable(root.sourcePsi?.manager!!, root.sourcePsi?.resolveScope!!) 308 | } 309 | 310 | override fun visitLocalVariable( 311 | node: ULocalVariable, 312 | data: TransferInput>, 313 | ): TransferResult> { 314 | val state = data.value() 315 | val value = node.uastInitializer?.let { state.value[it] } ?: Nullness.BOTTOM 316 | @Suppress("DEPRECATION") // .psi gives us a PsiLocalVariable 317 | return TransferResult.normal(State(state.value, state.store.withMapping(node.psi, value))) 318 | } 319 | 320 | @Suppress("DEPRECATION") // .psi gives us a PsiParameter 321 | override fun visitParameter( 322 | node: UParameter, 323 | data: TransferInput>, 324 | ): TransferResult> { 325 | val state = data.value() 326 | val value = 327 | when (val parent = node.uastParent) { 328 | is UMethod, 329 | is ULambdaExpression -> { 330 | check(state.store.containsKey(node.psi)) { 331 | "Method & lambda parameters should be in initial state, but $node missing: $data" 332 | } 333 | return super.visitParameter(node, data) 334 | } 335 | is UForEachExpression -> { 336 | parent.iteratedValue.resultNullness( 337 | state, 338 | analyzeLambda, 339 | javaLangIterable.typeParameters[0], 340 | ) 341 | ?: run { 342 | val iteratedType = 343 | when (val iterableType = parent.iteratedValue.getExpressionType()) { 344 | is PsiArrayType -> iterableType.componentType 345 | is PsiClassType -> 346 | extractIterableTypeParameter(iterableType, /* eraseTypeParameter= */ false) 347 | else -> null // Shouldn't get here but let's assume non-null otherwise 348 | } 349 | nullnessFromAnnotation(iteratedType) 350 | } 351 | } 352 | else -> Nullness.NONULL // most parameters are non-null, e.g., caught exceptions 353 | } 354 | return TransferResult.normal(State(state.value, state.store.withMapping(node.psi, value))) 355 | } 356 | 357 | override fun visitArrayAccessExpression( 358 | node: UArrayAccessExpression, 359 | data: TransferInput>, 360 | ): TransferResult> { 361 | val state = data.value() 362 | val value = 363 | node.resultNullness(state, analyzeLambda) ?: nullnessFromAnnotation(node.getExpressionType()) 364 | return state.toNormalResult(node, value, dereference = node.receiver) 365 | } 366 | 367 | override fun visitBinaryExpression( 368 | node: UBinaryExpression, 369 | data: TransferInput>, 370 | ): TransferResult> { 371 | fun identityEqualityStore(): State { 372 | val state = data.value() 373 | val impliedNullness = 374 | state.valueOrBottom(node.leftOperand) equate state.valueOrBottom(node.rightOperand) 375 | return if (impliedNullness == Nullness.BOTTOM) { 376 | bottom // Contradiction, so return bottom 377 | } else { 378 | val store = 379 | data.value().store + 380 | listOfNotNull( 381 | node.leftOperand.resolveTrackedVariable()?.let { it to impliedNullness }, 382 | node.rightOperand.resolveTrackedVariable()?.let { it to impliedNullness }, 383 | ) 384 | State(state.value.withMapping(node, Nullness.NONULL), store) 385 | } 386 | } 387 | 388 | fun identityInequalityStore(): State { 389 | val state = data.value() 390 | var leftNullness = state.valueOrBottom(node.leftOperand) 391 | var rightNullness = state.valueOrBottom(node.rightOperand) 392 | if (leftNullness == Nullness.NULL) rightNullness = rightNullness equate Nullness.NONULL 393 | if (rightNullness == Nullness.NULL) leftNullness = leftNullness equate Nullness.NONULL 394 | 395 | return if (leftNullness == Nullness.BOTTOM || rightNullness == Nullness.BOTTOM) { 396 | bottom // Contradiction, so return bottom 397 | } else { 398 | val store = 399 | data.value().store + 400 | listOfNotNull( 401 | node.leftOperand.resolveTrackedVariable()?.let { it to leftNullness }, 402 | node.rightOperand.resolveTrackedVariable()?.let { it to rightNullness }, 403 | ) 404 | State(state.value.withMapping(node, Nullness.NONULL), store) 405 | } 406 | } 407 | 408 | return when (node.operator) { 409 | UastBinaryOperator.IDENTITY_EQUALS -> 410 | TransferResult.conditional(identityEqualityStore(), identityInequalityStore()) 411 | UastBinaryOperator.IDENTITY_NOT_EQUALS -> 412 | TransferResult.conditional(identityInequalityStore(), identityEqualityStore()) 413 | else -> visitPolyadicExpression(node, data) 414 | } 415 | } 416 | 417 | override fun visitBinaryExpressionWithType( 418 | node: UBinaryExpressionWithType, 419 | data: TransferInput>, 420 | ): TransferResult> { 421 | return if (node.operationKind == UastBinaryExpressionWithTypeKind.InstanceCheck.INSTANCE) { 422 | // `instanceof` can only be `true` for non-null values 423 | TransferResult.conditional( 424 | trueValue = 425 | State( 426 | data.value(condition = true).value.withMapping(node, Nullness.NONULL), 427 | data.value(condition = true).store + 428 | listOfNotNull(node.operand.resolveTrackedVariable()?.let { it to Nullness.NONULL }), 429 | ), 430 | falseValue = 431 | State( 432 | data.value(condition = false).value.withMapping(node, Nullness.NONULL), 433 | data.value(condition = false).store, 434 | ), 435 | exceptionalValues = mapOf(throwableType to data.value()), 436 | ) 437 | } else { 438 | super.visitBinaryExpressionWithType(node, data) 439 | } 440 | } 441 | 442 | override fun visitPolyadicExpression( 443 | node: UPolyadicExpression, 444 | data: TransferInput>, 445 | ): TransferResult> { 446 | return when { 447 | node.operator == UastBinaryOperator.ASSIGN -> { 448 | val state = data.value() 449 | val lastOperand = 450 | node.operands.lastOrNull() ?: return state.toNormalResult(node, Nullness.BOTTOM) 451 | val value = state.valueOrBottom(lastOperand) 452 | state.toNormalResult(node, value) { 453 | this + 454 | node.operands.dropLast(1).mapNotNull { operand -> 455 | operand.resolveTrackedVariable()?.let { it to value } 456 | } 457 | } 458 | } 459 | data.isConditional && node.getExpressionType() == PsiTypes.booleanType() -> { 460 | // Other operators return nonnull values, but preserve conditional results for Booleans 461 | TransferResult.conditional( 462 | trueValue = 463 | State( 464 | data.value(condition = true).value.withMapping(node, Nullness.NONULL), 465 | data.value(condition = true).store, 466 | ), 467 | falseValue = 468 | State( 469 | data.value(condition = false).value.withMapping(node, Nullness.NONULL), 470 | data.value(condition = false).store, 471 | ), 472 | exceptionalValues = mapOf(throwableType to data.value()), 473 | ) 474 | } 475 | else -> { 476 | // Other operators return nonnull values 477 | // TODO(b/308816245): consider lhs of compound assignments non-null 478 | data.value().toNormalResult(node, Nullness.NONULL) 479 | } 480 | } 481 | } 482 | 483 | override fun visitUnaryExpression(node: UUnaryExpression, data: TransferInput>) = 484 | if (node.operator == UastPrefixOperator.LOGICAL_NOT && data.isConditional) { 485 | // Flip conditional stores; result is always non-null boolean 486 | TransferResult.conditional( 487 | trueValue = 488 | State( 489 | data.value(condition = false).value.withMapping(node, Nullness.NONULL), 490 | data.value(condition = false).store, 491 | ), 492 | falseValue = 493 | State( 494 | data.value(condition = true).value.withMapping(node, Nullness.NONULL), 495 | data.value(condition = true).store, 496 | ), 497 | exceptionalValues = mapOf(throwableType to data.value()), 498 | ) 499 | } else { 500 | data.value().toNormalResult(node, Nullness.NONULL) 501 | } 502 | 503 | override fun visitIfExpression( 504 | node: UIfExpression, 505 | data: TransferInput>, 506 | ): TransferResult> { 507 | val state = data.value() 508 | val value = 509 | if (node.isTernary) { 510 | state 511 | .valueOrBottom(node.thenExpression ?: node.condition) 512 | .join(state.valueOrBottom(node.elseExpression ?: node.condition)) 513 | } else { 514 | Nullness.BOTTOM 515 | } 516 | return data.value().toNormalResult(node, value) 517 | } 518 | 519 | override fun visitLiteralExpression( 520 | node: ULiteralExpression, 521 | data: TransferInput>, 522 | ) = data.value().toNormalResult(node, if (node.isNull) Nullness.NULL else Nullness.NONULL) 523 | 524 | override fun visitClassLiteralExpression( 525 | node: UClassLiteralExpression, 526 | data: TransferInput>, 527 | ) = data.value().toNormalResult(node, Nullness.NONULL) 528 | 529 | override fun visitLambdaExpression( 530 | node: ULambdaExpression, 531 | data: TransferInput>, 532 | ): TransferResult> = data.value().toNormalResult(node, Nullness.NONULL) 533 | 534 | override fun visitCallableReferenceExpression( 535 | node: UCallableReferenceExpression, 536 | data: TransferInput>, 537 | ) = 538 | // Callable references are non-null (like lambdas) but implicitly dereference any qualifier 539 | data.value().toNormalResult(node, Nullness.NONULL, dereference = node.qualifierExpression) 540 | 541 | override fun visitThisExpression(node: UThisExpression, data: TransferInput>) = 542 | // `this` references are non-null 543 | data.value().toNormalResult(node, Nullness.NONULL) 544 | 545 | override fun visitSuperExpression(node: USuperExpression, data: TransferInput>) = 546 | // `super` like `this` references are non-null 547 | data.value().toNormalResult(node, Nullness.NONULL) 548 | 549 | override fun visitCallExpression( 550 | node: UCallExpression, 551 | data: TransferInput>, 552 | ): TransferResult> { 553 | if ( 554 | node.kind == UastCallKind.CONSTRUCTOR_CALL || 555 | node.kind == UastCallKind.NEW_ARRAY_WITH_DIMENSIONS || 556 | node.kind == UastCallKind.NEW_ARRAY_WITH_INITIALIZER || 557 | node.kind == UastCallKind.NESTED_ARRAY_INITIALIZER 558 | ) { 559 | // New objects are non-null (including arrays) 560 | return data.value().toNormalResult(node, Nullness.NONULL) 561 | } 562 | 563 | val state = data.value() 564 | val value = 565 | analyzeCallee(node, state) 566 | ?: node.resultNullness(state, analyzeLambda) 567 | ?: nullnessFromAnnotation(node.getExpressionType(), node.resolve()) 568 | // TODO(b/308816245): havoc fields once we track them 569 | return state.toNormalResult(node, value, dereference = node.receiver) 570 | } 571 | 572 | override fun visitQualifiedReferenceExpression( 573 | node: UQualifiedReferenceExpression, 574 | data: TransferInput>, 575 | ): TransferResult> = 576 | if (node.selector is UCallExpression) { 577 | // foo.bar() is modeled as a UQualifiedReferenceExpression containing a UCallExpression, 578 | // so pass through the UCallExpression's result instead of recalculating it 579 | val state = data.value() 580 | state.toNormalResult(node, state.valueOrBottom(node.selector)) 581 | } else { 582 | visitReferenceExpression(node, data) 583 | } 584 | 585 | override fun visitReferenceExpression( 586 | node: UReferenceExpression, 587 | data: TransferInput>, 588 | ): TransferResult> { 589 | val state = data.value() 590 | val referee = node.resolve() as? PsiModifierListOwner 591 | // TODO(b/308816245): track fields 592 | val value = state.store[referee] ?: nullnessFromAnnotation(node.getExpressionType(), referee) 593 | return state.toNormalResult( 594 | node, 595 | value, 596 | dereference = (node as? UQualifiedReferenceExpression)?.receiver, 597 | ) 598 | } 599 | 600 | override fun visitCfgForeachIteratedExpression( 601 | node: CfgForeachIteratedExpression, 602 | data: TransferInput>, 603 | ) = data.value().toNormalResult(node, Nullness.NONULL, dereference = node.wrappedExpression) 604 | 605 | override fun visitCfgSwitchBranchExpression( 606 | node: CfgSwitchBranchExpression, 607 | data: TransferInput>, 608 | ) = data.value().toNormalResult(node, Nullness.NONULL, dereference = node.wrappedExpression) 609 | 610 | /** 611 | * Returns a [TransferResult.normal] based on this one with the given [State.value] that 612 | * optionally reflects a dereference of the given node. 613 | * 614 | * If [dereference] resolves to a variable, that variable is marked as non-null in the result; 615 | * otherwise, the given store is included in the result as-is. 616 | */ 617 | private fun State.toNormalResult( 618 | node: UExpression, 619 | newValue: Nullness, 620 | dereference: UExpression?, 621 | ) = 622 | toNormalResult(node, newValue) { 623 | val dereferenced = dereference?.resolveTrackedVariable() 624 | if (dereferenced != null) withMapping(dereferenced, Nullness.NONULL) else this 625 | } 626 | 627 | private inline fun State.toNormalResult( 628 | node: UExpression, 629 | newValue: Nullness, 630 | updateStore: Tuple.() -> Tuple = { store }, 631 | ) = 632 | TransferResult.normal( 633 | State(value.withMapping(node, newValue), store.updateStore()), 634 | exceptionalValues = mapOf(throwableType to this), 635 | ) 636 | 637 | private companion object { 638 | fun State.valueOrBottom(expression: UExpression): Nullness = 639 | value[expression] ?: Nullness.BOTTOM 640 | 641 | fun UExpression.resolveTrackedVariable(): PsiVariable? = accept(ResolveTrackedVariable, Unit) 642 | } 643 | 644 | private object ResolveTrackedVariable : UastTypedVisitor { 645 | override fun visitElement(node: UElement, data: Unit): PsiVariable? = null 646 | 647 | override fun visitReferenceExpression(node: UReferenceExpression, data: Unit): PsiVariable? { 648 | val result = node.resolve() as? PsiVariable 649 | return if (result is PsiField) null else result // ignore fields 650 | } 651 | 652 | override fun visitParenthesizedExpression( 653 | node: UParenthesizedExpression, 654 | data: Unit, 655 | ): PsiVariable? = node.expression.accept(this, data) 656 | 657 | override fun visitBinaryExpression(node: UBinaryExpression, data: Unit): PsiVariable? = 658 | runIf(node.operator is UastBinaryOperator.AssignOperator) { 659 | // TODO(b/308816245): extract all assigned variables, or track equal variables together 660 | node.leftOperand.resolveTrackedVariable() 661 | ?: runIf(node.operator == UastBinaryOperator.ASSIGN) { 662 | node.rightOperand.resolveTrackedVariable() 663 | } 664 | } 665 | } 666 | } 667 | -------------------------------------------------------------------------------- /convert/src/com/google/devtools/jvmtools/analysis/nullness/NullnessAnnotations.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devtools.jvmtools.analysis.nullness 18 | 19 | import com.intellij.codeInsight.AnnotationTargetUtil.isTypeAnnotation 20 | import com.intellij.lang.java.JavaLanguage 21 | import com.intellij.psi.PsiAnnotation 22 | import com.intellij.psi.PsiAnnotationOwner 23 | import com.intellij.psi.PsiCapturedWildcardType 24 | import com.intellij.psi.PsiClass 25 | import com.intellij.psi.PsiClassType 26 | import com.intellij.psi.PsiDisjunctionType 27 | import com.intellij.psi.PsiElement 28 | import com.intellij.psi.PsiFile 29 | import com.intellij.psi.PsiIntersectionType 30 | import com.intellij.psi.PsiModifierListOwner 31 | import com.intellij.psi.PsiPrimitiveType 32 | import com.intellij.psi.PsiType 33 | import com.intellij.psi.PsiTypeParameter 34 | import com.intellij.psi.PsiTypeVisitor 35 | import com.intellij.psi.PsiTypes 36 | import com.intellij.psi.PsiWildcardType 37 | import com.intellij.psi.util.TypeConversionUtil 38 | import com.intellij.psi.util.parentOfType 39 | 40 | fun hasNullableAnno(owner: PsiModifierListOwner?): Boolean = 41 | // Kotlin emits declaration annotations only, even though org.jetbrains.annotations.Nullable 42 | // is a type annotation, so honor type annotations if the owner isn't Java (b/354753804) 43 | indicatesNullable(owner?.modifierList, useTypeAnnos = owner?.isJava() != true) 44 | 45 | /** Returns if the receiver is defined in Java. */ 46 | private fun PsiModifierListOwner.isJava() = 47 | language == JavaLanguage.INSTANCE && 48 | // Language is always Java if defined in binary, so additionally check for Kotlin metadata 49 | parentOfType(withSelf = true)?.hasAnnotation("kotlin.Metadata") != true 50 | 51 | fun indicatesNullable(owner: PsiAnnotationOwner?, useTypeAnnos: Boolean = true): Boolean = 52 | // applicableAnnotations is sometimes available where .annotations throws. Note it doesn't work 53 | // correctly for array annos, where useTypeAnnos is still helpful. 54 | owner?.applicableAnnotations?.any { it.isNullableAnno(useTypeAnnos) } == true 55 | 56 | // TODO(b/308816245): use annotation names from JSpecify reference checker (or Error-Prone), see 57 | // https://github.com/jspecify/jspecify-reference-checker/blob/main/src/main/java/com/google/jspecify/nullness/NullSpecAnnotatedTypeFactory.java 58 | fun PsiAnnotation.isNullableAnno(useTypeAnnos: Boolean = true) = 59 | (qualifiedName?.endsWith(".Nullable") == true || 60 | qualifiedName?.endsWith(".NullableDecl") == true || // checkerframework 61 | qualifiedName == "javax.annotation.CheckForNull") && // used in Guava 62 | // This treats annotations that are type and declarations "conservatively" as type annotations 63 | // but that should (?) be ok as long as we look for both. Note 64 | // com.intellij.codeInsight.AnnotationTargetUtil.isStrictlyTypeUseAnnotation doesn't work 65 | // outside IntelliJ but treats nullness annotations as type annotations anyway. 66 | (useTypeAnnos || !isTypeAnnotation(this)) 67 | 68 | fun indicatesNonNull(owner: PsiAnnotationOwner?, useTypeAnnos: Boolean = true): Boolean = 69 | owner?.applicableAnnotations?.any { it.isNonNullAnno(useTypeAnnos) } == true 70 | 71 | fun PsiAnnotation.isNonNullAnno(useTypeAnnos: Boolean = true) = 72 | (qualifiedName?.endsWith(".NonNull") == true || 73 | qualifiedName?.endsWith(".NotNull") == true || 74 | qualifiedName?.endsWith(".NonNullDecl") == true || // checkerframework 75 | (qualifiedName == "javax.annotation.Nonnull" && 76 | findAttributeValue("when")?.textMatches("javax.annotation.meta.When.ALWAYS") == true)) && 77 | (useTypeAnnos || !isTypeAnnotation(this)) 78 | 79 | internal fun nullnessFromAnnotation( 80 | type: PsiType?, 81 | declaration: PsiModifierListOwner? = null, 82 | typeParameter: PsiTypeParameter? = null, 83 | ): Nullness = 84 | when { 85 | type == PsiTypes.nullType() -> Nullness.NULL 86 | type is PsiPrimitiveType -> Nullness.NONULL 87 | hasNullableAnno(declaration) || indicatesNullable(type) -> Nullness.NULLABLE 88 | indicatesNonNull(declaration?.modifierList, useTypeAnnos = false) || indicatesNonNull(type) -> 89 | Nullness.NONULL 90 | type?.hasNullableBound() == true && typeParameter?.hasNullableBound() != false -> 91 | if ( 92 | (type is PsiClassType && type.resolve() is PsiTypeParameter) || 93 | type is PsiWildcardType || 94 | type is PsiCapturedWildcardType 95 | ) { 96 | Nullness.PARAMETRIC 97 | } else { 98 | Nullness.NULLABLE 99 | } 100 | else -> Nullness.NONULL 101 | } 102 | 103 | /** 104 | * Returns if this type has a nullable upper bound, in particular: 105 | * * it [indicatesNullable] itself, or 106 | * * it represents a type variable or captured wildcard that has a nullable bound. 107 | */ 108 | fun PsiType.hasNullableBound(): Boolean = accept(TypeBoundVisitor) 109 | 110 | private object TypeBoundVisitor : PsiTypeVisitor() { 111 | override fun visitType(type: PsiType): Boolean = indicatesNullable(type) 112 | 113 | override fun visitCapturedWildcardType(capturedWildcardType: PsiCapturedWildcardType): Boolean = 114 | (!capturedWildcardType.wildcard.isExtends || capturedWildcardType.upperBound.accept(this)) && 115 | capturedWildcardType.typeParameter?.hasNullableBound() != false 116 | 117 | override fun visitClassType(classType: PsiClassType): Boolean { 118 | if (indicatesNullable(classType)) return true 119 | if (indicatesNonNull(classType)) return false // Don't check bounds below for `@NonNull T` 120 | // Class type can refer to a type parameter, which in turn may be annotated 121 | val referee = classType.resolve() 122 | return (referee is PsiTypeParameter && referee.hasNullableBound()) 123 | } 124 | 125 | override fun visitDisjunctionType(disjunctionType: PsiDisjunctionType): Boolean = 126 | disjunctionType.disjunctions.any { it.accept(this) } 127 | 128 | override fun visitIntersectionType(intersectionType: PsiIntersectionType): Boolean = 129 | intersectionType.conjuncts.all { it.accept(this) } 130 | 131 | override fun visitPrimitiveType(primitiveType: PsiPrimitiveType): Boolean = 132 | TypeConversionUtil.isNullType(primitiveType) 133 | 134 | override fun visitWildcardType(wildcardType: PsiWildcardType): Boolean = 135 | !wildcardType.isExtends || wildcardType.extendsBound.accept(this) 136 | } 137 | 138 | private fun PsiTypeParameter.hasNullableBound(): Boolean { 139 | val bounds = extendsList.referencedTypes 140 | return if (bounds.isEmpty()) !isInNullMarkedScope() else bounds.all { it.hasNullableBound() } 141 | } 142 | 143 | fun PsiElement.isInNullMarkedScope(): Boolean { 144 | var parent: PsiElement? = this 145 | while (parent != null && parent !is PsiFile) { 146 | if (parent is PsiModifierListOwner) { 147 | if (parent.hasAnnotation("org.jspecify.annotations.NullMarked")) { 148 | return true 149 | } 150 | if (parent.hasAnnotation("org.jspecify.annotations.NullUnmarked")) { 151 | return false 152 | } 153 | } 154 | parent = parent.parent 155 | } 156 | // TODO(b/308816245): check package and module @Null[Un]Marked 157 | return false 158 | } 159 | -------------------------------------------------------------------------------- /convert/src/com/google/devtools/jvmtools/analysis/nullness/TypeArgumentVisitor.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devtools.jvmtools.analysis.nullness 18 | 19 | import com.google.devtools.jvmtools.convert.util.runIf 20 | import com.google.devtools.jvmtools.analysis.State 21 | import com.google.devtools.jvmtools.analysis.join 22 | import com.intellij.psi.JavaPsiFacade 23 | import com.intellij.psi.LambdaUtil 24 | import com.intellij.psi.PsiArrayType 25 | import com.intellij.psi.PsiClass 26 | import com.intellij.psi.PsiClassType 27 | import com.intellij.psi.PsiDisjunctionType 28 | import com.intellij.psi.PsiEllipsisType 29 | import com.intellij.psi.PsiIntersectionType 30 | import com.intellij.psi.PsiMethod 31 | import com.intellij.psi.PsiModifierListOwner 32 | import com.intellij.psi.PsiSubstitutor 33 | import com.intellij.psi.PsiType 34 | import com.intellij.psi.PsiTypeParameter 35 | import com.intellij.psi.PsiTypeVisitor 36 | import com.intellij.psi.PsiVariable 37 | import com.intellij.psi.PsiWildcardType 38 | import com.intellij.psi.util.InheritanceUtil 39 | import com.intellij.psi.util.TypeConversionUtil.getSuperClassSubstitutor 40 | import com.intellij.psi.util.TypeConversionUtil.isPrimitiveAndNotNull 41 | import org.jetbrains.uast.UArrayAccessExpression 42 | import org.jetbrains.uast.UCallExpression 43 | import org.jetbrains.uast.UDeclaration 44 | import org.jetbrains.uast.UElement 45 | import org.jetbrains.uast.UExpression 46 | import org.jetbrains.uast.UExpressionList 47 | import org.jetbrains.uast.UIfExpression 48 | import org.jetbrains.uast.ULabeledExpression 49 | import org.jetbrains.uast.ULambdaExpression 50 | import org.jetbrains.uast.UMethod 51 | import org.jetbrains.uast.UParenthesizedExpression 52 | import org.jetbrains.uast.UQualifiedReferenceExpression 53 | import org.jetbrains.uast.UReturnExpression 54 | import org.jetbrains.uast.UastCallKind 55 | import org.jetbrains.uast.getParameterForArgument 56 | import org.jetbrains.uast.visitor.AbstractUastVisitor 57 | import org.jetbrains.uast.visitor.UastTypedVisitor 58 | 59 | /** 60 | * Returns nullness of the receiver method's return value based on the given [analysis] state. 61 | * 62 | * @param analyzeLambda closure to get analysis results for lambda expressions if needed 63 | */ 64 | fun UMethod.returnValueNullness( 65 | analysis: State, 66 | analyzeLambda: (ULambdaExpression) -> State, 67 | ): Nullness? = 68 | TypeArgumentVisitor.returnValueNullness( 69 | TypeArgumentVisitor(this, analysis, analyzeLambda), 70 | selector = listOf(), 71 | ) 72 | 73 | /** 74 | * Returns nullness of the given expression, which is recursively inferred from [analysis] results 75 | * and user-declared type annotations if [Nullness.PARAMETRIC] type parameters are involved. For 76 | * instance, this will return [Nullness.NONULL] for `identity("foo")` and [Nullness.NULLABLE] for 77 | * `identity(null)`, where `identity` is a generic method that returns its argument. 78 | * 79 | * @param analyzeLambda closure to get analysis results for lambda expressions 80 | * @param selector to retrieve nullness of the given type argument of this expression's type (use 81 | * `null` for array components) 82 | * @return inference result or `null` if inference wasn't able to produce any information 83 | */ 84 | internal fun UExpression.resultNullness( 85 | analysis: State, 86 | analyzeLambda: (ULambdaExpression) -> State, 87 | vararg selector: PsiTypeParameter?, 88 | ): Nullness? = accept(TypeArgumentVisitor(this, analysis, analyzeLambda), selector.toList()) 89 | 90 | /** 91 | * For lambdas that are method call arguments, tries to refine lambda parameters that are type 92 | * parameters from other method call arguments. 93 | * 94 | * @param storeContents lambda parameters to refine 95 | * @param analysis analysis results for expressions around this lambda (e.g., parent) 96 | * @param analyzeLambda closure to get analysis results for other lambda expressions 97 | */ 98 | internal fun ULambdaExpression.refineFromArguments( 99 | storeContents: MutableMap, 100 | analysis: (UElement) -> State, 101 | analyzeLambda: (ULambdaExpression) -> State, 102 | ) { 103 | val parentCall = uastParent as? UCallExpression 104 | val parentCallee = parentCall?.resolve() ?: return 105 | val funInterface = (functionalInterfaceType as? PsiClassType)?.resolve() ?: return 106 | val implementedMethod = LambdaUtil.getFunctionalInterfaceMethod(functionalInterfaceType) ?: return 107 | val callParam = parentCall.getParameterForArgument(this) ?: return 108 | val visitor by lazy { TypeArgumentVisitor(parentCall, analysis(parentCall), analyzeLambda) } 109 | for ((idx, param) in implementedMethod.parameterList.parameters.withIndex()) { 110 | if ((param.type as? PsiClassType)?.resolve() !is PsiTypeParameter) continue 111 | funInterface.fakeReceiverType(param.type).visitTypeComponents { component, selector -> 112 | @Suppress("DEPRECATION") // UParameter.psi gives us a PsiParameter as needed 113 | if (param.type.isAssignableFrom(component)) { 114 | val refined = 115 | storeContents[parameters[idx].psi] join 116 | callParam.type.componentNullness(selector, callParam) { remainingSelector -> 117 | visitor.inferFromArguments( 118 | parentCall, 119 | parentCallee, 120 | wantedTypeComponent = this, 121 | remainingSelector, 122 | argsToIgnore = setOf(this@refineFromArguments), 123 | ) 124 | } 125 | if (refined != null) storeContents[parameters[idx].psi] = refined 126 | } 127 | } 128 | } 129 | } 130 | 131 | /** 132 | * Sequence of [PsiTypeParameter]s to "point to" a generic type component, using `null` for array 133 | * components. The [PsiTypeParameter]s' owners must be classes (not methods). Using this 134 | * representation allows identifying type components relative to a particular reference type, which 135 | * is helpful when dealing with subtypes: for instance, we can retrieve an [Iterable]'s type 136 | * argument even when given a `StringList` or similar custom type that itself may declare no or 137 | * different type parameters but declares itself as a subtype of `Iterable`. 138 | * 139 | * @see [componentNullness] 140 | */ 141 | private typealias TypeComponentSelector = List 142 | 143 | /** Calls [block] for each (transitive) type component of this type. */ 144 | private inline fun PsiType.visitTypeComponents( 145 | crossinline block: (PsiType, TypeComponentSelector) -> Unit 146 | ) { 147 | accept( 148 | object : PsiTypeVisitor() { 149 | private val selector = ArrayDeque() 150 | 151 | override fun visitType(type: PsiType) { 152 | block(type, selector) 153 | } 154 | 155 | override fun visitArrayType(arrayType: PsiArrayType) { 156 | super.visitArrayType(arrayType) 157 | selector.addLast(null) 158 | arrayType.componentType.accept(this) 159 | selector.removeLast() 160 | } 161 | 162 | override fun visitClassType(classType: PsiClassType) { 163 | super.visitClassType(classType) 164 | val typeParams = classType.resolve()?.typeParameters ?: PsiTypeParameter.EMPTY_ARRAY 165 | for ((typeArgIndex, typeArg) in classType.parameters.withIndex()) { 166 | selector.addLast(typeParams.getOrNull(typeArgIndex) ?: continue) 167 | typeArg.accept(this) 168 | selector.removeLast() 169 | } 170 | } 171 | } 172 | ) 173 | } 174 | 175 | /** 176 | * Applies the given selector as far as possible and invokes [parametricNullness] if it lands on a 177 | * parametric type variable and the result of [nullnessFromAnnotation] otherwise. 178 | */ 179 | private fun PsiType.componentNullness( 180 | selector: TypeComponentSelector, 181 | decl: PsiModifierListOwner?, 182 | fromIndex: Int = 0, 183 | parametricNullness: PsiType.(TypeComponentSelector) -> Nullness? = { 184 | if (it.isEmpty()) Nullness.PARAMETRIC else null 185 | }, 186 | ): Nullness? { 187 | if (fromIndex == selector.size) { 188 | // Found it! Annotations take precedence even if this is a type parameter. 189 | val annotated = 190 | nullnessFromAnnotation( 191 | this, 192 | runIf(fromIndex == 0) { decl }, // only consider declaration annos at top level 193 | selector.lastOrNull(), 194 | ) 195 | return if (annotated == Nullness.PARAMETRIC) parametricNullness(emptyList()) else annotated 196 | } 197 | 198 | // PsiTypeVisitor automatically "sees through" lambda/method reference types (b/349161001). 199 | // It's a bit inefficient to create a new one on each recursion, but allows the logic above to 200 | // run on all types and without creating a visitor. 201 | return accept( 202 | object : PsiTypeVisitor() { 203 | override fun visitType(type: PsiType): Nullness? { 204 | TODO("Don't know how to traverse $this for $selector [$fromIndex]") 205 | } 206 | 207 | override fun visitArrayType(arrayType: PsiArrayType): Nullness? = 208 | arrayType.componentType.componentNullness(selector, decl, fromIndex + 1, parametricNullness) 209 | 210 | override fun visitClassType(classType: PsiClassType): Nullness? { 211 | val generics: PsiClassType.ClassResolveResult = classType.resolveGenerics() 212 | val clazz = generics.element ?: return null 213 | if (clazz is PsiTypeParameter) return parametricNullness(selector.subList(fromIndex)) 214 | 215 | val selectedParam = selector[fromIndex] ?: return null // expected array type 216 | val paramClass = 217 | requireNotNull(selectedParam.owner as? PsiClass) { 218 | "Selectors must be class type parameters: $selectedParam" 219 | } 220 | // This can happen when visiting wildcard bounds, in particular, Object 221 | if (!InheritanceUtil.isInheritorOrSelf(clazz, paramClass, /* checkDeep= */ true)) { 222 | return null 223 | } 224 | // Extract the selected type argument relative to the parameter's declaring class (see 225 | // getSuperClassSubstitutor's javadoc). 226 | val substitutor = getSuperClassSubstitutor(paramClass, clazz, generics.substitutor) 227 | val selectedType = substitutor.substitute(selectedParam) 228 | return selectedType?.componentNullness(selector, decl, fromIndex + 1, parametricNullness) 229 | } 230 | 231 | override fun visitWildcardType(wildcardType: PsiWildcardType): Nullness? { 232 | val wildcardedParam = selector[fromIndex - 1] 233 | val implicitBounds = 234 | wildcardedParam?.extendsList?.referencedTypes ?: PsiClassType.EMPTY_ARRAY 235 | // If there are implicit bounds, collect their constraints, otherwise ignore since the 236 | // implicit bound is Object on which we can't match a non-empty selector anyway. 237 | val implicitUpper = 238 | runIf(implicitBounds.isNotEmpty()) { 239 | implicitBounds 240 | .mapNotNull { it.componentNullness(selector, decl, fromIndex, parametricNullness) } 241 | .reduceOrNull(Nullness::equate) 242 | } 243 | return when { 244 | wildcardType.isSuper -> { 245 | val lowerBound = 246 | wildcardType.superBound.componentNullness( 247 | selector, 248 | decl, 249 | fromIndex, 250 | parametricNullness, 251 | ) 252 | implicitUpper join lowerBound 253 | } 254 | wildcardType.isExtends -> { 255 | val upperBound = 256 | wildcardType.extendsBound.componentNullness( 257 | selector, 258 | decl, 259 | fromIndex, 260 | parametricNullness, 261 | ) 262 | when { 263 | upperBound == null -> implicitUpper 264 | implicitUpper == null -> upperBound 265 | else -> implicitUpper.equate(upperBound) 266 | } 267 | } 268 | else -> implicitUpper 269 | } 270 | } 271 | 272 | override fun visitIntersectionType(intersectionType: PsiIntersectionType): Nullness? = 273 | intersectionType.superTypes 274 | .mapNotNull { it.componentNullness(selector, decl, fromIndex, parametricNullness) } 275 | .reduceOrNull(Nullness::equate) 276 | 277 | override fun visitDisjunctionType(disjunctionType: PsiDisjunctionType): Nullness? = 278 | disjunctionType.disjunctions 279 | .mapNotNull { it.componentNullness(selector, decl, fromIndex, parametricNullness) } 280 | .reduceOrNull(Nullness::join) 281 | } 282 | ) 283 | } 284 | 285 | private fun PsiClass.fakeReceiverType(knownTypeArg: PsiType? = null): PsiClassType { 286 | val fixedTypeParam = (knownTypeArg as? PsiClassType)?.resolve() as? PsiTypeParameter 287 | var substitutor = PsiSubstitutor.EMPTY 288 | if (fixedTypeParam != null) substitutor = substitutor.put(fixedTypeParam, knownTypeArg) 289 | return JavaPsiFacade.getElementFactory(project).createType(this, substitutor) 290 | } 291 | 292 | private fun List.subList(fromIndex: Int): List = subList(fromIndex, size) 293 | 294 | /** 295 | * Visitor that tries to infer [Nullness] for a given [TypeComponentSelector], often by recursively 296 | * visiting subexpressions and using their [analysis] results or explicitly declared annotations. 297 | * For instance, will infer [Nullness.NULLABLE] as the result type argument's nullness qualifier for 298 | * `List.of("foo", null)`, i.e., the inferred type is `List<@Nullable String>`. 299 | * 300 | * Similar to [org.jspecify.annotations.NullMarked] methods, the initial implementation requires 301 | * `@Nullable` type annotations on type arguments in local variable and method parameter types to 302 | * work correctly. It's also Java 5-like, in that it visits subexpressions but doesn't take the 303 | * target/needed type into account. 304 | */ 305 | private class TypeArgumentVisitor( 306 | private val startNode: UElement, 307 | private val analysis: State, 308 | private val analyzeLambda: (ULambdaExpression) -> State, 309 | ) : UastTypedVisitor { 310 | override fun visitElement(node: UElement, data: TypeComponentSelector): Nullness? { 311 | TODO("Not yet implemented: $node $data") 312 | } 313 | 314 | override fun visitExpression(node: UExpression, data: TypeComponentSelector): Nullness? = 315 | if (node != startNode && data.isEmpty()) { 316 | analysis.value[node] 317 | } else { 318 | expressionTypeNullness(node, data) 319 | } 320 | 321 | private fun expressionTypeNullness( 322 | node: UExpression, 323 | data: TypeComponentSelector, 324 | referencedDeclaration: PsiModifierListOwner? = null, 325 | ): Nullness? = node.getExpressionType()?.componentNullness(data, referencedDeclaration) 326 | 327 | override fun visitCallExpression(node: UCallExpression, data: TypeComponentSelector): Nullness? { 328 | if (node != startNode && data.isEmpty()) { 329 | return analysis.value[node] 330 | } 331 | 332 | when (node.kind) { 333 | UastCallKind.NEW_ARRAY_WITH_DIMENSIONS -> 334 | // Assume new Foo[N] object arrays have nullable elements but are themselves non-null 335 | // TODO(kmb): could check if the innermost dimension is 0 and use non-null in that case 336 | return if ( 337 | data.size < node.valueArgumentCount || 338 | isPrimitiveAndNotNull(node.returnType?.deepComponentType) 339 | ) { 340 | Nullness.NONULL 341 | } else { 342 | Nullness.NULLABLE 343 | } 344 | UastCallKind.NEW_ARRAY_WITH_INITIALIZER, 345 | UastCallKind.NESTED_ARRAY_INITIALIZER -> 346 | return if (data.isEmpty()) { 347 | Nullness.NONULL 348 | } else { 349 | val componentSelector = data.subList(1) 350 | node.valueArguments 351 | .mapNotNull { it.accept(this, componentSelector) } 352 | .reduceOrNull(Nullness::join) ?: expressionTypeNullness(node, data) 353 | } 354 | } 355 | 356 | val callee = node.resolve() ?: return visitExpression(node, data) 357 | 358 | val declaredReturnType = 359 | if (callee.isConstructor) callee.containingClass?.fakeReceiverType() else callee.returnType 360 | return declaredReturnType?.componentNullness(data, callee) { remainingSelector -> 361 | inferFromArguments(node, callee, wantedTypeComponent = this, remainingSelector) 362 | } ?: return expressionTypeNullness(node, data, callee) 363 | } 364 | 365 | fun inferFromArguments( 366 | node: UCallExpression, 367 | callee: PsiMethod, 368 | wantedTypeComponent: PsiType, 369 | remainingSelector: TypeComponentSelector, 370 | argsToIgnore: Set = emptySet(), 371 | ): Nullness? { 372 | val constraints = mutableListOf() 373 | node.receiverType?.let { actualReceiverType -> 374 | // If there's a receiver, collect constraints from it, but use a fake declared receiver type 375 | // that refers to the type parameter we're trying to infer 376 | // TODO(b/308816245): can we do anything with implicit receivers? 377 | val receiver = node.receiver ?: return@let 378 | if (receiver in argsToIgnore) return@let 379 | val fakeReceiverType = callee.containingClass?.fakeReceiverType(wantedTypeComponent) 380 | collectConstraintsFromArgument( 381 | constraints, 382 | wantedTypeComponent, 383 | remainingSelector, 384 | fakeReceiverType ?: actualReceiverType, 385 | receiver, 386 | ) 387 | } 388 | for ((paramIndex, param) in callee.parameterList.parameters.withIndex()) { 389 | val arg = node.getArgumentForParameter(paramIndex) ?: continue 390 | if (arg in argsToIgnore) continue 391 | val paramType = param.type 392 | if (param.isVarArgs && paramType is PsiEllipsisType && arg is UExpressionList) { 393 | for (element in arg.expressions) { 394 | collectConstraintsFromArgument( 395 | constraints, 396 | wantedTypeComponent, 397 | remainingSelector, 398 | paramType.componentType, 399 | element, 400 | ) 401 | } 402 | } else { 403 | collectConstraintsFromArgument( 404 | constraints, 405 | wantedTypeComponent, 406 | remainingSelector, 407 | paramType, 408 | arg, 409 | ) 410 | } 411 | } 412 | return constraints.reduceOrNull(Nullness::join).let { 413 | if (it == Nullness.NULL) Nullness.NULLABLE else it 414 | } 415 | } 416 | 417 | private fun collectConstraintsFromArgument( 418 | dest: MutableList, 419 | wantedTypeComponent: PsiType, 420 | remainingSelector: TypeComponentSelector, 421 | parameterType: PsiType, 422 | arg: UExpression, 423 | ) { 424 | parameterType.visitTypeComponents { typeComponent, paramTypeSelector -> 425 | if (wantedTypeComponent.isAssignableFrom(typeComponent)) { 426 | val fromArg = arg.accept(this, paramTypeSelector + remainingSelector) 427 | if (fromArg != null) dest += fromArg 428 | } 429 | } 430 | } 431 | 432 | override fun visitLambdaExpression( 433 | node: ULambdaExpression, 434 | data: TypeComponentSelector, 435 | ): Nullness? { 436 | if (data.isEmpty()) return Nullness.NONULL // lambdas always evaluate to non-null objects 437 | 438 | // We need a type component of the lambda's functional type (e.g., Supplier). Try to 439 | // infer it from the lambda body by (1) seeing if the type component appears in the implemented 440 | // method's return type and (2) collecting constraints from the body's returned expressions. 441 | val implemented = LambdaUtil.getFunctionalInterfaceMethod(node.functionalInterfaceType) 442 | val constraints = mutableListOf() 443 | val needed = data[0] 444 | val visitor by lazy { TypeArgumentVisitor(node, analyzeLambda(node), analyzeLambda) } 445 | implemented?.returnType?.visitTypeComponents { component, selector -> 446 | if ((component as? PsiClassType)?.resolve()?.isEquivalentTo(needed) == true) { 447 | returnValueNullness(visitor, selector + data.subList(1))?.let { constraints += it } 448 | } 449 | } 450 | 451 | return constraints.reduceOrNull(Nullness::join).let { 452 | if (it == Nullness.NULL) Nullness.NULLABLE else it 453 | } 454 | } 455 | 456 | override fun visitArrayAccessExpression( 457 | node: UArrayAccessExpression, 458 | data: TypeComponentSelector, 459 | ): Nullness? = 460 | if (node != startNode && data.isEmpty()) { 461 | analysis.value[node] 462 | } else { 463 | node.receiver.accept(this, ARRAY_COMPONENT_SELECTOR + data) 464 | } 465 | 466 | override fun visitIfExpression(node: UIfExpression, data: TypeComponentSelector): Nullness? = 467 | if (node.isTernary) { 468 | val thenNullness = node.thenExpression?.accept(this, data) 469 | val elseNullness = node.elseExpression?.accept(this, data) 470 | thenNullness join elseNullness 471 | } else { 472 | super.visitIfExpression(node, data) 473 | } 474 | 475 | override fun visitParenthesizedExpression( 476 | node: UParenthesizedExpression, 477 | data: TypeComponentSelector, 478 | ): Nullness? = node.expression.accept(this, data) 479 | 480 | override fun visitLabeledExpression( 481 | node: ULabeledExpression, 482 | data: TypeComponentSelector, 483 | ): Nullness? = node.expression.accept(this, data) 484 | 485 | override fun visitQualifiedReferenceExpression( 486 | node: UQualifiedReferenceExpression, 487 | data: TypeComponentSelector, 488 | ): Nullness? = 489 | if (node.selector is UCallExpression) { 490 | // UAST models method calls with receiver as a UCallExpr nested into UQualifiedReferenceExpr. 491 | // Just analyze the nested method call in that case (which will still look at the receiver). 492 | node.selector.accept(this, data) 493 | } else { 494 | super.visitQualifiedReferenceExpression(node, data) 495 | } 496 | 497 | companion object { 498 | val ARRAY_COMPONENT_SELECTOR: TypeComponentSelector = listOf(null) 499 | 500 | /** 501 | * Computes [join] of `return` expressions in [visitor]'s root (which should be a method or 502 | * lambda body). This isn't perfect should a return expression be "overridden" in a finally 503 | * block but in most cases good enough. 504 | */ 505 | fun returnValueNullness( 506 | visitor: TypeArgumentVisitor, 507 | selector: TypeComponentSelector, 508 | ): Nullness? { 509 | val root = visitor.startNode 510 | require(root is UMethod || root is ULambdaExpression) { 511 | "Expected visitor for analysis root but got $root" 512 | } 513 | var result: Nullness? = null 514 | root.accept( 515 | object : AbstractUastVisitor() { 516 | override fun visitDeclaration(node: UDeclaration) = node != root 517 | 518 | override fun visitLambdaExpression(node: ULambdaExpression) = node != root 519 | 520 | override fun visitReturnExpression(node: UReturnExpression): Boolean { 521 | node.returnExpression?.let { result = result join it.accept(visitor, selector) } 522 | return true 523 | } 524 | } 525 | ) 526 | return result 527 | } 528 | } 529 | } 530 | -------------------------------------------------------------------------------- /convert/src/com/google/devtools/jvmtools/convert/psi2k/JavaPsiUtils.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devtools.jvmtools.convert.psi2k 18 | 19 | import com.intellij.psi.JavaRecursiveElementWalkingVisitor 20 | import com.intellij.psi.PsiClass 21 | import com.intellij.psi.PsiCodeBlock 22 | import com.intellij.psi.PsiElement 23 | import com.intellij.psi.PsiExpression 24 | import com.intellij.psi.PsiJavaToken 25 | import com.intellij.psi.PsiLocalVariable 26 | import com.intellij.psi.PsiModifier 27 | import com.intellij.psi.PsiModifierList 28 | import com.intellij.psi.PsiModifierListOwner 29 | import com.intellij.psi.PsiParameter 30 | import com.intellij.psi.PsiReferenceExpression 31 | import com.intellij.psi.PsiVariable 32 | import com.intellij.psi.impl.JavaConstantExpressionEvaluator.computeConstantExpression 33 | import com.intellij.psi.util.PsiUtil 34 | import com.intellij.psi.util.parentOfType 35 | 36 | val PsiModifierListOwner.isAbstract: Boolean 37 | get() = hasModifierProperty(PsiModifier.ABSTRACT) 38 | val PsiModifierListOwner.isFinal: Boolean 39 | get() = hasModifierProperty(PsiModifier.FINAL) 40 | val PsiModifierListOwner.isStatic: Boolean 41 | get() = hasModifierProperty(PsiModifier.STATIC) 42 | val PsiModifierListOwner.isPrivate: Boolean 43 | get() = hasModifierProperty(PsiModifier.PRIVATE) 44 | 45 | val PsiModifierList.isStatic: Boolean 46 | get() = hasModifierProperty(PsiModifier.STATIC) 47 | val PsiModifierList.isPrivate: Boolean 48 | get() = hasModifierProperty(PsiModifier.PRIVATE) 49 | 50 | internal fun PsiVariable.isEffectivelyFinal(): Boolean { 51 | if (hasModifierProperty(PsiModifier.FINAL)) return true 52 | val scope = 53 | when (this) { 54 | is PsiParameter -> declarationScope 55 | is PsiLocalVariable -> parentOfType() ?: containingFile 56 | else -> return false // don't do anything clever for fields 57 | } 58 | // The following isn't accurate for variables that are initialized after declaration; need flow 59 | // analysis here. 60 | var effectivelyFinal = true 61 | scope.accept( 62 | object : JavaRecursiveElementWalkingVisitor() { 63 | override fun visitReferenceExpression(expr: PsiReferenceExpression) { 64 | if (expr.resolve() == this@isEffectivelyFinal && PsiUtil.isAccessedForWriting(expr)) { 65 | effectivelyFinal = false 66 | stopWalking() 67 | } 68 | } 69 | } 70 | ) 71 | return effectivelyFinal 72 | } 73 | 74 | internal fun PsiElement.tokenOrNull(): String? = (this as? PsiJavaToken)?.text 75 | 76 | internal fun PsiElement.isToken(value: String): Boolean = tokenOrNull() == value 77 | 78 | internal fun PsiExpression.constantValue(): Any? = 79 | computeConstantExpression(this, /* throwExceptionOnOverflow= */ false) 80 | 81 | /** Get name of a class with all its outer class names */ 82 | internal fun PsiClass.getNameOfPossiblyInnerClass(): String { 83 | val outerName = (parent as? PsiClass)?.getNameOfPossiblyInnerClass() 84 | return if (outerName == null) { 85 | name ?: "" 86 | } else { 87 | "$outerName.$name" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /convert/src/com/google/devtools/jvmtools/convert/psi2k/MappedMethod.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devtools.jvmtools.convert.psi2k 18 | 19 | import com.intellij.psi.PsiClass 20 | import com.intellij.psi.util.InheritanceUtil 21 | 22 | /** Type of method conversion. */ 23 | enum class MappingType { 24 | /** Direct method conversion (name change only). */ 25 | DIRECT, 26 | /** Method-to-property conversion. */ 27 | PROPERTY, 28 | /** Method-to-extension conversion. */ 29 | EXTENSION, 30 | } 31 | 32 | /** Information for mapping a Java method to a Kotlin method (or property). */ 33 | data class MappedMethod( 34 | val className: String, 35 | val javaMethodName: String, 36 | val kotlinName: String, 37 | val type: MappingType, 38 | ) { 39 | 40 | companion object { 41 | /** 42 | * Returns a [MappedMethod] that matches the given method & class, or `null` if it does not 43 | * exist. 44 | */ 45 | fun get(methodRef: String, containingClass: PsiClass?): MappedMethod? = 46 | MAPPED_METHODS_BY_NAME.get(methodRef)?.firstOrNull { method -> 47 | InheritanceUtil.isInheritor(containingClass, method.className) 48 | } 49 | 50 | private val MAPPED_METHODS = 51 | listOf( 52 | // go/keep-sorted block=yes start 53 | MappedMethod( 54 | "com.google.common.base.Strings", 55 | "isNullOrEmpty", 56 | "isNullOrEmpty", 57 | MappingType.EXTENSION, 58 | ), 59 | MappedMethod("java.lang.CharSequence", "charAt", "get", MappingType.DIRECT), 60 | MappedMethod("java.lang.CharSequence", "length", "length", MappingType.PROPERTY), 61 | MappedMethod("java.lang.Object", "getClass", "javaClass", MappingType.PROPERTY), 62 | MappedMethod("java.lang.String", "getBytes", "toByteArray", MappingType.DIRECT), 63 | MappedMethod("java.lang.String", "valueOf", "toString", MappingType.EXTENSION), 64 | MappedMethod("java.lang.Throwable", "getCause", "cause", MappingType.PROPERTY), 65 | MappedMethod("java.lang.Throwable", "getMessage", "message", MappingType.PROPERTY), 66 | MappedMethod("java.util.Collection", "size", "size", MappingType.PROPERTY), 67 | MappedMethod("java.util.Map", "entrySet", "entries", MappingType.PROPERTY), 68 | MappedMethod("java.util.Map", "keySet", "keys", MappingType.PROPERTY), 69 | MappedMethod("java.util.Map", "size", "size", MappingType.PROPERTY), 70 | MappedMethod("java.util.Map", "values", "values", MappingType.PROPERTY), 71 | MappedMethod("java.util.Map.Entry", "getKey", "key", MappingType.PROPERTY), 72 | MappedMethod("java.util.Map.Entry", "getValue", "value", MappingType.PROPERTY), 73 | // go/keep-sorted end 74 | ) 75 | 76 | private val MAPPED_METHODS_BY_NAME: Map> = 77 | MAPPED_METHODS.groupBy { it.javaMethodName } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /convert/src/com/google/devtools/jvmtools/convert/util/ControlFlow.kt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2024 Google LLC 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * https://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.google.devtools.jvmtools.convert.util 18 | 19 | /** 20 | * Evaluates [then] if [condition] and otherwise returns `null`. 21 | * 22 | * @see https://youtrack.jetbrains.com/issue/KT-6938/Boolean.ifTrue 23 | */ 24 | internal inline fun runIf(condition: Boolean, then: () -> R): R? = 25 | if (condition) then() else null 26 | -------------------------------------------------------------------------------- /docs/contributing.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | We would love to accept your patches and contributions to this project. 4 | 5 | ## Before you begin 6 | 7 | ### Sign our Contributor License Agreement 8 | 9 | Contributions to this project must be accompanied by a 10 | [Contributor License Agreement](https://cla.developers.google.com/about) (CLA). 11 | You (or your employer) retain the copyright to your contribution; this simply 12 | gives us permission to use and redistribute your contributions as part of the 13 | project. 14 | 15 | If you or your current employer have already signed the Google CLA (even if it 16 | was for a different project), you probably don't need to do it again. 17 | 18 | Visit to see your current agreements or to 19 | sign a new one. 20 | 21 | ### Review our Community Guidelines 22 | 23 | This project follows [Google's Open Source Community 24 | Guidelines](https://opensource.google/conduct/). 25 | 26 | ## Contribution process 27 | 28 | ### Code Reviews 29 | 30 | All submissions, including submissions by project members, require review. We 31 | use [GitHub pull requests](https://docs.github.com/articles/about-pull-requests) 32 | for this purpose. 33 | 34 | -------------------------------------------------------------------------------- /repositories.bzl: -------------------------------------------------------------------------------- 1 | # Copyright 2024 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | """Functions to load repositories for this workspace.""" 16 | 17 | load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") 18 | 19 | def rules_jvm_external(): 20 | tag = "6.1" 21 | sha = "08ea921df02ffe9924123b0686dc04fd0ff875710bfadb7ad42badb931b0fd50" 22 | 23 | http_archive( 24 | name = "rules_jvm_external", 25 | strip_prefix = "rules_jvm_external-%s" % tag, 26 | sha256 = sha, 27 | url = "https://github.com/bazelbuild/rules_jvm_external/releases/download/%s/rules_jvm_external-%s.tar.gz" % (tag, tag), 28 | ) 29 | 30 | def rules_kotlin(): 31 | http_archive( 32 | name = "rules_kotlin", 33 | sha256 = "34e8c0351764b71d78f76c8746e98063979ce08dcf1a91666f3f3bc2949a533d", 34 | url = "https://github.com/bazelbuild/rules_kotlin/releases/download/v1.9.5/rules_kotlin-v1.9.5.tar.gz", 35 | ) 36 | --------------------------------------------------------------------------------