├── .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 |
6 |
7 |
8 |
9 |
12 |
17 |
18 |
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 |
--------------------------------------------------------------------------------