├── .gitignore ├── .run └── Run IDE with Plugin.run.xml ├── LICENSE ├── README.md ├── build.gradle.kts ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle.kts └── src └── main ├── kotlin └── dev │ └── programadorthi │ └── migration │ ├── MigrationAction.kt │ ├── ext │ └── StringExt.kt │ ├── migration │ ├── BuildGradleMigration.kt │ ├── BuildGradleStatusProvider.kt │ ├── ClassWithSetContentViewMigration.kt │ ├── CommonAndroidClassMigration.kt │ ├── CommonMigration.kt │ ├── FileMigration.kt │ ├── GroupieMigration.kt │ ├── ParcelizeMigration.kt │ ├── ParcelizeStatusProvider.kt │ └── ViewMigration.kt │ ├── model │ ├── AndroidView.kt │ ├── BindingData.kt │ ├── BindingFunction.kt │ ├── BindingType.kt │ ├── BuildGradleItem.kt │ └── MigrationStatus.kt │ ├── notification │ └── MigrationNotification.kt │ ├── processor │ └── MigrationProcessor.kt │ └── visitor │ ├── BuildGradleVisitor.kt │ └── SyntheticReferenceRecursiveVisitor.kt └── resources └── META-INF ├── plugin.xml └── pluginIcon.svg /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | !gradle/wrapper/gradle-wrapper.jar 4 | !**/src/main/**/build/ 5 | !**/src/test/**/build/ 6 | 7 | ### IntelliJ IDEA ### 8 | .idea 9 | *.iws 10 | *.iml 11 | *.ipr 12 | out/ 13 | !**/src/main/**/out/ 14 | !**/src/test/**/out/ 15 | 16 | ### Eclipse ### 17 | .apt_generated 18 | .classpath 19 | .factorypath 20 | .project 21 | .settings 22 | .springBeans 23 | .sts4-cache 24 | bin/ 25 | !**/src/main/**/bin/ 26 | !**/src/test/**/bin/ 27 | 28 | ### NetBeans ### 29 | /nbproject/private/ 30 | /nbbuild/ 31 | /dist/ 32 | /nbdist/ 33 | /.nb-gradle/ 34 | 35 | ### VS Code ### 36 | .vscode/ 37 | 38 | ### Mac OS ### 39 | .DS_Store 40 | 41 | local.properties 42 | /.idea/ 43 | by-hand-things.txt -------------------------------------------------------------------------------- /.run/Run IDE with Plugin.run.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 12 | 17 | 19 | true 20 | true 21 | false 22 | 23 | 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Thiago Santos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # synthetic-to-viewbinding 2 | A Intellij plugin to migrate Kotlin synthetics to Jetpack view binding 3 | 4 | > Please, this plugin is not perfect. Read all sections below before use it. 5 | > 6 | > It was developed using my context that can be totally different from your. Keep it in mind! 7 | 8 | #### This plugin supports kotlin source and build.gradle(.kts) files only. 9 | 10 | ## How to use 11 | 1. Install the plugin from Marketplace (link soon) 12 | 2. On the android module, enable viewBinding in your build.gradle(.kts) 13 | ```groovy 14 | android { 15 | // New AGP version setup 16 | viewBinding.enable = true 17 | // Old AGP version setup 18 | buildFeatures { 19 | viewBinding true 20 | } 21 | } 22 | ``` 23 | 3. Right click on your class or module and select the menu: `Refactor -> Migrate Synthetic to ViewBinding` 24 | 4. In the popup select your options, filters and click in the button `Run`. 25 | 26 | > Popup options are from your IDE code styles settings and run after view binding migration. 27 | 28 | ## Features 29 | 30 | - Activity, Dialog, ViewGroup and View migration 31 | - Replace setContentView(R.layout.name) with setContentView(binding.root) 32 | - Remove View.inflate() or LayoutInflate.inflate from init {} blocks 33 | - Support for multiple synthetics in the same class 34 | - Remove plugin and android extensions configurations from build(.gradle|.kts) 35 | - Update @Parcelize imports and add plugin to build(.gradle|.kts) 36 | - Generate bind behaviors to ViewStub inflate() 37 | - Organize imports, Reformat code, Code cleanup based on your IDE code style settings 38 | 39 | > At the end, all synthetics references become lazy {} properties because replacing all references with `binding.` prefix is a mess and is easy for pull request reviewers. 40 | 41 | From: 42 | ```kotlin 43 | class MyClass : AnySupportedType { 44 | fun something() { 45 | synthetic1.do() 46 | synthetic2.do() 47 | ... 48 | } 49 | } 50 | ``` 51 | To: 52 | ```kotlin 53 | class MyClass : AnySupportedType { 54 | // For Activity, Dialog, ViewGroup or View without parent 55 | // Inside MyClass is the same as: MyViewBinding.inflate(layoutInflater) 56 | private val bindingName by viewBinding(MyViewBinding::inflate) 57 | // For ViewGroup or View used as xml root tag and it is inflated already 58 | // Like: my_layout.xml 59 | // ... 60 | // Inside MyClass is the same as: MyLayoutBinding.bind(this) 61 | private val bindingName by viewBinding(MyLayoutBinding::bind) 62 | // For ViewGroup or View not used as xml root tag and will be the parent 63 | // Like: my_layout.xml 64 | // ... 65 | // Inside MyClass is the same as: MyLayoutBinding.inflate(layoutInflater, this, true) 66 | private val bindingName by viewBindingAsChild(MyLayoutBinding::inflate) 67 | // For ViewGroup or View having as xml root tag. 68 | // Like: my_layout.xml 69 | // ... 70 | // Inside MyClass is the same as: MyLayoutBinding.inflate(layoutInflater, this) 71 | private val bindingName by viewBindingMergeTag(MyLayoutBinding::inflate) 72 | 73 | private val synthetic1 by lazy { bindingName.synthetic1 } 74 | private val synthetic2 by lazy { bindingName.synthetic2 } 75 | 76 | // for layout 77 | // include layout binding are, almost, already resolved by view binding plugin 78 | // so we do not need do manual bind like: MyIncludeLayout.bind(bindingName.root) 79 | private val includeId by lazy { bindingName.includeId } 80 | 81 | // for ViewStub 82 | private val viewStubId by lazy { 83 | val view = bindingName.viewStubId.inflate() 84 | ViewStubBinding.bind(view) 85 | } 86 | 87 | fun something() { 88 | synthetic1.do() 89 | synthetic2.do() 90 | includeId.do() 91 | viewStubId.something() 92 | } 93 | } 94 | ``` 95 | 96 | Things to know here: 97 | 1. `viewBinding` came from our custom extension functions and all rights to [@Zhuinden](https://github.com/Zhuinden/simple-stack) article [Simple one-liner ViewBinding in Fragments and Activities with Kotlin](https://medium.com/@Zhuinden/simple-one-liner-viewbinding-in-fragments-and-activities-with-kotlin-961430c6c07c) 98 | 2. If you do not have this kind of extensions, you are free to adapt the plugin. Maybe I do in the future. Pull requests are welcome! 99 | 100 | ## Groupie features 101 | 102 | - Replace super type from Item to BindableItem 103 | 104 | From: 105 | ```kotlin 106 | class MyViewHolder : Item() { 107 | ... 108 | } 109 | ``` 110 | To: 111 | ```kotlin 112 | class MyViewHolder : BindableItem() { 113 | ... 114 | } 115 | ``` 116 | 117 | - Add initializeViewBinding(view) function 118 | 119 | ```kotlin 120 | class MyViewHolder : BindableItem() { 121 | override fun initializeViewBinding(view: View): MyViewBinding = 122 | MyViewBinding.bind(view) 123 | } 124 | ``` 125 | 126 | - Replace `itemView` or `contentView` with `root` 127 | - Replace `GroupieViewHolder` functions parameters with MyViewBinding class 128 | 129 | From: 130 | ```kotlin 131 | class MyViewHolder : Item() { 132 | override fun bind(viewHolder: GroupieViewHolder, position: Int) { 133 | ... 134 | } 135 | } 136 | ``` 137 | To: 138 | ```kotlin 139 | class MyViewHolder : BindableItem() { 140 | override fun bind(viewHolder: MyViewBinding, position: Int) { 141 | ... 142 | } 143 | } 144 | ``` 145 | 146 | # Things not supported to keep in mind 147 | 148 | - Fragments because we do not use it anywhere! :D 149 | - RecyclerView.Adapter because we use Groupie instead. 150 | - Inner classes 151 | - Constructor layout Activity|Fragment(R.layout.something) 152 | - Functions having `GroupieViewHolder` as argument type but there is no synthetic references in your body are not changed by default. 153 | ```kotlin 154 | class MyViewHolder : Item() { 155 | // bind parameter type will not be replaced because it is difficult to know the view binding type 156 | override fun bind(viewHolder: GroupieViewHolder, position: Int) { 157 | // No synthetic references here or used in another function like 158 | functionHavingSyntheticReferences(viewHolder) 159 | } 160 | } 161 | ``` 162 | - Functions having multiples GroupieViewHolder because I do not know whom is the source of truth to synthetics inside it. 163 | ```kotlin 164 | class MyViewHolder : Item() { 165 | // Two references to GroupieViewHolder to know the source of truth to synthetics :/ 166 | fun GroupieViewHolder.doSomenthing(other: GroupieViewHolder) { 167 | // synthetic references here 168 | } 169 | } 170 | ``` 171 | - Referencing root view with itemView at same time because itemView is root already 172 | 173 | From: 174 | ```kotlin 175 | class MyViewHolder : Item() { 176 | fun doSomenthing(viewHolder: GroupieViewHolder) { 177 | // here itemView and rootTagId are the same root tag 178 | viewHolder.itemView.rootTagId.something() 179 | } 180 | } 181 | ``` 182 | To: 183 | ```kotlin 184 | class MyViewHolder : BindableItem() { 185 | fun doSomenthing(viewHolder: MyViewBinding) { 186 | // will change to root but it is an error 187 | viewHolder.root.rootTagId.something() 188 | } 189 | } 190 | ``` 191 | - ViewStub resolution property when declared by lazy { viewStub.inflate() } or referenced in . As you saw above, properties for ViewStub are automatically generated. So, if you have custom inflation, you need to fix 192 | - layouts binding not resolved by view binding plugin. As you saw above, includes are almost resolve by view binding plugin. But, sometimes, you will need to do manual binding. 193 | 194 | ## Author 195 | - Thiago Santos - [@programadorthi](https://github.com/programadorthi) -------------------------------------------------------------------------------- /build.gradle.kts: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java") 3 | id("org.jetbrains.kotlin.jvm") version "1.7.20" 4 | id("org.jetbrains.intellij") version "1.10.1" 5 | } 6 | 7 | group = "dev.programadorthi" 8 | version = "1.0-SNAPSHOT" 9 | 10 | repositories { 11 | mavenCentral() 12 | 13 | } 14 | 15 | // Configure Gradle IntelliJ Plugin 16 | // Read more: https://plugins.jetbrains.com/docs/intellij/tools-gradle-intellij-plugin.html 17 | intellij { 18 | version.set("2022.1.1.19") 19 | type.set("AI") // Target IDE Platform 20 | 21 | plugins.set(listOf("android", "java", "org.jetbrains.kotlin")) 22 | } 23 | 24 | tasks { 25 | // Set the JVM compatibility versions 26 | withType { 27 | sourceCompatibility = "11" 28 | targetCompatibility = "11" 29 | } 30 | withType { 31 | kotlinOptions.jvmTarget = "11" 32 | } 33 | 34 | patchPluginXml { 35 | sinceBuild.set("221") 36 | untilBuild.set("231.*") 37 | } 38 | 39 | signPlugin { 40 | certificateChain.set(System.getenv("CERTIFICATE_CHAIN")) 41 | privateKey.set(System.getenv("PRIVATE_KEY")) 42 | password.set(System.getenv("PRIVATE_KEY_PASSWORD")) 43 | } 44 | 45 | publishPlugin { 46 | token.set(System.getenv("PUBLISH_TOKEN")) 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/programadorthi/synthetic-to-viewbinding/ac0bdfd1add6d6c37d8eebf96666833f259e910c/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Use "xargs" to parse quoted args. 209 | # 210 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 211 | # 212 | # In Bash we could simply go: 213 | # 214 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 215 | # set -- "${ARGS[@]}" "$@" 216 | # 217 | # but POSIX shell has neither arrays nor command substitution, so instead we 218 | # post-process each arg (as a line of input to sed) to backslash-escape any 219 | # character that might be a shell metacharacter, then use eval to reverse 220 | # that process (while maintaining the separation between arguments), and wrap 221 | # the whole thing up as a single "set" statement. 222 | # 223 | # This will of course break if any of these variables contains a newline or 224 | # an unmatched quote. 225 | # 226 | 227 | eval "set -- $( 228 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 229 | xargs -n1 | 230 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 231 | tr '\n' ' ' 232 | )" '"$@"' 233 | 234 | exec "$JAVACMD" "$@" 235 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%" == "" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%" == "" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if "%ERRORLEVEL%" == "0" goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if "%ERRORLEVEL%"=="0" goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 84 | exit /b 1 85 | 86 | :mainEnd 87 | if "%OS%"=="Windows_NT" endlocal 88 | 89 | :omega 90 | -------------------------------------------------------------------------------- /settings.gradle.kts: -------------------------------------------------------------------------------- 1 | rootProject.name = "synthetic-to-viewbinding" -------------------------------------------------------------------------------- /src/main/kotlin/dev/programadorthi/migration/MigrationAction.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.migration 2 | 3 | import com.android.tools.idea.concurrency.addCallback 4 | import com.android.tools.idea.databinding.module.LayoutBindingModuleCache 5 | import com.android.tools.idea.databinding.project.LayoutBindingEnabledFacetsProvider 6 | import com.android.tools.idea.databinding.psiclass.LightBindingClass 7 | import com.android.tools.idea.gradle.project.GradleProjectInfo 8 | import com.android.tools.idea.gradle.project.build.invoker.GradleBuildInvoker 9 | import com.android.tools.idea.gradle.project.build.invoker.TestCompileType 10 | import com.android.tools.idea.gradle.project.model.GradleAndroidModel 11 | import com.android.tools.idea.gradle.project.sync.GradleSyncState 12 | import com.google.common.util.concurrent.MoreExecutors 13 | import com.intellij.codeInsight.CodeInsightBundle 14 | import com.intellij.codeInsight.actions.AbstractLayoutCodeProcessor 15 | import com.intellij.codeInsight.actions.CodeCleanupCodeProcessor 16 | import com.intellij.codeInsight.actions.DirectoryFormattingOptions 17 | import com.intellij.codeInsight.actions.LayoutDirectoryDialog 18 | import com.intellij.codeInsight.actions.LayoutProjectCodeDialog 19 | import com.intellij.codeInsight.actions.OptimizeImportsProcessor 20 | import com.intellij.codeInsight.actions.RearrangeCodeProcessor 21 | import com.intellij.codeInsight.actions.ReformatFilesOptions 22 | import com.intellij.find.impl.FindInProjectUtil 23 | import com.intellij.ide.lightEdit.LightEditCompatible 24 | import com.intellij.openapi.actionSystem.AnAction 25 | import com.intellij.openapi.actionSystem.AnActionEvent 26 | import com.intellij.openapi.actionSystem.CommonDataKeys 27 | import com.intellij.openapi.actionSystem.LangDataKeys 28 | import com.intellij.openapi.application.ApplicationManager 29 | import com.intellij.openapi.diagnostic.Logger 30 | import com.intellij.openapi.module.Module 31 | import com.intellij.openapi.progress.ProgressManager 32 | import com.intellij.openapi.project.DumbAware 33 | import com.intellij.openapi.project.DumbService 34 | import com.intellij.openapi.project.Project 35 | import com.intellij.openapi.util.Conditions 36 | import com.intellij.openapi.util.Ref 37 | import com.intellij.psi.PsiDirectory 38 | import com.intellij.psi.PsiDirectoryContainer 39 | import com.intellij.psi.PsiDocumentManager 40 | import com.intellij.psi.search.SearchScope 41 | import com.intellij.util.ArrayUtil 42 | import com.intellij.util.concurrency.Semaphore 43 | import dev.programadorthi.migration.notification.MigrationNotification 44 | import dev.programadorthi.migration.processor.MigrationProcessor 45 | import org.jetbrains.android.util.AndroidUtils 46 | import org.jetbrains.kotlin.idea.util.module 47 | import java.util.regex.PatternSyntaxException 48 | 49 | class MigrationAction : AnAction(), DumbAware, LightEditCompatible { 50 | private val log = Logger.getInstance(MigrationAction::class.java) 51 | 52 | init { 53 | isEnabledInModalContext = true 54 | setInjectedContext(true) 55 | } 56 | 57 | override fun update(e: AnActionEvent) { 58 | e.presentation.isEnabledAndVisible = canPerform(e) 59 | } 60 | 61 | override fun actionPerformed(event: AnActionEvent) { 62 | if (!canPerform(event)) { 63 | return 64 | } 65 | 66 | val dataContext = event.dataContext 67 | val project = requireNotNull(CommonDataKeys.PROJECT.getData(dataContext)) { 68 | "No project found to do migration" 69 | } 70 | MigrationNotification.setProject(project) 71 | val psiElement = CommonDataKeys.PSI_ELEMENT.getData(dataContext) 72 | val moduleContext = LangDataKeys.MODULE_CONTEXT.getData(dataContext) 73 | val module = moduleContext ?: psiElement?.module 74 | if (psiElement == null || module == null) { 75 | MigrationNotification.showError("Selecting a module or file is required to do a migration") 76 | return 77 | } 78 | 79 | val model = GradleAndroidModel.get(module) 80 | if (model == null || model.androidProject.viewBindingOptions?.enabled != true) { 81 | MigrationNotification.showError("View Binding not enabled in the build.gradle(.kts)") 82 | return 83 | } 84 | 85 | if (moduleContext != null) { 86 | tryModuleMigration(project = project, module = module) 87 | } else { 88 | val dir = when (psiElement) { 89 | is PsiDirectoryContainer -> ArrayUtil.getFirstElement(psiElement.directories) 90 | is PsiDirectory -> psiElement 91 | else -> psiElement.containingFile?.containingDirectory 92 | } 93 | if (dir == null) { 94 | MigrationNotification.showError("No directory selected to do migration") 95 | return 96 | } 97 | tryDirectoryMigration(project = project, module = module, dir = dir) 98 | } 99 | } 100 | 101 | private fun tryModuleMigration(project: Project, module: Module) { 102 | val selectedFlags = getLayoutModuleOptions(project, module) ?: return 103 | PsiDocumentManager.getInstance(project).commitAllDocuments() 104 | registerAndRunProcessor( 105 | project = project, 106 | module = module, 107 | selectedFlags = selectedFlags, 108 | initialProcessor = MigrationProcessor(project, module), 109 | ) 110 | } 111 | 112 | private fun tryDirectoryMigration(project: Project, module: Module, dir: PsiDirectory) { 113 | val selectedFlags = getDirectoryFormattingOptions(project, dir) ?: return 114 | PsiDocumentManager.getInstance(project).commitAllDocuments() 115 | 116 | registerAndRunProcessor( 117 | project = project, 118 | module = module, 119 | selectedFlags = selectedFlags, 120 | initialProcessor = MigrationProcessor( 121 | project, 122 | dir, 123 | selectedFlags.isIncludeSubdirectories, 124 | ), 125 | ) 126 | } 127 | 128 | private fun checkGradleBuild(project: Project, module: Module): List { 129 | val result = Ref>(emptyList()) 130 | 131 | ProgressManager.getInstance().runProcessWithProgressSynchronously({ 132 | val indicator = ProgressManager.getInstance().progressIndicator 133 | indicator.isIndeterminate = true 134 | val targetDone = Semaphore() 135 | targetDone.down() 136 | GradleBuildInvoker.getInstance(project).compileJava( 137 | modules = arrayOf(module), 138 | testCompileType = TestCompileType.NONE, 139 | ).addCallback( 140 | MoreExecutors.directExecutor(), 141 | { taskResult -> 142 | try { 143 | ProgressManager.checkCanceled() 144 | if (taskResult?.isBuildSuccessful == true) { 145 | val enabledFacetsProvider = LayoutBindingEnabledFacetsProvider.getInstance(project) 146 | val bindings = enabledFacetsProvider.getAllBindingEnabledFacets() 147 | .flatMap { facet -> 148 | val bindingModuleCache = LayoutBindingModuleCache.getInstance(facet) 149 | bindingModuleCache.bindingLayoutGroups.flatMap { group -> 150 | bindingModuleCache.getLightBindingClasses(group) 151 | } 152 | } 153 | result.set(bindings) 154 | } else { 155 | MigrationNotification.showError("Gradle ViewBinding generation finished without success") 156 | } 157 | } finally { 158 | targetDone.up() 159 | } 160 | }, 161 | { 162 | try { 163 | MigrationNotification.showError("Error when generating ViewBinding classes") 164 | it?.printStackTrace() 165 | } finally { 166 | targetDone.up() 167 | } 168 | }, 169 | ) 170 | targetDone.waitFor() 171 | indicator.isIndeterminate = false 172 | }, "Generating ViewBinding classes...", true, project) 173 | 174 | return result.get() 175 | } 176 | 177 | private fun registerAndRunProcessor( 178 | project: Project, 179 | module: Module, 180 | initialProcessor: MigrationProcessor, 181 | selectedFlags: ReformatFilesOptions, 182 | ) { 183 | val shouldOptimizeImports = selectedFlags.isOptimizeImports && !DumbService.getInstance(project).isDumb 184 | var processor: AbstractLayoutCodeProcessor = initialProcessor 185 | 186 | registerScopeFilter(processor, selectedFlags.searchScope) 187 | registerFileMaskFilter(processor, selectedFlags.fileTypeMask) 188 | 189 | if (shouldOptimizeImports) { 190 | processor = OptimizeImportsProcessor(processor) 191 | } 192 | 193 | if (selectedFlags.isRearrangeCode) { 194 | processor = RearrangeCodeProcessor(processor) 195 | } 196 | 197 | if (selectedFlags.isCodeCleanup) { 198 | processor = CodeCleanupCodeProcessor(processor) 199 | } 200 | 201 | val bindingClasses = checkGradleBuild(project, module) 202 | if (bindingClasses.isNotEmpty()) { 203 | initialProcessor.addBindingClasses(bindingClasses) 204 | processor.run() 205 | } 206 | } 207 | 208 | private fun registerFileMaskFilter(processor: AbstractLayoutCodeProcessor, fileTypeMask: String?) { 209 | if (fileTypeMask == null) return 210 | val patternCondition = try { 211 | FindInProjectUtil.createFileMaskCondition(fileTypeMask) 212 | } catch (ex: PatternSyntaxException) { 213 | log.error("Error while creating file mask condition: ", ex) 214 | Conditions.alwaysTrue() 215 | } 216 | processor.addFileFilter { file -> 217 | patternCondition.value(file.nameSequence) 218 | } 219 | } 220 | 221 | private fun registerScopeFilter(processor: AbstractLayoutCodeProcessor, scope: SearchScope?) { 222 | if (scope == null) return 223 | processor.addFileFilter(scope::contains) 224 | } 225 | 226 | private fun getLayoutModuleOptions(project: Project, moduleContext: Module?): ReformatFilesOptions? { 227 | if (ApplicationManager.getApplication().isUnitTestMode) { 228 | return null 229 | } 230 | val text = if (moduleContext != null) { 231 | CodeInsightBundle.message("process.scope.module", moduleContext.moduleFilePath) 232 | } else { 233 | CodeInsightBundle.message("process.scope.project", project.presentableUrl) 234 | } 235 | val dialog = LayoutProjectCodeDialog( 236 | project, 237 | MigrationProcessor.getCommandName(), 238 | text, 239 | false, 240 | ) 241 | if (dialog.showAndGet()) { 242 | return dialog 243 | } 244 | return null 245 | } 246 | 247 | private fun getDirectoryFormattingOptions(project: Project, dir: PsiDirectory): DirectoryFormattingOptions? { 248 | val dialog = LayoutDirectoryDialog( 249 | project, 250 | MigrationProcessor.getCommandName(), 251 | CodeInsightBundle.message("process.scope.directory", dir.virtualFile.path), 252 | false, 253 | ) 254 | val enableIncludeDirectoriesCb = dir.subdirectories.isNotEmpty() 255 | dialog.setEnabledIncludeSubdirsCb(enableIncludeDirectoriesCb) 256 | dialog.setSelectedIncludeSubdirsCb(enableIncludeDirectoriesCb) 257 | if (dialog.showAndGet()) { 258 | return dialog 259 | } 260 | return null 261 | } 262 | 263 | private fun canPerform(e: AnActionEvent): Boolean { 264 | val project = e.project ?: return false 265 | return GradleProjectInfo.getInstance(project).isBuildWithGradle && 266 | !GradleSyncState.getInstance(project).isSyncInProgress && 267 | AndroidUtils.hasAndroidFacets(project) 268 | } 269 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/programadorthi/migration/ext/StringExt.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.migration.ext 2 | 3 | import android.databinding.tool.ext.capitalizeUS 4 | import android.databinding.tool.ext.stripNonJava 5 | 6 | fun String.layoutToBindingName(): String = 7 | stripNonJava().capitalizeUS() + "Binding" -------------------------------------------------------------------------------- /src/main/kotlin/dev/programadorthi/migration/migration/BuildGradleMigration.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.migration.migration 2 | 3 | import com.intellij.openapi.diagnostic.Logger 4 | import com.intellij.psi.PsiFile 5 | import dev.programadorthi.migration.model.MigrationStatus 6 | import dev.programadorthi.migration.notification.MigrationNotification 7 | import dev.programadorthi.migration.visitor.BuildGradleVisitor 8 | import org.jetbrains.kotlin.idea.configuration.externalProjectPath 9 | import org.jetbrains.kotlin.idea.util.module 10 | 11 | internal object BuildGradleMigration { 12 | const val BUILD_GRADLE_FILE_NAME = "build.gradle" 13 | const val BUILD_GRADLE_KTS_FILE_NAME = "build.gradle.kts" 14 | 15 | private val log = Logger.getInstance(BuildGradleMigration::class.java) 16 | 17 | fun migrateScript(psiFile: PsiFile, buildGradleStatusProvider: BuildGradleStatusProvider) { 18 | val modulePath = psiFile.module?.externalProjectPath ?: return 19 | if (buildGradleStatusProvider.currentBuildGradleStatus(modulePath) != MigrationStatus.NOT_STARTED) return 20 | 21 | runCatching { 22 | buildGradleStatusProvider.updateBuildGradleStatus(modulePath, MigrationStatus.IN_PROGRESS) 23 | migrateScript(psiFile) 24 | }.onFailure { 25 | log.error("Failed migrate gradle file android extensions setups", it) 26 | buildGradleStatusProvider.updateBuildGradleStatus(modulePath, MigrationStatus.NOT_STARTED) 27 | }.onSuccess { 28 | buildGradleStatusProvider.updateBuildGradleStatus(modulePath, MigrationStatus.DONE) 29 | MigrationNotification.showInfo("${psiFile.name} migration successfully!") 30 | } 31 | } 32 | 33 | private fun migrateScript(psiFile: PsiFile) { 34 | val visitor = BuildGradleVisitor() 35 | psiFile.accept(visitor) 36 | for (item in visitor.buildGradleItems) { 37 | item.element.delete() 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/programadorthi/migration/migration/BuildGradleStatusProvider.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.migration.migration 2 | 3 | import dev.programadorthi.migration.model.MigrationStatus 4 | 5 | internal interface BuildGradleStatusProvider { 6 | fun currentBuildGradleStatus(path: String): MigrationStatus 7 | 8 | fun updateBuildGradleStatus(path: String, status: MigrationStatus) 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/programadorthi/migration/migration/ClassWithSetContentViewMigration.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.migration.migration 2 | 3 | import android.databinding.tool.writer.ViewBinder 4 | import com.android.tools.idea.databinding.psiclass.LightBindingClass 5 | import com.intellij.psi.PsiElement 6 | import dev.programadorthi.migration.ext.layoutToBindingName 7 | import dev.programadorthi.migration.model.BindingData 8 | import dev.programadorthi.migration.model.BindingFunction 9 | import dev.programadorthi.migration.model.BindingType 10 | import org.jetbrains.kotlin.psi.KtClass 11 | import org.jetbrains.kotlin.psi.KtNamedFunction 12 | 13 | internal class ClassWithSetContentViewMigration( 14 | private val ktClass: KtClass, 15 | bindingData: List, 16 | bindingClass: List, 17 | ) : CommonAndroidClassMigration(ktClass, bindingData, bindingClass) { 18 | 19 | override fun mapToFunctionAndType( 20 | bindingClassName: String, 21 | propertyName: String, 22 | rootNode: ViewBinder.RootNode, 23 | ): Pair { 24 | val setContentView = findSetContentView(ktClass) ?: return BindingFunction.DEFAULT to BindingType.INFLATE 25 | val layoutNameAsBinding = setContentView.text 26 | .substringAfterLast('.') 27 | .removeSuffix(")") 28 | .layoutToBindingName() 29 | if (layoutNameAsBinding == bindingClassName) { 30 | val setContentViewWithBinding = SET_CONTENT_VIEW_TEMPLATE.format(propertyName) 31 | val expression = psiFactory.createExpression(setContentViewWithBinding) 32 | setContentView.replace(expression) 33 | } 34 | return BindingFunction.DEFAULT to BindingType.INFLATE 35 | } 36 | 37 | private fun findSetContentView(ktClass: KtClass): PsiElement? { 38 | val allBodyFunctions = ktClass.body?.children?.filterIsInstance() ?: return null 39 | return allBodyFunctions 40 | .filter { it.name == "onCreate" } 41 | .mapNotNull { it.bodyBlockExpression?.children?.toList() } 42 | .flatten() 43 | .firstOrNull { element -> element.text.contains("setContentView(R.layout.") } 44 | } 45 | 46 | private companion object { 47 | private const val SET_CONTENT_VIEW_TEMPLATE = "setContentView(%s.root)" 48 | } 49 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/programadorthi/migration/migration/CommonAndroidClassMigration.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.migration.migration 2 | 3 | import android.databinding.tool.ext.parseXmlResourceReference 4 | import android.databinding.tool.store.ResourceBundle 5 | import android.databinding.tool.writer.ViewBinder 6 | import com.android.tools.idea.databinding.psiclass.LightBindingClass 7 | import com.intellij.psi.PsiReference 8 | import dev.programadorthi.migration.ext.layoutToBindingName 9 | import dev.programadorthi.migration.model.AndroidView 10 | import dev.programadorthi.migration.model.BindingData 11 | import dev.programadorthi.migration.model.BindingFunction 12 | import dev.programadorthi.migration.model.BindingType 13 | import org.jetbrains.kotlin.psi.KtClass 14 | import org.jetbrains.kotlin.psi.KtClassInitializer 15 | 16 | abstract class CommonAndroidClassMigration( 17 | private val ktClass: KtClass, 18 | private val bindingData: List, 19 | private val bindingClass: List, 20 | ) : CommonMigration(ktClass) { 21 | private val bindingPropertyToCreate = mutableSetOf() 22 | private val includePropertyToCreate = mutableSetOf() 23 | private val syntheticBindingPropertyToCreate = mutableSetOf() 24 | 25 | override fun process(androidViews: List, viewHolderItemViews: List) { 26 | // key: my_view_layout 27 | // value: [androidView1, androidView2, androidView3, ...] 28 | val syntheticsByLayout = androidViews.groupBy { androidView -> 29 | androidView.layoutNameWithoutExtension 30 | } 31 | check(syntheticsByLayout.size <= bindingData.size) { 32 | "Invalid operation. Current class [${ktClass.name}] is referencing more layouts than in the import list" 33 | } 34 | 35 | // All layouts found in the tags 36 | val includedLayouts = bindingData 37 | .map { it.baseLayoutModel.sortedTargets } 38 | .flatten() 39 | .filterNot { it.includedLayout.isNullOrBlank() } 40 | .associate { it.includedLayout to it.id.parseXmlResourceReference().name } 41 | 42 | for (data in bindingData) { 43 | val bindings = data.baseLayoutModel.sortedTargets 44 | val syntheticsById = 45 | syntheticsByLayout[data.baseLayoutModel.baseFileName]?.associateBy { it.viewId } ?: continue 46 | check(syntheticsById.size <= bindings.size) { 47 | "Class has more references than IDs in the layout ${data.layoutName}" 48 | } 49 | 50 | // Using ID as property name instead of viewBinding camelCase 51 | val viewBindingPropertyName = when (val includeId = includedLayouts[data.layoutName]) { 52 | null -> data.baseLayoutModel.bindingClassName.replaceFirstChar { it.lowercase() } 53 | else -> includeId 54 | } 55 | 56 | createBindingProperties( 57 | bindings = bindings, 58 | syntheticsById = syntheticsById, 59 | syntheticsByLayout = syntheticsByLayout, 60 | viewBindingPropertyName = viewBindingPropertyName, 61 | ) 62 | 63 | // Stopping here because view binding creates ViewBinding automatically 64 | if (includedLayouts[data.layoutName] != null) continue 65 | 66 | val (bindingFunction, bindingType) = mapToFunctionAndType( 67 | bindingClassName = data.baseLayoutModel.bindingClassName, 68 | propertyName = viewBindingPropertyName, 69 | rootNode = data.rootNode, 70 | ) 71 | bindingPropertyToCreate += provideTemplate( 72 | propertyName = viewBindingPropertyName, 73 | bindingClassName = data.baseLayoutModel.bindingClassName, 74 | bindingFunction = bindingFunction, 75 | bindingType = bindingType, 76 | ) 77 | 78 | addGenericImport(BINDING_FUNCTION_IMPORT_TEMPLATE.format(bindingFunction.value)) 79 | removeExistingViewInflate(layoutName = data.layoutName) 80 | } 81 | 82 | // The loop order matters 83 | addBlankSpace() 84 | for (property in syntheticBindingPropertyToCreate) { 85 | addProperty(content = property) 86 | } 87 | addBlankSpace() 88 | for (property in includePropertyToCreate) { 89 | addProperty(content = property) 90 | } 91 | addBlankSpace() 92 | for (property in bindingPropertyToCreate) { 93 | addProperty(content = property) 94 | } 95 | addBlankSpace() 96 | } 97 | 98 | private fun createBindingProperties( 99 | bindings: List, 100 | syntheticsById: Map, 101 | syntheticsByLayout: Map>, 102 | viewBindingPropertyName: String 103 | ) { 104 | for (binding in bindings) { 105 | // ViewBinding is generated for view having ID only 106 | val viewId = binding.id?.parseXmlResourceReference()?.name ?: continue 107 | 108 | // Well, ID out of synthetic references are not in usage in the class 109 | if (syntheticsById[viewId] == null) continue 110 | 111 | if (binding.isBinder && !binding.includedLayout.isNullOrBlank()) { 112 | createIncludeProperty( 113 | syntheticsByLayout = syntheticsByLayout, 114 | binding = binding, 115 | viewId = viewId, 116 | viewBindingPropertyName = viewBindingPropertyName, 117 | ) 118 | } else if (binding.viewName.endsWith("ViewStub")) { 119 | createViewStubProperty( 120 | viewBindingPropertyName = viewBindingPropertyName, 121 | viewId = viewId, 122 | layoutName = syntheticsById[viewId]?.viewStubLayoutName, 123 | ) 124 | } else { 125 | syntheticBindingPropertyToCreate += SYNTHETIC_BINDING_AS_LAZY_TEMPLATE.format( 126 | viewId, viewBindingPropertyName, viewId, 127 | ) 128 | } 129 | } 130 | } 131 | 132 | private fun createIncludeProperty( 133 | syntheticsByLayout: Map>, 134 | binding: ResourceBundle.BindingTargetBundle, 135 | viewId: String, 136 | viewBindingPropertyName: String 137 | ) { 138 | val viewsInTheIncludedLayout = syntheticsByLayout[binding.includedLayout] ?: return 139 | if (viewsInTheIncludedLayout.isEmpty()) return 140 | val interfaceType = binding.interfaceType.substringAfterLast(".") 141 | // ViewBinding automatically references layouts as Binding too 142 | includePropertyToCreate += SYNTHETIC_BINDING_WITH_INCLUDE_AS_LAZY_TEMPLATE.format( 143 | viewId, interfaceType, viewBindingPropertyName, viewId 144 | ) 145 | } 146 | 147 | private fun createViewStubProperty(viewBindingPropertyName: String, viewId: String, layoutName: String?) { 148 | val bindingClassName = layoutName?.layoutToBindingName() ?: return 149 | bindingClass.filter { klass -> 150 | klass.name == bindingClassName 151 | }.forEach { klass -> 152 | addGenericImport(klass.qualifiedName) 153 | } 154 | syntheticBindingPropertyToCreate += SYNTHETIC_BINDING_WITH_VIEW_STUB_AS_LAZY_TEMPLATE.format( 155 | viewId, viewBindingPropertyName, viewId, bindingClassName, 156 | ) 157 | } 158 | 159 | private fun removeExistingViewInflate(layoutName: String) { 160 | val declarations = ktClass.body?.declarations ?: return 161 | val regex = """inflate\(.*R\.layout\.$layoutName""".toRegex() 162 | val initializers = declarations.filterIsInstance() 163 | for (ini in initializers) { 164 | val children = ini.body?.children ?: continue 165 | val inflatesToRemove = children.filter { it.text.contains(regex) } 166 | if (inflatesToRemove.isEmpty()) continue 167 | if (children.size == 1) { 168 | ini.delete() 169 | } else { 170 | inflatesToRemove.forEach { it.delete() } 171 | } 172 | } 173 | } 174 | 175 | private fun addBlankSpace() { 176 | val body = ktClass.body ?: return 177 | val whitespace = psiFactory.createWhiteSpace("\n") 178 | body.addAfter(whitespace, body.lBrace) 179 | } 180 | 181 | private fun addProperty(content: String) { 182 | val body = ktClass.body ?: return 183 | val property = psiFactory.createProperty(content) 184 | body.addAfter(property, body.lBrace) 185 | } 186 | 187 | private fun provideTemplate( 188 | propertyName: String, 189 | bindingClassName: String, 190 | bindingFunction: BindingFunction, 191 | bindingType: BindingType, 192 | ): String = BINDING_PROPERTY_TEMPLATE.format( 193 | propertyName, bindingFunction.value, bindingClassName, bindingType.value, 194 | ) 195 | 196 | abstract fun mapToFunctionAndType( 197 | bindingClassName: String, 198 | propertyName: String, 199 | rootNode: ViewBinder.RootNode, 200 | ): Pair 201 | 202 | companion object { 203 | private const val BINDING_FUNCTION_IMPORT_TEMPLATE = "co.stone.cactus.utils.ktx.%s" 204 | private const val BINDING_PROPERTY_TEMPLATE = "private val %s by %s(%s::%s)" 205 | private const val SYNTHETIC_BINDING_AS_LAZY_TEMPLATE = "private val %s by lazy { %s.%s }" 206 | private const val SYNTHETIC_BINDING_WITH_INCLUDE_AS_LAZY_TEMPLATE = "private val %s:%s by lazy { %s.%s }" 207 | private const val SYNTHETIC_BINDING_WITH_VIEW_STUB_AS_LAZY_TEMPLATE = """private val %s by lazy { 208 | val view = %s.%s.inflate() 209 | %s.bind(view) 210 | } 211 | """ 212 | } 213 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/programadorthi/migration/migration/CommonMigration.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.migration.migration 2 | 3 | import com.android.tools.idea.databinding.psiclass.LightBindingClass 4 | import com.intellij.psi.PsiReference 5 | import dev.programadorthi.migration.model.AndroidView 6 | import dev.programadorthi.migration.model.BindingData 7 | import dev.programadorthi.migration.visitor.SyntheticReferenceRecursiveVisitor 8 | import org.jetbrains.kotlin.android.synthetic.AndroidConst 9 | import org.jetbrains.kotlin.psi.KtClass 10 | import org.jetbrains.kotlin.psi.KtPsiFactory 11 | 12 | abstract class CommonMigration( 13 | private val ktClass: KtClass, 14 | ) { 15 | private val mutableBindingsToImport = mutableSetOf() 16 | protected val psiFactory = KtPsiFactory(ktClass.project) 17 | 18 | val bindingsToImport: Set 19 | get() = mutableBindingsToImport 20 | 21 | fun doMigration() { 22 | val visitor = SyntheticReferenceRecursiveVisitor() 23 | ktClass.accept(visitor) 24 | if (visitor.androidViews.isEmpty()) return 25 | process(visitor.androidViews, visitor.viewHolderItemViews) 26 | } 27 | 28 | protected fun addGenericImport(import: String) { 29 | mutableBindingsToImport += import 30 | } 31 | 32 | protected abstract fun process(androidViews: List, viewHolderItemViews: List) 33 | 34 | companion object { 35 | const val GROUPIE_PACKAGE_PREFIX = "com.xwray.groupie.kotlinandroidextensions" 36 | private const val GROUPIE_ITEM_CLASS = "${GROUPIE_PACKAGE_PREFIX}.Item" 37 | private const val GROUPIE_VIEW_HOLDER_CLASS = "${GROUPIE_PACKAGE_PREFIX}.GroupieViewHolder" 38 | 39 | fun getInstance( 40 | parents: Set, 41 | ktClass: KtClass, 42 | bindingData: List, 43 | bindingClass: List, 44 | ): CommonMigration? { 45 | if (parents.contains(AndroidConst.ACTIVITY_FQNAME) || parents.contains(AndroidConst.DIALOG_FQNAME)) { 46 | return ClassWithSetContentViewMigration(ktClass, bindingData, bindingClass) 47 | } 48 | if (parents.contains(AndroidConst.VIEW_FQNAME)) { 49 | return ViewMigration(ktClass, bindingData, bindingClass) 50 | } 51 | if (parents.contains(GROUPIE_ITEM_CLASS) || parents.contains(GROUPIE_VIEW_HOLDER_CLASS)) { 52 | return GroupieMigration(ktClass, bindingClass) 53 | } 54 | return null 55 | } 56 | } 57 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/programadorthi/migration/migration/FileMigration.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.migration.migration 2 | 3 | import android.databinding.tool.store.LayoutFileParser 4 | import android.databinding.tool.store.ResourceBundle 5 | import android.databinding.tool.util.RelativizableFile 6 | import android.databinding.tool.writer.BaseLayoutModel 7 | import android.databinding.tool.writer.toViewBinder 8 | import com.android.tools.idea.databinding.psiclass.LightBindingClass 9 | import com.android.tools.idea.util.toIoFile 10 | import com.intellij.psi.util.InheritanceUtil 11 | import dev.programadorthi.migration.model.BindingData 12 | import org.jetbrains.kotlin.android.model.AndroidModuleInfoProvider 13 | import org.jetbrains.kotlin.android.synthetic.AndroidConst 14 | import org.jetbrains.kotlin.android.synthetic.res.AndroidLayoutGroupData 15 | import org.jetbrains.kotlin.android.synthetic.res.AndroidResource 16 | import org.jetbrains.kotlin.android.synthetic.res.AndroidVariant 17 | import org.jetbrains.kotlin.android.synthetic.res.CliAndroidLayoutXmlFileManager 18 | import org.jetbrains.kotlin.asJava.elements.KtLightElement 19 | import org.jetbrains.kotlin.idea.caches.project.toDescriptor 20 | import org.jetbrains.kotlin.idea.util.module 21 | import org.jetbrains.kotlin.psi.KtClass 22 | import org.jetbrains.kotlin.psi.KtFile 23 | import org.jetbrains.kotlin.psi.KtImportDirective 24 | import org.jetbrains.kotlin.psi.KtNamedFunction 25 | import org.jetbrains.kotlin.psi.KtPsiFactory 26 | import org.jetbrains.kotlin.resolve.ImportPath 27 | import java.io.File 28 | import java.nio.file.Files 29 | 30 | internal object FileMigration { 31 | private const val PARCELIZE_PACKAGE_PREFIX = "kotlinx.android.parcel" 32 | private val parcelizeImports = setOf("kotlinx.parcelize.Parcelize") 33 | 34 | fun migrate( 35 | ktFile: KtFile, 36 | bindingClass: List, 37 | applicationPackage: String, 38 | moduleInfoProvider: AndroidModuleInfoProvider, 39 | parcelizeStatusProvider: ParcelizeStatusProvider, 40 | ) { 41 | val syntheticImports = ktFile.importDirectives.filter(::shouldIMigrate) 42 | if (syntheticImports.isEmpty()) return 43 | 44 | if (syntheticImports.all(::isParcelize)) { 45 | ParcelizeMigration.migrate(ktFile, parcelizeStatusProvider) 46 | updateImports(ktFile, syntheticImports, parcelizeImports) 47 | } else { 48 | lookupForReferences( 49 | ktFile = ktFile, 50 | bindingClass = bindingClass, 51 | applicationPackage = applicationPackage, 52 | syntheticImports = syntheticImports, 53 | moduleInfoProvider = moduleInfoProvider, 54 | ) 55 | } 56 | } 57 | 58 | private fun lookupForReferences( 59 | ktFile: KtFile, 60 | bindingClass: List, 61 | applicationPackage: String, 62 | syntheticImports: List, 63 | moduleInfoProvider: AndroidModuleInfoProvider, 64 | ) { 65 | val lookupLayouts = syntheticImports 66 | .filterNot(::isParcelize) 67 | .mapNotNull { 68 | it.importPath?.pathStr 69 | ?.substringBeforeLast(".") 70 | ?.removeSuffix(".view") 71 | } 72 | .toSet() 73 | 74 | val variants = moduleInfoProvider.getActiveSourceProviders().map { active -> 75 | AndroidVariant( 76 | name = active.name, 77 | resDirectories = active.resDirectories.mapNotNull { it.canonicalPath }, 78 | ) 79 | } 80 | val layoutXmlFileManager = CliAndroidLayoutXmlFileManager( 81 | project = ktFile.project, 82 | applicationPackage = applicationPackage, 83 | variants = variants, 84 | ) 85 | val moduleData = layoutXmlFileManager.getModuleData() 86 | val moduleDescriptor = ktFile.module?.toDescriptor() ?: error("Module descriptor not found for ${ktFile.name}") 87 | val layoutWithResources = mutableMapOf>() 88 | val resourceBundle = ResourceBundle(applicationPackage, true) 89 | for (variantData in moduleData.variants) { 90 | for ((layoutName, layouts) in variantData.layouts) { 91 | val packageFqName = AndroidConst.SYNTHETIC_PACKAGE + '.' + variantData.variant.name + '.' + layoutName 92 | if (lookupLayouts.contains(packageFqName)) { 93 | layoutWithResources[layoutName] = layoutXmlFileManager.extractResources( 94 | layoutGroupFiles = AndroidLayoutGroupData(layoutName, layouts), 95 | module = moduleDescriptor, 96 | ) 97 | val tempDirPath = Files.createTempDirectory("res-stripped") 98 | for (layout in layouts) { 99 | val bundle = LayoutFileParser.parseXml( 100 | RelativizableFile.fromAbsoluteFile(layout.virtualFile.toIoFile()), 101 | File(tempDirPath.toFile(), layout.name), 102 | applicationPackage, 103 | { it }, 104 | true, 105 | false, 106 | ) 107 | resourceBundle.addLayoutBundle(bundle, true) 108 | } 109 | if (lookupLayouts.size == layoutWithResources.size) { 110 | break 111 | } 112 | } 113 | } 114 | } 115 | resourceBundle.validateAndRegisterErrors() 116 | 117 | // Sort the layout bindings to ensure deterministic order 118 | val layoutBindings = resourceBundle.allLayoutFileBundlesInSource 119 | .groupBy(ResourceBundle.LayoutFileBundle::getFileName).toSortedMap() 120 | 121 | val bindingData = mutableListOf() 122 | for ((layoutName, variations) in layoutBindings) { 123 | val baseLayoutModel = BaseLayoutModel(variations, null) 124 | bindingData += BindingData( 125 | layoutName = layoutName, 126 | resources = layoutWithResources[layoutName] ?: emptyList(), 127 | baseLayoutModel = baseLayoutModel, 128 | rootNode = baseLayoutModel.toViewBinder().rootNode, 129 | ) 130 | } 131 | 132 | val bindingsToImport = mutableSetOf() 133 | bindingsToImport.addAll( 134 | bindingData.map { 135 | val model = it.baseLayoutModel 136 | "${model.bindingClassPackage}.${model.bindingClassName}" 137 | } 138 | ) 139 | bindingsToImport.addAll(processEachClass(ktFile, bindingData, bindingClass)) 140 | 141 | if (syntheticImports.any(::isParcelize)) { 142 | bindingsToImport.addAll(parcelizeImports) 143 | } 144 | 145 | // Avoiding remove imports from class not supported yet 146 | if (bindingsToImport.isNotEmpty()) { 147 | updateImports(ktFile, syntheticImports, bindingsToImport) 148 | } 149 | } 150 | 151 | private fun processEachClass( 152 | ktFile: KtFile, 153 | bindingData: List, 154 | bindingClass: List, 155 | ): Set { 156 | val bindingsToImport = mutableSetOf() 157 | for (psiClass in ktFile.classes) { 158 | val currentClass = when (psiClass) { 159 | is KtLightElement<*, *> -> psiClass.kotlinOrigin as? KtClass ?: continue 160 | is KtClass -> psiClass 161 | else -> error("Not supported class type to migrate --> ${psiClass.name}") 162 | } 163 | val parents = InheritanceUtil.getSuperClasses(psiClass).mapNotNull { it.qualifiedName }.toSet() 164 | val migration = CommonMigration.getInstance(parents, currentClass, bindingData, bindingClass) 165 | if (migration != null) { 166 | migration.doMigration() 167 | bindingsToImport.addAll(migration.bindingsToImport) 168 | } 169 | } 170 | return bindingsToImport 171 | } 172 | 173 | private fun updateImports( 174 | ktFile: KtFile, 175 | syntheticImports: List, 176 | bindingsToImport: Set, 177 | ) { 178 | val importList = ktFile.importList ?: return 179 | val psiFactory = KtPsiFactory(ktFile.project) 180 | for (import in bindingsToImport) { 181 | val importPath = ImportPath.fromString(import) 182 | val importDirective = psiFactory.createImportDirective(importPath) 183 | val newLine = psiFactory.createWhiteSpace("\n") 184 | importList.add(newLine) 185 | importList.add(importDirective) 186 | } 187 | for (importDirective in syntheticImports) { 188 | importDirective.delete() 189 | } 190 | } 191 | 192 | private fun isParcelize(importDirective: KtImportDirective): Boolean { 193 | val pathStr = importDirective.importPath?.pathStr ?: return false 194 | return pathStr.startsWith(PARCELIZE_PACKAGE_PREFIX) 195 | } 196 | 197 | private fun shouldIMigrate(importDirective: KtImportDirective): Boolean { 198 | val pathStr = importDirective.importPath?.pathStr ?: return false 199 | return pathStr.startsWith(AndroidConst.SYNTHETIC_PACKAGE) || 200 | pathStr.startsWith(CommonMigration.GROUPIE_PACKAGE_PREFIX) || 201 | isParcelize(importDirective) 202 | } 203 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/programadorthi/migration/migration/GroupieMigration.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.migration.migration 2 | 3 | import com.android.tools.idea.databinding.psiclass.LightBindingClass 4 | import com.intellij.psi.PsiElement 5 | import com.intellij.psi.PsiReference 6 | import com.intellij.psi.util.parents 7 | import dev.programadorthi.migration.ext.layoutToBindingName 8 | import dev.programadorthi.migration.model.AndroidView 9 | import dev.programadorthi.migration.model.BindingData 10 | import org.jetbrains.kotlin.psi.KtClass 11 | import org.jetbrains.kotlin.psi.KtNamedDeclaration 12 | import org.jetbrains.kotlin.psi.KtTypeReference 13 | import org.jetbrains.kotlin.psi.psiUtil.findFunctionByName 14 | import org.jetbrains.kotlin.psi.psiUtil.getValueParameterList 15 | import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull 16 | 17 | internal class GroupieMigration( 18 | private val ktClass: KtClass, 19 | private val bindingClass: List, 20 | ) : CommonMigration(ktClass) { 21 | 22 | override fun process(androidViews: List, viewHolderItemViews: List) { 23 | // If you are using more functions than bind(GroupieViewHolder, Int), we look for them 24 | val referencesByFunction = mutableMapOf>() 25 | for (view in androidViews) { 26 | val parents = view.reference?.element?.parents(true) ?: continue 27 | parents 28 | .filterIsInstance() 29 | .associateWith { it.lookupForGroupieViewHolderParameters() } 30 | .filterValues { it.isNotEmpty() } 31 | .forEach { (ktNamedDeclaration, _) -> 32 | val currentList = referencesByFunction[ktNamedDeclaration] ?: emptyList() 33 | referencesByFunction[ktNamedDeclaration] = currentList + view 34 | } 35 | } 36 | 37 | val bindingReferences = mutableSetOf() 38 | for ((func, views) in referencesByFunction) { 39 | val candidates = mutableListOf() 40 | val idsInsideFunction = views.map { it.viewId }.toSet() 41 | val bindingNames = views.map { androidView -> 42 | androidView.layoutNameWithoutExtension.layoutToBindingName() 43 | }.toSet() 44 | for (bindingName in bindingNames) { 45 | val candidatesByName = bindingClass 46 | .filter { klass -> klass.name == bindingName } 47 | .associateWith { it.fields } 48 | for ((candidateClass, fields) in candidatesByName) { 49 | val fieldNames = fields.map { it.name }.toSet() 50 | if (fieldNames.containsAll(idsInsideFunction)) { 51 | candidates += candidateClass 52 | } 53 | } 54 | } 55 | // Not found or multiple references are not supported 56 | if (candidates.size == 1) { 57 | val candidate = candidates.first() 58 | replaceFunctionParameterType( 59 | parameters = func.lookupForGroupieViewHolderParameters(), 60 | layoutNameAsBinding = candidate.name, 61 | ) 62 | addGenericImport(candidate.qualifiedName) 63 | } 64 | bindingReferences.addAll(candidates) 65 | } 66 | 67 | // itemView.layoutId can't be replaced by root.layoutId 68 | if (viewHolderItemViews.isNotEmpty()) { 69 | val property = psiFactory.createExpression("root") 70 | for (itemView in viewHolderItemViews) { 71 | itemView.element.replace(property) 72 | } 73 | } 74 | 75 | // Well, maybe happen and is good to abort 76 | if (bindingReferences.isEmpty()) return 77 | 78 | // Has current class references to multiple layouts? Is a generic ViewHolder? 79 | if (bindingReferences.size > 1) { 80 | addGenericImport("androidx.viewbinding.ViewBinding") 81 | } 82 | 83 | val bindingName = when { 84 | bindingReferences.size > 2 -> "ViewBinding" 85 | else -> bindingReferences.first().name 86 | } 87 | 88 | tryUpdateViewAttachedOrDetachedFromWindow(bindingName) 89 | tryReplaceSuperType(bindingName) 90 | tryAddInitializeBindingFunction(bindingName) 91 | } 92 | 93 | private fun tryUpdateViewAttachedOrDetachedFromWindow(bindingName: String) { 94 | val onViewParams = lookupForFunctionByName(name = "onViewAttachedToWindow") + 95 | lookupForFunctionByName(name = "onViewDetachedFromWindow") 96 | if (onViewParams.isEmpty()) return 97 | replaceFunctionParameterType( 98 | parameters = onViewParams, 99 | layoutNameAsBinding = "$GROUPIE_VIEW_HOLDER<$bindingName>", 100 | ) 101 | addGenericImport("com.xwray.groupie.viewbinding.$GROUPIE_VIEW_HOLDER") 102 | } 103 | 104 | private fun tryReplaceSuperType(bindingName: String) { 105 | val oldSuperTypes = ktClass.superTypeListEntries.filter { type -> 106 | type.text.startsWith("Item") || type.text.startsWith("Entry") 107 | } 108 | if (oldSuperTypes.isNotEmpty()) { 109 | val superType = psiFactory.createSuperTypeCallEntry("BindableItem<$bindingName>()") 110 | for (old in oldSuperTypes) { 111 | old.replace(superType) 112 | } 113 | addGenericImport("com.xwray.groupie.viewbinding.BindableItem") 114 | } 115 | } 116 | 117 | private fun tryAddInitializeBindingFunction(bindingName: String) { 118 | val bindFunction = ktClass.findFunctionByName("bind") ?: return 119 | val whitespace = psiFactory.createWhiteSpace("\n") 120 | val function = psiFactory.createFunction( 121 | "override fun initializeViewBinding(view: View): $bindingName =\n" + 122 | " $bindingName.bind(view)" 123 | ) 124 | bindFunction.parent.run { 125 | addBefore(function, bindFunction) 126 | addBefore(whitespace, bindFunction) 127 | } 128 | } 129 | 130 | private fun replaceFunctionParameterType( 131 | parameters: List, 132 | layoutNameAsBinding: String, 133 | ) { 134 | if (parameters.isEmpty()) return 135 | val type = psiFactory.createType(layoutNameAsBinding) 136 | for (viewHolder in parameters) { 137 | viewHolder.replace(type) 138 | } 139 | } 140 | 141 | private fun lookupForFunctionByName(name: String): List { 142 | val func = ktClass.findFunctionByName(name) ?: return emptyList() 143 | return func.lookupForGroupieViewHolderParameters() 144 | } 145 | 146 | private fun KtNamedDeclaration.lookupForGroupieViewHolderParameters(): List { 147 | val extensionFunctionType = children.firstIsInstanceOrNull() 148 | val usedAsExtensionType = extensionFunctionType?.text?.contains(GROUPIE_VIEW_HOLDER) 149 | val parameters = getValueParameterList()?.parameters ?: emptyList() 150 | val groupieViewHolderParameters = parameters.map { 151 | it.children.toList() 152 | }.flatten().filter { 153 | it.text.startsWith(GROUPIE_VIEW_HOLDER) 154 | } 155 | if (usedAsExtensionType == true) { 156 | if (groupieViewHolderParameters.isNotEmpty()) { 157 | // Well, how to solve type as extension and parameter in the same function? 158 | // 159 | // fun GroupieViewHolder.doSomething(viewHolder: GroupieViewHolder) { 160 | // impossible to know what synthetics are used here 161 | // } 162 | return emptyList() 163 | } 164 | return listOf(extensionFunctionType) 165 | } 166 | return groupieViewHolderParameters 167 | } 168 | 169 | private companion object { 170 | private const val GROUPIE_VIEW_HOLDER = "GroupieViewHolder" 171 | } 172 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/programadorthi/migration/migration/ParcelizeMigration.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.migration.migration 2 | 3 | import com.intellij.openapi.diagnostic.Logger 4 | import com.intellij.openapi.project.Project 5 | import com.intellij.openapi.vfs.VfsUtil 6 | import com.intellij.psi.PsiFile 7 | import com.intellij.psi.PsiManager 8 | import dev.programadorthi.migration.model.MigrationStatus 9 | import dev.programadorthi.migration.visitor.BuildGradleVisitor 10 | import org.jetbrains.kotlin.idea.configuration.externalProjectPath 11 | import org.jetbrains.kotlin.idea.util.module 12 | import org.jetbrains.kotlin.psi.KtCallExpression 13 | import org.jetbrains.kotlin.psi.KtFile 14 | import org.jetbrains.kotlin.psi.KtPsiFactory 15 | import org.jetbrains.kotlin.psi.KtScript 16 | import org.jetbrains.kotlin.psi.KtScriptInitializer 17 | import org.jetbrains.kotlin.psi.psiUtil.referenceExpression 18 | import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull 19 | import org.jetbrains.plugins.groovy.lang.psi.GroovyFile 20 | import org.jetbrains.plugins.groovy.lang.psi.GroovyFileBase 21 | import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElementFactory 22 | import org.jetbrains.plugins.groovy.lang.psi.api.statements.blocks.GrClosableBlock 23 | import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrApplicationStatement 24 | import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression 25 | import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.path.GrMethodCallExpression 26 | import java.nio.file.Paths 27 | 28 | internal object ParcelizeMigration { 29 | private const val PLUGIN_NAME = "kotlin-parcelize" 30 | private const val GROOVY_PARCELIZE_PLUGIN = """id '$PLUGIN_NAME'""" 31 | private const val KOTLIN_PARCELIZE_PLUGIN = """id("$PLUGIN_NAME")""" 32 | 33 | private val log = Logger.getInstance(ParcelizeMigration::class.java) 34 | 35 | fun migrate(ktFile: KtFile, parcelizeStatusProvider: ParcelizeStatusProvider) { 36 | val modulePath = ktFile.module?.externalProjectPath ?: return 37 | if (parcelizeStatusProvider.currentParcelizeStatus(modulePath) != MigrationStatus.NOT_STARTED) return 38 | 39 | runCatching { 40 | parcelizeStatusProvider.updateParcelizeStatus(modulePath, MigrationStatus.IN_PROGRESS) 41 | 42 | var virtualFile = 43 | VfsUtil.findFile(Paths.get(modulePath, BuildGradleMigration.BUILD_GRADLE_KTS_FILE_NAME), false) 44 | if (virtualFile?.exists() != true) { 45 | virtualFile = 46 | VfsUtil.findFile(Paths.get(modulePath, BuildGradleMigration.BUILD_GRADLE_FILE_NAME), false) 47 | } 48 | 49 | if (virtualFile?.exists() == true) { 50 | checkOrAddPluginTo(PsiManager.getInstance(ktFile.project).findFile(virtualFile)) 51 | } 52 | }.onFailure { 53 | log.error("Failed migrate gradle file parcelize setup", it) 54 | parcelizeStatusProvider.updateParcelizeStatus(modulePath, MigrationStatus.NOT_STARTED) 55 | }.onSuccess { 56 | parcelizeStatusProvider.updateParcelizeStatus(modulePath, MigrationStatus.DONE) 57 | } 58 | } 59 | 60 | private fun checkOrAddPluginTo(psiFile: PsiFile?) { 61 | if (psiFile is KtFile) { 62 | visitKtFile(psiFile) 63 | } else if (psiFile is GroovyFile) { 64 | visitGroovyFile(psiFile) 65 | } 66 | } 67 | 68 | /** 69 | * build.gradle file 70 | */ 71 | private fun visitGroovyFile(file: GroovyFileBase) { 72 | for (child in file.children) { 73 | if (child is GrMethodCallExpression) { 74 | visitMethodCallExpression(child) 75 | } 76 | } 77 | } 78 | 79 | /** 80 | * build.gradle.kts file 81 | */ 82 | private fun visitKtFile(file: KtFile) { 83 | file.children 84 | .filterIsInstance() 85 | .forEach(::visitScript) 86 | } 87 | 88 | /** 89 | * Each top-level configuration block as: 90 | * 91 | * plugins {} 92 | * android {} 93 | * dependencies {} 94 | * androidExtensions {} 95 | */ 96 | private fun visitMethodCallExpression(methodCallExpression: GrMethodCallExpression) { 97 | val expression = methodCallExpression.children.firstIsInstanceOrNull() ?: return 98 | if (expression.text == BuildGradleVisitor.PLUGINS_SECTION) { 99 | val closableBlock = methodCallExpression.children.firstIsInstanceOrNull() ?: return 100 | addParcelizePlugin(closableBlock, methodCallExpression.project) 101 | } 102 | } 103 | 104 | /** 105 | * .kts body without packages and imports 106 | */ 107 | private fun visitScript(script: KtScript) { 108 | script.blockExpression.children 109 | .filterIsInstance() 110 | .forEach(::visitScriptInitializer) 111 | } 112 | 113 | /** 114 | * Each top-level configuration block as: 115 | * 116 | * plugins {} 117 | * android {} 118 | * dependencies {} 119 | * androidExtensions {} 120 | */ 121 | private fun visitScriptInitializer(initializer: KtScriptInitializer) { 122 | val parent = initializer.children.firstIsInstanceOrNull() ?: return 123 | val expression = parent.referenceExpression() 124 | if (expression?.text == BuildGradleVisitor.PLUGINS_SECTION) { 125 | addParcelizePlugin(parent, initializer.project) 126 | } 127 | } 128 | 129 | private fun addParcelizePlugin(parent: KtCallExpression, project: Project) { 130 | val blockExpression = parent 131 | .lambdaArguments 132 | .firstOrNull() 133 | ?.getLambdaExpression() 134 | ?.functionLiteral 135 | ?.bodyBlockExpression 136 | val children = blockExpression?.children ?: return 137 | for (child in children) { 138 | if (child.text.contains(PLUGIN_NAME)) return 139 | } 140 | val psiFactory = KtPsiFactory(project) 141 | val whitespace = psiFactory.createWhiteSpace("\n") 142 | val callExpression = psiFactory.createExpression(KOTLIN_PARCELIZE_PLUGIN) 143 | blockExpression.addBefore(whitespace, blockExpression.rBrace) 144 | blockExpression.addBefore(callExpression, blockExpression.rBrace) 145 | } 146 | 147 | private fun addParcelizePlugin(grClosableBlock: GrClosableBlock, project: Project) { 148 | var anchor: GrApplicationStatement? = null 149 | for (child in grClosableBlock.children) { 150 | if (child.text.contains(PLUGIN_NAME)) return 151 | if (child is GrApplicationStatement) { 152 | anchor = child 153 | } 154 | } 155 | if (anchor != null) { 156 | val callExpression = GroovyPsiElementFactory 157 | .getInstance(project) 158 | .createExpressionFromText(GROOVY_PARCELIZE_PLUGIN) 159 | grClosableBlock.addAfter(callExpression, anchor) 160 | } 161 | } 162 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/programadorthi/migration/migration/ParcelizeStatusProvider.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.migration.migration 2 | 3 | import dev.programadorthi.migration.model.MigrationStatus 4 | 5 | internal interface ParcelizeStatusProvider { 6 | fun currentParcelizeStatus(path: String): MigrationStatus 7 | 8 | fun updateParcelizeStatus(path: String, status: MigrationStatus) 9 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/programadorthi/migration/migration/ViewMigration.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.migration.migration 2 | 3 | import android.databinding.tool.writer.ViewBinder 4 | import com.android.tools.idea.databinding.psiclass.LightBindingClass 5 | import com.android.tools.idea.kotlin.getQualifiedName 6 | import dev.programadorthi.migration.model.BindingData 7 | import dev.programadorthi.migration.model.BindingFunction 8 | import dev.programadorthi.migration.model.BindingType 9 | import org.jetbrains.kotlin.psi.KtClass 10 | 11 | internal class ViewMigration( 12 | private val ktClass: KtClass, 13 | bindingData: List, 14 | bindingClass: List, 15 | ) : CommonAndroidClassMigration(ktClass, bindingData, bindingClass) { 16 | override fun mapToFunctionAndType( 17 | bindingClassName: String, 18 | propertyName: String, 19 | rootNode: ViewBinder.RootNode, 20 | ): Pair { 21 | if (rootNode is ViewBinder.RootNode.Merge) { 22 | // private val propertyName by viewBindingMergeTag(viewBindingName::inflate) 23 | return BindingFunction.AS_MERGE to BindingType.INFLATE 24 | } 25 | if (rootNode is ViewBinder.RootNode.View && rootNode.type.toString() == ktClass.getQualifiedName()) { 26 | // private val propertyName by viewBinding(viewBindingName::bind) 27 | return BindingFunction.DEFAULT to BindingType.BIND 28 | } 29 | // private val propertyName by viewBindingAsChild(viewBindingName::inflate) 30 | return BindingFunction.AS_CHILD to BindingType.INFLATE 31 | } 32 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/programadorthi/migration/model/AndroidView.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.migration.model 2 | 3 | import com.intellij.psi.PsiReference 4 | 5 | data class AndroidView( 6 | val reference: PsiReference?, 7 | val layoutNameWithoutExtension: String, 8 | val rootTagName: String, 9 | val viewId: String, 10 | val includeLayoutName: String? = null, 11 | val viewStubLayoutName: String? = null, 12 | ) 13 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/programadorthi/migration/model/BindingData.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.migration.model 2 | 3 | import android.databinding.tool.writer.BaseLayoutModel 4 | import android.databinding.tool.writer.ViewBinder 5 | import org.jetbrains.kotlin.android.synthetic.res.AndroidResource 6 | 7 | data class BindingData( 8 | val layoutName: String, 9 | val resources: List, 10 | val baseLayoutModel: BaseLayoutModel, 11 | val rootNode: ViewBinder.RootNode, 12 | ) 13 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/programadorthi/migration/model/BindingFunction.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.migration.model 2 | 3 | enum class BindingFunction(val value: String) { 4 | AS_CHILD("viewBindingAsChild"), 5 | AS_MERGE("viewBindingMergeTag"), 6 | DEFAULT("viewBinding"), 7 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/programadorthi/migration/model/BindingType.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.migration.model 2 | 3 | enum class BindingType(val value: String) { 4 | BIND("bind"), 5 | INFLATE("inflate"), 6 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/programadorthi/migration/model/BuildGradleItem.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.migration.model 2 | 3 | import com.intellij.psi.PsiElement 4 | 5 | data class BuildGradleItem(val element: PsiElement) 6 | -------------------------------------------------------------------------------- /src/main/kotlin/dev/programadorthi/migration/model/MigrationStatus.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.migration.model 2 | 3 | internal enum class MigrationStatus { 4 | NOT_STARTED, 5 | IN_PROGRESS, 6 | DONE, 7 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/programadorthi/migration/notification/MigrationNotification.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.migration.notification 2 | 3 | import com.intellij.notification.NotificationGroupManager 4 | import com.intellij.notification.NotificationType 5 | import com.intellij.openapi.project.Project 6 | 7 | object MigrationNotification { 8 | private val manager = NotificationGroupManager.getInstance() 9 | .getNotificationGroup("Synthetic Migration") 10 | private var current: Project? = null 11 | 12 | fun setProject(project: Project) { 13 | current = project 14 | } 15 | 16 | fun showInfo(text: String) { 17 | notify(text, NotificationType.INFORMATION) 18 | } 19 | 20 | fun showError(text: String) { 21 | notify(text, NotificationType.ERROR) 22 | } 23 | 24 | private fun notify(text: String, type: NotificationType) { 25 | val project = requireNotNull(current) { 26 | "No project provided. Have you called MigrationNotification.setProject() before show notification?" 27 | } 28 | manager.createNotification(text, type).notify(project) 29 | } 30 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/programadorthi/migration/processor/MigrationProcessor.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.migration.processor 2 | 3 | import com.android.tools.idea.databinding.psiclass.LightBindingClass 4 | import com.intellij.codeInsight.actions.AbstractLayoutCodeProcessor 5 | import com.intellij.openapi.application.ReadAction 6 | import com.intellij.openapi.diagnostic.Logger 7 | import com.intellij.openapi.editor.ex.util.EditorScrollingPositionKeeper 8 | import com.intellij.openapi.module.Module 9 | import com.intellij.openapi.progress.ProcessCanceledException 10 | import com.intellij.openapi.project.Project 11 | import com.intellij.psi.PsiDirectory 12 | import com.intellij.psi.PsiDocumentManager 13 | import com.intellij.psi.PsiFile 14 | import com.intellij.util.IncorrectOperationException 15 | import com.intellij.util.SlowOperations 16 | import dev.programadorthi.migration.migration.BuildGradleMigration 17 | import dev.programadorthi.migration.migration.BuildGradleStatusProvider 18 | import dev.programadorthi.migration.migration.FileMigration 19 | import dev.programadorthi.migration.migration.ParcelizeStatusProvider 20 | import dev.programadorthi.migration.model.MigrationStatus 21 | import dev.programadorthi.migration.notification.MigrationNotification 22 | import org.jetbrains.kotlin.android.model.AndroidModuleInfoProvider 23 | import org.jetbrains.kotlin.psi.KtFile 24 | import org.jetbrains.plugins.groovy.lang.psi.GroovyFile 25 | import java.util.concurrent.ConcurrentHashMap 26 | import java.util.concurrent.FutureTask 27 | 28 | internal class MigrationProcessor : AbstractLayoutCodeProcessor, BuildGradleStatusProvider, ParcelizeStatusProvider { 29 | private val log = Logger.getInstance(MigrationProcessor::class.java) 30 | 31 | private val bindingClass = mutableListOf() 32 | private val buildGradleStatus = ConcurrentHashMap() 33 | private val parcelizeStatus = ConcurrentHashMap() 34 | 35 | constructor(project: Project, module: Module) : super( 36 | project, 37 | module, 38 | getCommandName(), 39 | getProgressText(), 40 | false, 41 | ) 42 | 43 | constructor( 44 | project: Project, 45 | directory: PsiDirectory, 46 | includeSubdirs: Boolean, 47 | ) : super( 48 | project, 49 | directory, 50 | includeSubdirs, 51 | getProgressText(), 52 | getCommandName(), 53 | false, 54 | ) 55 | 56 | override fun prepareTask(file: PsiFile, processChangedTextOnly: Boolean): FutureTask { 57 | val fileToProcess = ReadAction.compute { 58 | ensureValid(file) 59 | } ?: return FutureTask { false } 60 | return FutureTask { 61 | doMigration(fileToProcess) 62 | } 63 | } 64 | 65 | override fun currentBuildGradleStatus(path: String): MigrationStatus { 66 | return buildGradleStatus[path] ?: MigrationStatus.NOT_STARTED 67 | } 68 | 69 | override fun currentParcelizeStatus(path: String): MigrationStatus { 70 | return parcelizeStatus[path] ?: MigrationStatus.NOT_STARTED 71 | } 72 | 73 | override fun updateBuildGradleStatus(path: String, status: MigrationStatus) { 74 | buildGradleStatus[path] = status 75 | } 76 | 77 | override fun updateParcelizeStatus(path: String, status: MigrationStatus) { 78 | parcelizeStatus[path] = status 79 | } 80 | 81 | fun addBindingClasses(bindingClass: List) { 82 | this.bindingClass.addAll(bindingClass) 83 | } 84 | 85 | private fun doMigration(file: PsiFile): Boolean { 86 | try { 87 | val document = PsiDocumentManager.getInstance(myProject).getDocument(file) 88 | log.assertTrue(infoCollector == null || document != null) 89 | val before = document?.immutableCharSequence 90 | try { 91 | EditorScrollingPositionKeeper.perform(document, true) { 92 | SlowOperations.allowSlowOperations { 93 | if (document != null) { 94 | // In languages that are supported by a non-commit typing assistant (such as C++ and Kotlin), 95 | // the `document` here can be in an uncommitted state. In the case of an external formatter, 96 | // this may be the cause of formatting artifacts 97 | PsiDocumentManager.getInstance(myProject).commitDocument(document) 98 | } 99 | if (isGroovyBuildGradle(file) || isKtsBuildGradle(file)) { 100 | BuildGradleMigration.migrateScript(file, this@MigrationProcessor) 101 | } else if (file is KtFile && !file.isScript()) { 102 | val provider = AndroidModuleInfoProvider.getInstance(file) 103 | val applicationPackage = provider?.getApplicationPackage() 104 | if (provider?.isAndroidModule() != true || applicationPackage == null) { 105 | log.warn("${file.name} is in a module not supported. Migration supports android modules only") 106 | } else { 107 | FileMigration.migrate( 108 | ktFile = file, 109 | bindingClass = bindingClass, 110 | applicationPackage = applicationPackage, 111 | moduleInfoProvider = provider, 112 | parcelizeStatusProvider = this@MigrationProcessor, 113 | ) 114 | } 115 | } 116 | } 117 | } 118 | } catch (pce: ProcessCanceledException) { 119 | log.error(pce) 120 | if (before != null) { 121 | document.setText(before) 122 | } 123 | MigrationNotification.showError("Process cancelled") 124 | return false 125 | } 126 | return true 127 | } catch (ioe: IncorrectOperationException) { 128 | log.error(ioe) 129 | MigrationNotification.showError("Operation not supported") 130 | } 131 | return false 132 | } 133 | 134 | private fun isGroovyBuildGradle(psiFile: PsiFile): Boolean = 135 | psiFile is GroovyFile && psiFile.name == BuildGradleMigration.BUILD_GRADLE_FILE_NAME 136 | 137 | private fun isKtsBuildGradle(psiFile: PsiFile): Boolean = 138 | psiFile is KtFile && psiFile.isScript() && psiFile.name == BuildGradleMigration.BUILD_GRADLE_KTS_FILE_NAME 139 | 140 | private fun ensureValid(file: PsiFile): PsiFile? { 141 | if (file.isValid) { 142 | return file 143 | } 144 | val virtualFile = file.virtualFile 145 | if (virtualFile.isValid) { 146 | return null 147 | } 148 | val provider = file.manager.findViewProvider(virtualFile) ?: return null 149 | val language = file.language 150 | return if (provider.hasLanguage(language)) { 151 | provider.getPsi(language) 152 | } else { 153 | provider.getPsi(provider.baseLanguage) 154 | } 155 | } 156 | 157 | companion object { 158 | private fun getProgressText(): String = "Migrating files..." 159 | fun getCommandName(): String = "Synthetic to ViewBinding" 160 | } 161 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/programadorthi/migration/visitor/BuildGradleVisitor.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.migration.visitor 2 | 3 | import com.intellij.psi.PsiElement 4 | import com.intellij.psi.PsiElementVisitor 5 | import com.intellij.psi.PsiFile 6 | import dev.programadorthi.migration.model.BuildGradleItem 7 | import org.jetbrains.kotlin.psi.KtCallExpression 8 | import org.jetbrains.kotlin.psi.KtFile 9 | import org.jetbrains.kotlin.psi.KtScript 10 | import org.jetbrains.kotlin.psi.KtScriptInitializer 11 | import org.jetbrains.kotlin.psi.psiUtil.referenceExpression 12 | import org.jetbrains.kotlin.utils.addToStdlib.firstIsInstanceOrNull 13 | import org.jetbrains.plugins.groovy.lang.psi.GroovyFile 14 | import org.jetbrains.plugins.groovy.lang.psi.GroovyFileBase 15 | import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression 16 | import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.path.GrMethodCallExpression 17 | 18 | class BuildGradleVisitor : PsiElementVisitor() { 19 | private val mutableBuildGradleItems = mutableListOf() 20 | 21 | val buildGradleItems: List 22 | get() = mutableBuildGradleItems 23 | 24 | /** 25 | * build.gradle or build.gradle.kts file 26 | */ 27 | override fun visitFile(file: PsiFile) { 28 | super.visitFile(file) 29 | when (file) { 30 | is KtFile -> visitKtFile(file) 31 | is GroovyFile -> visitGroovyFile(file) 32 | } 33 | } 34 | 35 | /** 36 | * build.gradle file 37 | */ 38 | private fun visitGroovyFile(file: GroovyFileBase) { 39 | for (child in file.children) { 40 | if (child is GrMethodCallExpression) { 41 | visitMethodCallExpression(child) 42 | } 43 | } 44 | } 45 | 46 | /** 47 | * build.gradle.kts file 48 | */ 49 | private fun visitKtFile(file: KtFile) { 50 | file.children 51 | .filterIsInstance() 52 | .forEach(::visitScript) 53 | } 54 | 55 | /** 56 | * Each top-level configuration block as: 57 | * 58 | * plugins {} 59 | * android {} 60 | * dependencies {} 61 | * androidExtensions {} 62 | */ 63 | private fun visitMethodCallExpression(methodCallExpression: GrMethodCallExpression) { 64 | val expression = methodCallExpression.children.firstIsInstanceOrNull() ?: return 65 | checkExpression(methodCallExpression, expression) 66 | } 67 | 68 | /** 69 | * .kts body without packages and imports 70 | */ 71 | private fun visitScript(script: KtScript) { 72 | script.blockExpression.children 73 | .filterIsInstance() 74 | .forEach(::visitScriptInitializer) 75 | } 76 | 77 | /** 78 | * Each top-level configuration block as: 79 | * 80 | * plugins {} 81 | * android {} 82 | * dependencies {} 83 | * androidExtensions {} 84 | */ 85 | private fun visitScriptInitializer(initializer: KtScriptInitializer) { 86 | val parent = initializer.children.firstIsInstanceOrNull() ?: return 87 | val expression = parent.referenceExpression() 88 | checkExpression(parent, expression) 89 | } 90 | 91 | private fun checkExpression(element: PsiElement, expression: PsiElement?) { 92 | val content = expression?.text ?: return 93 | when (content) { 94 | ANDROID_EXTENSIONS_SECTION -> { 95 | mutableBuildGradleItems += BuildGradleItem(element = element) 96 | } 97 | 98 | CONFIGURE_SECTION -> lookupForAndroidExtensionType(element) 99 | else -> lookupFor(element, content) 100 | } 101 | } 102 | 103 | private fun lookupForAndroidExtensionType(element: PsiElement) { 104 | val content = element.text ?: return 105 | if (content.contains("AndroidExtensionsExtension")) { 106 | mutableBuildGradleItems += BuildGradleItem(element = element) 107 | } 108 | } 109 | 110 | private fun lookupFor(element: PsiElement, content: String) { 111 | val lookupFunction: (PsiElement?) -> Unit = when (content) { 112 | PLUGINS_SECTION -> ::lookupForPlugins 113 | else -> return 114 | } 115 | if (element is GrMethodCallExpression) { 116 | for (argument in element.closureArguments) { 117 | lookupFunction(argument) 118 | } 119 | return 120 | } 121 | if (element is KtCallExpression) { 122 | for (argument in element.lambdaArguments) { 123 | val body = argument.getLambdaExpression()?.functionLiteral?.bodyBlockExpression 124 | lookupFunction(body) 125 | } 126 | } 127 | } 128 | 129 | private fun lookupForPlugins(body: PsiElement?) { 130 | val children = body?.children ?: return 131 | for (child in children) { 132 | if (child.text.contains(androidExtensionsRegex)) { 133 | mutableBuildGradleItems += BuildGradleItem(element = child) 134 | } 135 | } 136 | } 137 | 138 | companion object { 139 | private const val ANDROID_EXTENSIONS_SECTION = "androidExtensions" 140 | private const val CONFIGURE_SECTION = "configure" 141 | const val PLUGINS_SECTION = "plugins" 142 | 143 | private val androidExtensionsRegex = """android[.-]extensions""".toRegex() 144 | } 145 | } -------------------------------------------------------------------------------- /src/main/kotlin/dev/programadorthi/migration/visitor/SyntheticReferenceRecursiveVisitor.kt: -------------------------------------------------------------------------------- 1 | package dev.programadorthi.migration.visitor 2 | 3 | import android.databinding.tool.ext.parseXmlResourceReference 4 | import android.databinding.tool.ext.toCamelCaseAsVar 5 | import com.intellij.psi.PsiElement 6 | import com.intellij.psi.PsiReference 7 | import com.intellij.psi.xml.XmlAttribute 8 | import com.intellij.psi.xml.XmlAttributeValue 9 | import com.intellij.psi.xml.XmlFile 10 | import com.intellij.psi.xml.XmlTag 11 | import dev.programadorthi.migration.model.AndroidView 12 | import org.jetbrains.kotlin.asJava.namedUnwrappedElement 13 | import org.jetbrains.kotlin.nj2k.postProcessing.resolve 14 | import org.jetbrains.kotlin.psi.KtBinaryExpression 15 | import org.jetbrains.kotlin.psi.KtBinaryExpressionWithTypeRHS 16 | import org.jetbrains.kotlin.psi.KtBlockExpression 17 | import org.jetbrains.kotlin.psi.KtCallExpression 18 | import org.jetbrains.kotlin.psi.KtCatchClause 19 | import org.jetbrains.kotlin.psi.KtClass 20 | import org.jetbrains.kotlin.psi.KtClassBody 21 | import org.jetbrains.kotlin.psi.KtClassInitializer 22 | import org.jetbrains.kotlin.psi.KtContainerNode 23 | import org.jetbrains.kotlin.psi.KtDoWhileExpression 24 | import org.jetbrains.kotlin.psi.KtDotQualifiedExpression 25 | import org.jetbrains.kotlin.psi.KtElement 26 | import org.jetbrains.kotlin.psi.KtFile 27 | import org.jetbrains.kotlin.psi.KtForExpression 28 | import org.jetbrains.kotlin.psi.KtIfExpression 29 | import org.jetbrains.kotlin.psi.KtLambdaExpression 30 | import org.jetbrains.kotlin.psi.KtNamedFunction 31 | import org.jetbrains.kotlin.psi.KtProperty 32 | import org.jetbrains.kotlin.psi.KtPropertyAccessor 33 | import org.jetbrains.kotlin.psi.KtPropertyDelegate 34 | import org.jetbrains.kotlin.psi.KtReferenceExpression 35 | import org.jetbrains.kotlin.psi.KtSafeQualifiedExpression 36 | import org.jetbrains.kotlin.psi.KtStringTemplateEntry 37 | import org.jetbrains.kotlin.psi.KtStringTemplateExpression 38 | import org.jetbrains.kotlin.psi.KtTryExpression 39 | import org.jetbrains.kotlin.psi.KtValueArgument 40 | import org.jetbrains.kotlin.psi.KtValueArgumentList 41 | import org.jetbrains.kotlin.psi.KtVisitorVoid 42 | import org.jetbrains.kotlin.psi.KtWhenEntry 43 | import org.jetbrains.kotlin.psi.KtWhenExpression 44 | 45 | class SyntheticReferenceRecursiveVisitor : KtVisitorVoid() { 46 | private val mutableAndroidViews = mutableListOf() 47 | private val mutableViewHolderItemViews = mutableListOf() 48 | 49 | val androidViews: List 50 | get() = mutableAndroidViews 51 | 52 | val viewHolderItemViews: List 53 | get() = mutableViewHolderItemViews 54 | 55 | // ======================================================================== 56 | // Start section that do something 57 | // ======================================================================== 58 | 59 | /** 60 | * Class in the file 61 | */ 62 | override fun visitClass(klass: KtClass) { 63 | super.visitClass(klass) 64 | val body = klass.body ?: return 65 | body.accept(this) 66 | } 67 | 68 | /** 69 | * Reference to other element 70 | */ 71 | override fun visitReferenceExpression(expression: KtReferenceExpression) { 72 | super.visitReferenceExpression(expression) 73 | tryMapXmlAttributeValue(expression.resolve(), expression.references.firstOrNull()) 74 | } 75 | 76 | private fun tryMapXmlAttributeValue(psiElement: PsiElement?, psiReference: PsiReference?) { 77 | if (psiElement == null || psiReference == null || checkAndMapViewHolderType(psiElement, psiReference)) return 78 | if (psiElement is XmlAttributeValue) { 79 | val xmlFile = psiElement.containingFile as? XmlFile ?: return 80 | 81 | // TODO: Are we still need lookup for include layouts? 82 | val elementTag = getXmlAttributeValueTag(psiElement) ?: return 83 | 84 | val viewIdReference = runCatching { 85 | psiElement.value.parseXmlResourceReference() 86 | }.getOrNull() 87 | if (viewIdReference == null || viewIdReference.type != "id") return 88 | 89 | mutableAndroidViews += AndroidView( 90 | reference = psiReference, 91 | layoutNameWithoutExtension = xmlFile.name.removeSuffix(".xml"), 92 | rootTagName = xmlFile.rootTag?.name ?: "", 93 | includeLayoutName = if (elementTag != "include") null else getLayoutName(psiElement), 94 | viewStubLayoutName = if (elementTag != "ViewStub") null else getLayoutName(psiElement), 95 | viewId = viewIdReference.name.toCamelCaseAsVar() 96 | ) 97 | } 98 | } 99 | 100 | private fun checkAndMapViewHolderType(psiElement: PsiElement, psiReference: PsiReference): Boolean { 101 | val currentPsiElement = psiElement.namedUnwrappedElement ?: return false 102 | val parentName = currentPsiElement.parent?.namedUnwrappedElement?.name ?: "" 103 | if (("itemView" == currentPsiElement.name || "containerView" == currentPsiElement.name) && 104 | ("ViewHolder" == parentName || "GroupieViewHolder" == parentName) 105 | ) { 106 | mutableViewHolderItemViews += psiReference 107 | return true 108 | } 109 | return false 110 | } 111 | 112 | /** 113 | * current ref is: "@+id/someIdentifier" 114 | * His parent is: android:id="@+id/someIdentifier" 115 | * His parent from parent is always a tag: 116 | */ 117 | private fun getXmlAttributeValueTag(xmlAttributeValue: XmlAttributeValue): String? { 118 | var currentParent = xmlAttributeValue.parent 119 | while (true) { 120 | if (currentParent is XmlTag) { 121 | return currentParent.name 122 | } 123 | currentParent = currentParent.parent ?: break 124 | } 125 | return null 126 | } 127 | 128 | // TODO: Are we still need lookup for include layouts? 129 | private fun getLayoutName(xmlAttributeValue: XmlAttributeValue): String? { 130 | var parent: PsiElement? = xmlAttributeValue.parent ?: return null 131 | while (parent != null && parent !is XmlTag) { 132 | parent = parent.parent 133 | } 134 | if (parent is XmlTag && (parent.name == "include" || parent.name == "ViewStub")) { 135 | val layoutAttribute = parent.children 136 | .filterIsInstance() 137 | .firstOrNull { it.text.contains(ANDROID_LAYOUT_PREFIX) } 138 | if (layoutAttribute != null) { 139 | val layout = layoutAttribute.text.split(ANDROID_LAYOUT_PREFIX) 140 | if (layout.size > 1) { 141 | return layout[1].removeSuffix("\"") 142 | } 143 | } 144 | } 145 | return null 146 | } 147 | 148 | // ======================================================================== 149 | // End section that do something 150 | // ======================================================================== 151 | 152 | // ======================================================================== 153 | // Start recursive operations section 154 | // ======================================================================== 155 | 156 | /** 157 | * Function argument 158 | * 159 | * doSomething(something) <-- something is an argument 160 | * setOnClickListener {} <-- lambda is an argument for setOnClickListener function 161 | * something.apply {} <-- {} is an argument for apply function 162 | */ 163 | override fun visitArgument(argument: KtValueArgument) { 164 | super.visitArgument(argument) 165 | for (child in argument.children) { 166 | child.accept(this) 167 | } 168 | } 169 | 170 | /** 171 | * Assignment operation (=) 172 | */ 173 | override fun visitBinaryExpression(expression: KtBinaryExpression) { 174 | super.visitBinaryExpression(expression) 175 | for (child in expression.children) { 176 | child.accept(this) 177 | } 178 | } 179 | 180 | /** 181 | * Instance of operation (is) 182 | */ 183 | override fun visitBinaryWithTypeRHSExpression(expression: KtBinaryExpressionWithTypeRHS) { 184 | super.visitBinaryWithTypeRHSExpression(expression) 185 | for (child in expression.children) { 186 | child.accept(this) 187 | } 188 | } 189 | 190 | /** 191 | * Function block {} 192 | */ 193 | override fun visitBlockExpression(expression: KtBlockExpression) { 194 | super.visitBlockExpression(expression) 195 | for (child in expression.children) { 196 | child.accept(this) 197 | } 198 | } 199 | 200 | /** 201 | * Function call 202 | * 203 | * lazy {} 204 | * doSomething() 205 | * doOtherSomething(something) 206 | * setOnClickListener {} 207 | * super.onStart() <-- onStart() is call expression only 208 | * something.apply {} <-- apply{} is call expression only 209 | */ 210 | override fun visitCallExpression(expression: KtCallExpression) { 211 | super.visitCallExpression(expression) 212 | for (child in expression.children) { 213 | child.accept(this) 214 | } 215 | } 216 | 217 | /** 218 | * try {} catch {} 219 | */ 220 | override fun visitCatchSection(catchClause: KtCatchClause) { 221 | super.visitCatchSection(catchClause) 222 | for (child in catchClause.children) { 223 | child.accept(this) 224 | } 225 | } 226 | 227 | /** 228 | * Class content inside {} 229 | */ 230 | override fun visitClassBody(classBody: KtClassBody) { 231 | super.visitClassBody(classBody) 232 | for (child in classBody.children) { 233 | child.accept(this) 234 | } 235 | } 236 | 237 | /** 238 | * init {} 239 | */ 240 | override fun visitClassInitializer(initializer: KtClassInitializer) { 241 | super.visitClassInitializer(initializer) 242 | for (child in initializer.children) { 243 | child.accept(this) 244 | } 245 | } 246 | 247 | /** 248 | * Expression using Dot(.) 249 | * 250 | * super.onStart() 251 | * variable.run() 252 | * noNullable.doSomething() 253 | */ 254 | override fun visitDotQualifiedExpression(expression: KtDotQualifiedExpression) { 255 | super.visitDotQualifiedExpression(expression) 256 | for (child in expression.children) { 257 | child.accept(this) 258 | } 259 | } 260 | 261 | /** 262 | * do {} while() 263 | * 264 | * Check [visitKtElement] to know how access his condition or body content 265 | */ 266 | override fun visitDoWhileExpression(expression: KtDoWhileExpression) { 267 | super.visitDoWhileExpression(expression) 268 | for (child in expression.children) { 269 | child.accept(this) 270 | } 271 | } 272 | 273 | /** 274 | * for () {} 275 | * 276 | * Check [visitKtElement] to know how access his condition or body content 277 | */ 278 | override fun visitForExpression(expression: KtForExpression) { 279 | super.visitForExpression(expression) 280 | for (child in expression.children) { 281 | child.accept(this) 282 | } 283 | } 284 | 285 | /** 286 | * if {} or both if {} else {} 287 | * 288 | * Check [visitKtElement] to know how access his condition or body content 289 | */ 290 | override fun visitIfExpression(expression: KtIfExpression) { 291 | super.visitIfExpression(expression) 292 | for (child in expression.children) { 293 | child.accept(this) 294 | } 295 | } 296 | 297 | /** 298 | * Any element 299 | */ 300 | override fun visitKtElement(element: KtElement) { 301 | super.visitKtElement(element) 302 | // if/else/while/for conditional section () or content body {} 303 | if (element is KtContainerNode) { 304 | for (child in element.children) { 305 | child.accept(this) 306 | } 307 | } 308 | } 309 | 310 | /** 311 | * Kotlin file content 312 | */ 313 | override fun visitKtFile(file: KtFile) { 314 | super.visitKtFile(file) 315 | for (child in file.children) { 316 | if (child is KtClass || child is KtNamedFunction) { 317 | child.accept(this) 318 | } 319 | } 320 | } 321 | 322 | /** 323 | * Function declared in class body or top-level file 324 | * 325 | * class { 326 | * fun ... <- this is a named function 327 | * } 328 | * 329 | * fun ... <- this is a named function too 330 | */ 331 | override fun visitNamedFunction(function: KtNamedFunction) { 332 | super.visitNamedFunction(function) 333 | for (child in function.children) { 334 | child.accept(this) 335 | } 336 | } 337 | 338 | /** 339 | * Any argument with lambda value 340 | * 341 | * myVariable = {} <-- {} is a lambda expression 342 | * setOnClickListener {} <-- {} is a lambda expression 343 | * something.apply {} <-- {} is a lambda expression 344 | * something.map {} <-- {} is a lambda expression 345 | */ 346 | override fun visitLambdaExpression(lambdaExpression: KtLambdaExpression) { 347 | super.visitLambdaExpression(lambdaExpression) 348 | for (child in lambdaExpression.functionLiteral.children) { 349 | child.accept(this) 350 | } 351 | } 352 | 353 | /** 354 | * Property inside class, function or any declaration local 355 | * Almost declared using val or var 356 | */ 357 | override fun visitProperty(property: KtProperty) { 358 | super.visitProperty(property) 359 | for (child in property.children) { 360 | child.accept(this) 361 | } 362 | } 363 | 364 | /** 365 | * get() or set(...) for a property 366 | */ 367 | override fun visitPropertyAccessor(accessor: KtPropertyAccessor) { 368 | super.visitPropertyAccessor(accessor) 369 | for (child in accessor.children) { 370 | child.accept(this) 371 | } 372 | } 373 | 374 | /** 375 | * Delegated property 376 | * 377 | * val variable by lazy {} <-- by is a property delegate 378 | */ 379 | override fun visitPropertyDelegate(delegate: KtPropertyDelegate) { 380 | super.visitPropertyDelegate(delegate) 381 | for (child in delegate.children) { 382 | child.accept(this) 383 | } 384 | } 385 | 386 | /** 387 | * Expression using ?. 388 | * 389 | * nullable?.something() 390 | * fun1()?.fun2() 391 | */ 392 | override fun visitSafeQualifiedExpression(expression: KtSafeQualifiedExpression) { 393 | super.visitSafeQualifiedExpression(expression) 394 | for (child in expression.children) { 395 | child.accept(this) 396 | } 397 | } 398 | 399 | /** 400 | * String in template form "${}" or """${}""" 401 | */ 402 | override fun visitStringTemplateExpression(expression: KtStringTemplateExpression) { 403 | super.visitStringTemplateExpression(expression) 404 | for (child in expression.children) { 405 | child.accept(this) 406 | } 407 | } 408 | 409 | /** 410 | * String template entry "${entry1}..." or """${entry1}something${entry2}...""" 411 | */ 412 | override fun visitStringTemplateEntry(entry: KtStringTemplateEntry) { 413 | super.visitStringTemplateEntry(entry) 414 | for (child in entry.children) { 415 | child.accept(this) 416 | } 417 | } 418 | 419 | /** 420 | * try {} catch {} 421 | */ 422 | override fun visitTryExpression(expression: KtTryExpression) { 423 | super.visitTryExpression(expression) 424 | for (child in expression.children) { 425 | child.accept(this) 426 | } 427 | } 428 | 429 | /** 430 | * Argument list for a function call 431 | * 432 | * fun1(arg1, arg2, ...) 433 | * something.map { arg1, arg2, ... } 434 | */ 435 | override fun visitValueArgumentList(list: KtValueArgumentList) { 436 | super.visitValueArgumentList(list) 437 | for (argument in list.arguments) { 438 | argument.accept(this) 439 | } 440 | } 441 | 442 | /** 443 | * when (...) {} or when {} 444 | */ 445 | override fun visitWhenExpression(expression: KtWhenExpression) { 446 | super.visitWhenExpression(expression) 447 | for (child in expression.children) { 448 | child.accept(this) 449 | } 450 | } 451 | 452 | /** 453 | * when ... { 454 | * entry1 -> ... 455 | * entry2 -> ... 456 | * ... 457 | * else -> ... <-- else is a when entry too 458 | * } 459 | */ 460 | override fun visitWhenEntry(jetWhenEntry: KtWhenEntry) { 461 | super.visitWhenEntry(jetWhenEntry) 462 | for (child in jetWhenEntry.children) { 463 | child.accept(this) 464 | } 465 | } 466 | 467 | // ======================================================================== 468 | // End recursive operations section 469 | // ======================================================================== 470 | 471 | companion object { 472 | private const val ANDROID_LAYOUT_PREFIX = "@layout/" 473 | } 474 | } -------------------------------------------------------------------------------- /src/main/resources/META-INF/plugin.xml: -------------------------------------------------------------------------------- 1 | 2 | dev.programadorthi.synthetictoviewbinding 3 | Kotlin Synthetic to ViewBinding 4 | programadorthi 5 | Support for Migrate from Kotlin synthetics to Jetpack view binding

7 |

Features

8 |
    9 |
  • Activity, Dialog, ViewGroup or View migration
  • 10 |
  • Groupie Item migration with support to add initializeViewBinding(view) function
  • 11 |
  • Replace setContentView(R.layout.name) to setContentView(binding.root)
  • 12 |
  • Remove View.inflate() or LayoutInflate.inflate from init {} blocks
  • 13 |
  • Support for multiple synthetics in the same class
  • 14 |
  • Support to add .root in view binding
  • 15 |
  • Remove plugin and android extensions configurations from build(.gradle|.kts)
  • 16 |
  • Update @Parcelize imports and add plugin to build(.gradle|.kts)
  • 17 |
  • Organize imports, Reformat code, Code cleanup based on your IDE code style settings
  • 18 |
19 |

Find more information on my repository

20 | ]]>
21 | com.intellij.modules.platform 22 | com.intellij.modules.lang 23 | org.jetbrains.kotlin 24 | org.jetbrains.android 25 | 26 | 27 | 28 | 29 | 32 | 33 | 34 | 35 | 36 |
-------------------------------------------------------------------------------- /src/main/resources/META-INF/pluginIcon.svg: -------------------------------------------------------------------------------- 1 | --------------------------------------------------------------------------------